MyGo
Drawing and animation

Guides

Drawing and animation

Painting on elements, shaping text, and values that ease to their targets.

Draw paints on an element after its background, and DrawOver after its children, with a *ui.Painter in DIPs of the window: rectangles with Fill, FillGradient, Stroke and StrokeDashed, Shadow, Line, Image, Clip, paths of lines and curves with FillPath and StrokePath, or in a gradient with FillPathGradient and StrokePathGradient, and text with Text, or RichText in spans of any style, which MeasureText measures first, to center or align it:

ui.Box(c).Height(120).Draw(func(p *ui.Painter, r ui.Rect) {
	var wave ui.Path
	for i := 0; i <= 100; i++ {
		x := r.X + r.W*float32(i)/100
		y := r.Y + r.H/2 + 40*float32(math.Sin(float64(i)/8))
		if i == 0 {
			wave.MoveTo(x, y)
		} else {
			wave.LineTo(x, y)
		}
	}
	p.StrokePath(&wave, 2, c.Theme().Accent)
})
ui.Box(c).Size(200, 40).Draw(func(p *ui.Painter, r ui.Rect) {
	label := ui.Span{Text: "42%", Weight: 600, Color: c.Theme().TextMuted}
	w, h := p.MeasureText(0, label)
	p.RichText(r.X+(r.W-w)/2, r.Y+(r.H-h)/2, 0, label)
})

Draw functions only paint: MyGo may call them more than once a frame. Painter.Icon and Painter.Image draw SVGs and bitmaps, and Painter.FocusRing the ring of the keyboard focus, for widgets that draw their own.

Shaping text

Text that a widget lays out itself, as in the cells of a grid, is shaped once with ui.Shape, which returns glyphs placed along a line by the system's text engine, with ligatures, kerning and fallback fonts, and the runes each comes from. Move their X and draw them with Painter.Glyphs; Font.Metrics returns the font's ascent, descent and line gap, and Painter.Scale the device pixels of a DIP, to line things up with the display's pixels. Shape caches nothing, unlike the text of elements: keep the glyphs of text drawn in many frames. Font.Features turns OpenType features on and off, and Font.Thicken draws text with a thicker stroke on macOS, as Ghostty's font-thicken.

font := ui.Font{Family: "monospace", Size: 13}
glyphs := ui.Shape("grid", font)
for i := range glyphs {
	glyphs[i].X = float32(glyphs[i].Cluster) * cellWidth // one per cell
}
ui.Box(c).Height(20).Draw(func(p *ui.Painter, r ui.Rect) {
	p.Glyphs(glyphs, r.X, r.Y+font.Metrics().Ascent, c.Theme().Text)
})

Animation

For motion, Element.Animate returns a value that eases to a target and draws frames until it gets there:

panel := ui.Column(c).Clip()
width := float32(0)
if app.sidebar {
	width = 280
}
panel.Width(panel.Animate("width", width, 200*time.Millisecond))

AnimateWith takes the easing: ui.Linear, ui.EaseIn, ui.EaseOut (as Animate), ui.EaseInOut, any func(t float32) float32, or ui.Bounce(e), which goes along e and comes back. Loop returns the progress of an animation that starts over every period, for spinners and pulses; Rotate turns an element, as an icon:

spin := ui.Icon(c, loader)
spin.Rotate(spin.Loop("spin", time.Second, ui.Linear) * 360)

skeleton := ui.Box(c).Height(14).Radius(7).Background(t.Border)
skeleton.Opacity(0.4 + 0.6*skeleton.Loop("pulse", 1600*time.Millisecond, ui.Bounce(ui.EaseInOut)))

To animate in other ways, compute from c.Now() and call c.AnimationFrame() in every frame that moves: MyGo draws the next frame when the display can show it, and draws nothing while nothing changes.

A drawing that moves while the layout stays, as a spinner or a chart scrolling by, animates from its Draw instead: compute it from p.Now() and call p.AnimationFrame() while it moves, or p.After(d) when it changes in steps. Unless something else changed, those frames paint the elements of the last frame again without running the view, so they cost only the painting; and an element out of view is not painted, so it asks for none. While other windows cover the window, nothing that moves draws frames, from Draw or the view, until it shows again:

ui.Box(c).Height(4).Draw(func(p *ui.Painter, r ui.Rect) {
	p.AnimationFrame()
	x := r.X + (r.W-20)*float32(p.Now().UnixMilli()%1000)/1000
	p.Fill(ui.Rect{X: x, Y: r.Y, W: 20, H: r.H}, c.Theme().Accent, 2)
})

Such a Draw sees the state as the view last ran: what it reads that changes otherwise is the view's to build anew, after Window.Update.

When the desktop asks for less motion (c.Preferences().ReduceMotion), Animate and AnimateWith go to their target at once. Loop goes on, as the system's spinners do: it shows that something is going on. Motion you compute yourself should read the preference too.