text.go
Functions
func DrawShapedGlyphs
func (c *Context) DrawShapedGlyphs(glyphs []text.ShapedGlyph, face text.Face, x, y float64) {
if face == nil || len(glyphs) == 0 {
return
}
defer c.setGPUClipRect()()
col := FromColor(c.currentColor())
target := c.gpuRenderTarget()
if rc := c.gpuCtxOps(); rc != nil {
if sta, ok := rc.(GPUShapedTextAccelerator); ok {
if sta.DrawShapedGlyphMaskText(target, face, glyphs, x, y, col, c.totalMatrix(), c.deviceScale) == nil {
return
}
}
}
a := Accelerator()
if a != nil {
if sta, ok := a.(GPUShapedTextAccelerator); ok {
if sta.DrawShapedGlyphMaskText(target, face, glyphs, x, y, col, c.totalMatrix(), c.deviceScale) == nil {
return
}
}
}
// Fallback: reconstruct string is not possible from glyphs,
// so render each glyph outline through the fill pipeline.
c.drawShapedGlyphsAsOutlines(glyphs, face, x, y)
}
func DrawString
DrawString draws text at position (x, y) where y is the baseline.
If no font has been set with SetFont, this function does nothing.
If a GPU accelerator is registered and supports text rendering (implements
GPUTextAccelerator), the text is rendered via the GPU MSDF pipeline.
The CTM (Current Transform Matrix) is passed to the GPU so that Scale,
Rotate, and Skew transforms affect text rendering, not just position.
Otherwise, the CPU text pipeline is used with transform-aware rendering:
- Translation-only: bitmap fast path (zero quality loss)
- Uniform scale ≤256px: bitmap at device size (Strategy A, Skia pattern)
- Everything else: glyph outlines as vector paths (Strategy B, Vello pattern)
The baseline is the line on which most letters sit. Characters with
descenders (like 'g', 'j', 'p', 'q', 'y') extend below the baseline.
func (c *Context) DrawString(s string, x, y float64) {
if c.face == nil {
return
}
// Set GPU scissor rect for rectangular clips.
defer c.setGPUClipRect()()
switch c.selectTextStrategy() {
case TextModeGlyphMask:
// Try GPU glyph mask (Tier 6) first; fall back to MSDF, then CPU.
if c.tryGPUGlyphMaskText(s, x, y) {
return
}
if c.tryGPUText(s, x, y) {
return
}
c.drawStringCPU(s, x, y)
case TextModeAliased:
// Aliased text through glyph mask pipeline with binary rasterization.
// Same Tier 6 atlas + GPU path, but NoAAFiller instead of AnalyticFiller.
if c.tryGPUGlyphMaskTextAliased(s, x, y) {
return
}
// CPU fallback: bitmap rasterization (already non-AA at small sizes).
c.flushGPUAccelerator()
c.drawStringCPU(s, x, y)
case TextModeMSDF:
// Try GPU MSDF first; fall back to CPU if unavailable.
if c.tryGPUText(s, x, y) {
return
}
c.drawStringCPU(s, x, y)
case TextModeVector:
// Vector text is rendered as glyph outline paths through the normal
// fill pipeline (doFill). This routes through GPU stencil+cover when
// a SurfaceTarget is active, or CPU when standalone. No explicit
// flush here — doFill() manages GPU/CPU routing and any necessary
// flush internally. An explicit flush would create a mid-frame
// render pass with LoadOpClear, wiping previously drawn content.
c.drawStringAsOutlines(s, x, y)
case TextModeBitmap:
// Skip GPU entirely, use CPU pipeline directly.
c.flushGPUAccelerator()
c.drawStringCPU(s, x, y)
default: // TextModeAuto — current behavior
// ADR-027: CJK ≤64px → prefer Tier 6 (bitmap) over MSDF.
// MSDF at 64px reference produces stroke fusion on dense CJK characters.
if c.isCJKText(s) && c.glyphMaskDeviceSize() <= glyphMaskMaxSizeCJK {
if c.tryGPUGlyphMaskText(s, x, y) {
return
}
}
if c.tryGPUText(s, x, y) {
return
}
c.drawStringCPU(s, x, y)
}
}
func DrawStringAnchored
DrawStringAnchored draws text with an anchor point.
The anchor point is specified by ax and ay, which are in the range [0, 1].
(0, 0) = top-left
(0.5, 0.5) = center
(1, 1) = bottom-right
The text is positioned so that the anchor point is at (x, y).
func (c *Context) DrawStringAnchored(s string, x, y, ax, ay float64) {
if c.face == nil {
return
}
// Measure the text and calculate offset based on anchor.
// The anchor maps linearly within the text bounding box:
// ay=0 → y is the top of the text (baseline = y + ascent)
// ay=0.5 → y is the vertical center (baseline = y + ascent - h/2)
// ay=1 → y is the bottom (baseline = y + ascent - h)
// Formula: baseline = y + ascent - ay * h
// where h = ascent + descent (visual bounding box, no lineGap).
w, _ := text.Measure(s, c.face)
metrics := c.face.Metrics()
h := metrics.Ascent + metrics.Descent
x -= w * ax
y = y + metrics.Ascent - ay*h
// Delegate to DrawString which handles TextMode routing.
c.DrawString(s, x, y)
}
func DrawStringWrapped
DrawStringWrapped wraps text to the given width and draws it with alignment.
The text is positioned relative to (x, y) using the anchor (ax, ay):
(0, 0) = top-left of the text block is at (x, y)
(0.5, 0.5) = center of the text block is at (x, y)
(1, 1) = bottom-right of the text block is at (x, y)
The lineSpacing parameter multiplies the font's natural line height
(1.0 = normal, 1.5 = 50% extra space between lines).
The align parameter controls horizontal alignment within the wrapped width.
If no font face is set, this method does nothing.
This method is compatible with fogleman/gg's DrawStringWrapped.
func (c *Context) DrawStringWrapped(s string, x, y, ax, ay, width, lineSpacing float64, align Align) {
if c.face == nil {
return
}
lines := c.WordWrap(s, width)
if len(lines) == 0 {
return
}
metrics := c.face.Metrics()
fh := metrics.LineHeight()
// Visual height of the text block:
// - (n-1) inter-line gaps of fh*lineSpacing
// - ascent above first baseline + descent below last baseline
n := float64(len(lines))
h := (n-1)*fh*lineSpacing + metrics.Ascent + metrics.Descent
// Adjust starting position by anchor (bounding-box model):
// ay=0 → y is the top of the block (first baseline = y + ascent)
// ay=0.5 → y is the vertical center
// ay=1 → y is the bottom of the block
// Formula: first_baseline = y + ascent - ay * h
x -= ax * width
y = y + metrics.Ascent - ay*h
// Adjust x base for alignment
switch align {
case text.AlignCenter:
x += width / 2
case text.AlignRight:
x += width
}
for _, line := range lines {
drawX := x
switch align {
case text.AlignCenter:
lw, _ := c.MeasureString(line)
drawX = x - lw/2
case text.AlignRight:
lw, _ := c.MeasureString(line)
drawX = x - lw
}
c.DrawString(line, drawX, y)
y += fh * lineSpacing
}
}
func Font
Font returns the current font face.
Returns nil if no font has been set.
func (c *Context) Font() text.Face {
return c.face
}
func LoadFontFace
LoadFontFace loads a font from a file and sets it as the current font.
The size is specified in points.
Deprecated: Use text.NewFontSourceFromFile and SetFont instead.
This method is provided for convenience and backward compatibility.
Example (new way):
source, err := text.NewFontSourceFromFile("font.ttf")
if err != nil {
return err
}
face := source.Face(12.0)
ctx.SetFont(face)
func (c *Context) LoadFontFace(path string, points float64) error {
source, err := text.NewFontSourceFromFile(path)
if err != nil {
return err
}
c.face = source.Face(points)
return nil
}
func MeasureMultilineString
MeasureMultilineString measures text that may contain newlines.
The lineSpacing parameter is a multiplier for the font's natural line height
(1.0 = normal spacing, 1.5 = 50% extra space between lines).
Returns (width, height) where width is the maximum line width and height
is the total height of all lines with the given line spacing.
If no font face is set, returns (0, 0).
This method is compatible with fogleman/gg's MeasureMultilineString.
func (c *Context) MeasureMultilineString(s string, lineSpacing float64) (width, height float64) {
if c.face == nil {
return 0, 0
}
lines := splitLines(s)
metrics := c.face.Metrics()
fh := metrics.LineHeight()
for _, line := range lines {
lw, _ := text.Measure(line, c.face)
if lw > width {
width = lw
}
}
// Visual height: ascent above first baseline + (n-1) inter-line gaps + descent below last baseline.
n := float64(len(lines))
height = (n-1)*fh*lineSpacing + metrics.Ascent + metrics.Descent
return
}
func MeasureString
MeasureString returns the dimensions of text in pixels.
Returns (width, height) where:
- width is the horizontal advance of the text
- height is the line height (ascent + descent + line gap)
If no font has been set, returns (0, 0).
func (c *Context) MeasureString(s string) (w, h float64) {
if c.face == nil {
return 0, 0
}
return text.Measure(s, c.face)
}
func SetFont
SetFont sets the current font face for text drawing.
The face should be created from a FontSource.
Example:
source, _ := text.NewFontSourceFromFile("font.ttf")
face := source.Face(12.0)
ctx.SetFont(face)
func (c *Context) SetFont(face text.Face) {
c.face = face
}
func StrokeString
StrokeString strokes text outlines at position (x, y) where y is the baseline.
The stroke width, cap, join, and dash come from the current paint state.
Unlike DrawString, StrokeString always uses vector outlines regardless of the
current TextMode — MSDF and glyph mask pipelines cannot produce stroked text.
If no font has been set with SetFont, this function does nothing.
Enterprise pattern: matches HTML5 Canvas strokeText(), Cairo show_text() + stroke(),
Skia SkPaint::kStroke_Style + drawTextBlob.
func (c *Context) StrokeString(s string, x, y float64) {
if c.face == nil {
return
}
path := c.textOutlinePath(s, x, y)
if path == nil {
return
}
devicePath := path.Transform(c.totalMatrix())
c.trackDamage(devicePath.Bounds())
// Set GPU scissor rect for rectangular clips.
defer c.setGPUClipRect()()
// Save and restore context path — doStroke uses c.path.
savedPath := c.path
c.path = devicePath
_ = c.doStroke()
c.path = savedPath
}
func StrokeStringAnchored
StrokeStringAnchored strokes text outlines with an anchor point.
The anchor point is specified by ax and ay, which are in the range [0, 1].
(0, 0) = top-left
(0.5, 0.5) = center
(1, 1) = bottom-right
The text is positioned so that the anchor point is at (x, y).
The stroke width, cap, join, and dash come from the current paint state.
Always uses vector outlines regardless of TextMode.
func (c *Context) StrokeStringAnchored(s string, x, y, ax, ay float64) {
if c.face == nil {
return
}
w, _ := text.Measure(s, c.face)
metrics := c.face.Metrics()
h := metrics.Ascent + metrics.Descent
x -= w * ax
y = y + metrics.Ascent - ay*h
c.StrokeString(s, x, y)
}
func TextPath
TextPath returns a user-space Path containing the vector outlines of text s
positioned at (x, y) where y is the baseline. The returned path can be filled,
stroked, or used for hit-testing with the caller's own pipeline.
Returns nil if no font is set or the text produces no outlines.
Enterprise pattern: matches HTML5 Canvas addText() (proposed), Cairo text_path(),
Skia SkTextBlob → SkPath (via getPath).
func (c *Context) TextPath(s string, x, y float64) *Path {
if c.face == nil {
return nil
}
return c.textOutlinePath(s, x, y)
}
func WordWrap
WordWrap wraps text to fit within the given width using word boundaries.
Returns a slice of strings, one per wrapped line.
If no font face is set, returns the input string as a single-element slice.
This method is compatible with fogleman/gg's WordWrap.
func (c *Context) WordWrap(s string, w float64) []string {
if c.face == nil {
return []string{s}
}
results := text.WrapText(s, c.face, w, text.WrapWord)
lines := make([]string, len(results))
for i, r := range results {
lines[i] = r.Text
}
return lines
}
DrawShapedGlyphs renders pre-shaped glyphs through the GPU text pipeline
without re-shaping. This implements the ADR-022 "shape once" guarantee:
glyphs are shaped at scene recording time, then rendered here with stored
positions. Falls back to DrawString (re-shaping) if the GPU accelerator
doesn't implement GPUShapedTextAccelerator.
Enterprise pattern: matches Skia drawTextBlob, Vello draw_glyphs.