MyGo
Motion

Guides

Motion

Springs that keep their velocity, curves, delays, values thrown by the pointer, keyframes, timelines that play several tweens on one playhead, and knowing when they are done.

Motion

An element animates values for you: you give the target, it returns the value to show in this frame, and draws frames until the value gets there. The view says where things are; the animation finds the way:

card := ui.Box(c).Size(120, 80).Radius(12).Background(t.Accent)
target := float32(0)
if app.open {
	target = 200
}
card.Translate(card.AnimateTo("x", target, ui.Snappy), 0)

The key ("x") tells apart the values of one element, which needs the same ID from frame to frame: a Key, or the same place among its siblings. A value starts at its first target. Use the value in any way: a transform (see Transforms), a size, an opacity, a color mix, or what a Draw paints.

Animations

An Animation says how a value goes to a new target:

  • Springs, the zero Animation and the presets ui.Smooth (no bounce), ui.Snappy (a little) and ui.Bouncy (a visible one). A spring keeps its velocity when its target changes on the way: clicked again mid-flight, the card above turns around smoothly instead of stopping dead.
  • Curves: a Duration and an Ease (ui.EaseOut when nil), as ui.Animation{Duration: 150 * time.Millisecond, Ease: ui.EaseInOut}. Animate(key, target, d) and AnimateWith(key, target, d, ease) are short for them.

A spring's SpringOptions describe it as one perceives it, with a Duration (500 ms when zero), about how long it takes to arrive, and a Bounce, from 0, arriving without overshooting, toward 1; negative values arrive more slowly still. These are SwiftUI's and Motion's parameters. Stiffness, Damping and Mass describe it physically instead:

gentle := ui.Animation{Spring: ui.SpringOptions{Duration: 700 * time.Millisecond, Bounce: 0.1}}
stiff := ui.Animation{Spring: ui.SpringOptions{Stiffness: 400, Damping: 30}}
x := card.AnimateTo("x", target, gentle)

Spring(key, target, options) is short for AnimateTo with a spring. Springs are solved exactly rather than stepped, so a slow frame does not change their path. A value can change its kind of animation on the way: a spring taking over a curve keeps the curve's velocity.

Easing functions are ui.Linear, ui.EaseIn, ui.EaseOut, ui.EaseInOut, ui.Bounce(e), CSS's ui.CubicBezier(x1, y1, x2, y2), or any func(t float32) float32.

AnimateColor(key, target, a) animates a color the same way, mixed with premultiplied alpha, so that a color fading in from transparent keeps its hue:

row.Background(row.AnimateColor("bg", bg, ui.Animation{Duration: 120 * time.Millisecond}))

Poses that only paint

The values above are read by the view, so every frame that moves runs it. AnimatePose moves an element instead: it translates, scales, turns and fades it, after its own transform, and the frames that move it only paint, without running the view, as Core Animation and the Web Animations API run transforms and opacity. It is the cheapest motion there is, which keeps a large view smooth:

lift := ui.Pose{}
if card.Hovered() {
	lift = ui.Pose{Y: -4, Scale: 1.02}
}
card.AnimatePose(lift, ui.Snappy)

A Pose moves by X and Y, scales by Scale (0 keeps the size), turns by Rotate degrees, and fades by Fade, from 0, opaque, to 1, gone. The pointer, carets and overlays follow the element as it moves, and Transitioning() reports whether its pose was moving.

Poses nest: each acts after those of the elements around it, so an element inside another adds its own motion, along its own curve. In the confetti example, a burst falls and fades, each piece in it bursts out along a curve that slows down, and spins inside: hundreds of pieces fly without building the view again. Each element posed still costs every frame that paints it, so share a pose among the elements that move alike, as the burst's fall, rather than giving each its own.

Delays and staggering

Delay keeps the value going as it was for a while after its target changes. Delays growing with the index stagger a list:

for i, it := range app.items {
	row := ui.Row(c.Key(it.ID))
	a := ui.Snappy
	a.Delay = time.Duration(i) * 30 * time.Millisecond
	row.Opacity(row.AnimateTo("shown", 1, a))
}

Gestures

Draggable lets the pointer move an element and throw it, as motion.dev's drag does: the element follows the pointer, and once let go, the pointer's velocity carries it to where it comes to rest, with a spring:

card := ui.Box(c).Size(90, 60).Radius(10).Background(t.Accent)
card.Draggable(&app.card, ui.DraggableOptions{
	Axis:   ui.Horizontal,
	Bounds: &ui.Rect{W: 200},
	Snap:   ui.Nearest(ui.Point{}, ui.Point{X: 200}),
})

app.card is a ui.Point, the element's offset from its place in the layout. As the pointer drags the element, it follows the pointer; once the pointer lets go, it is where the element comes to rest; and setting it moves the element there, as a button turning a page does. A nil offset is kept in the element. The options say how it moves:

  • Axis, ui.Horizontal or ui.Vertical, is the direction it moves in; both when zero.
  • Bounds hold the offset within a rectangle at rest, from X to X+W and from Y to Y+H. The pointer pulls the element past them less and less far, as iOS holds content pulled past its edges, and it springs back within them once let go. Rigid stops it at them.
  • Snap chooses where it comes to rest, from where the throw would carry it (ui.Project, held within the bounds), so that a flick goes on to the next page or height: ui.Nearest(points...) chooses the nearest of points. A Snap returning its point lets the element glide where the throw carries it. Without Snap, it rests where the pointer lets it go, as an item placed on a canvas.
  • Animation is how it goes to rest, and to an offset the app sets: ui.Smooth when zero. A spring starts at the pointer's velocity.

The pointer drags the element once it moved a few DIPs, and that press is no click; a press on the element as it moves catches it. The element moves after its own transform and its pose, along its parent's axes, so it stays under the pointer as it scales or turns, and it goes to rest in frames that only paint, without building the view, as AnimatePose moves it. Dragging() reports that the pointer drags it, to lift it as it does:

lift := ui.Pose{}
if card.Dragging() {
	lift = ui.Pose{Scale: 1.05}
}
card.AnimatePose(lift, ui.Snappy)

Transitioning() is false once it is at rest, for the view to go on, as removing a row swiped away:

for i := range app.rows {
	r := &app.rows[i]
	row := ui.Row(c).Key(r.ID)
	row.Draggable(&r.swipe, ui.DraggableOptions{
		Axis: ui.Horizontal,
		Snap: func(p ui.Point) ui.Point {
			if p.X < -160 {
				return ui.Point{X: -400} // away to the left
			}
			return ui.Point{}
		},
	})
	if r.swipe.X == -400 && !row.Transitioning() {
		app.remove(r.ID)
	}
}

DragOffset() returns where the element shows in this frame, following the pointer or on its way to rest, for what follows it, as a backdrop fading as a sheet goes down. Reading it builds the view in every frame the element moves in, as AnimateTo does; call it after Draggable:

backdrop := ui.Box(c).Absolute().Fill().Background(ui.RGBA(0, 0, 0, 0.4))
sheet := ui.Column(c).Draggable(&app.sheet, ui.DraggableOptions{
	Axis:   ui.Vertical,
	Bounds: &ui.Rect{H: 400},
	Snap:   ui.Nearest(ui.Point{}, ui.Point{Y: 400}),
})
backdrop.Opacity(1 - sheet.DragOffset().Y/400)

While the desktop asks for less motion, the element follows the pointer, and goes to rest at once.

By hand

For a value other than an offset that follows the pointer, as the angle of a dial, build the gesture from smaller pieces. Jump sets a value at once, following the pointer, and keeps track of how fast it moves; the spring that takes over when the pointer lets go starts at that speed. DragEnded reports the pointer's velocity as it let go, which SpringOptions.Velocity hands to the spring:

dial := ui.Box(c).Size(80, 80)
var turn float32
if dx, _, ok := dial.Dragged(); ok {
	app.turn += dx
	turn = dial.Jump("turn", app.turn)
} else {
	a := ui.Snappy
	if vx, _, ok := dial.DragEnded(); ok {
		// Where the throw would carry it decides the notch it lands on.
		app.turn = float32(math.Round(float64(ui.Project(app.turn, vx)/45))) * 45
		a.Spring.Velocity = vx
	}
	turn = dial.AnimateTo("turn", app.turn, a)
}
dial.Transform(ui.Rotation(turn))

DragVelocity is the pointer's velocity while it presses, in DIPs per second along the element's axes, zero once it rests a moment. Velocity(key) is the velocity of an animated value. A click, which does not move, ends no drag.

ui.Project(value, velocity) is where a fling would carry a value as iOS's scroll views slow down, to choose where it lands, as a page of a pager. ui.Rubberband(value, lo, hi, dimension) holds a value dragged past lo or hi as iOS holds content pulled past its edges, going less and less far:

if _, dy, ok := sheet.Dragged(); ok {
	app.y += dy
	y = sheet.Jump("y", ui.Rubberband(app.y, 0, 400, 600))
}

When it is done

Settled(key) reports whether a value rests at its target, and Arrived(key) is true in the one frame in which it came to rest, for the view to go on, as removing what faded out:

toast.Opacity(toast.AnimateTo("fade", 0, ui.Animation{Duration: 200 * time.Millisecond}))
if toast.Arrived("fade") {
	app.dismiss(toast)
}

Without animation

c.DisableAnimations() makes the values, transitions, poses and router pages of the frame go to their targets at once, as SwiftUI's disablesAnimations: for a change that should not show as motion, as restoring a window's state or jumping to a search result. Call it before building what changes. Keyframes and timelines play on their own rather than towards a target, and go on as they were; seek a timeline to jump it.

Keyframes

Keyframes plays timed values, each segment along the Ease of the frame it arrives at, and holds the last:

tile := ui.Box(c).Key("tile").Size(80, 50)
x := tile.Keyframes("x", []ui.Keyframe{
	{Value: 0},
	{At: 200 * time.Millisecond, Value: 100, Ease: ui.EaseOut},
	{At: 700 * time.Millisecond, Value: 40},
	{At: time.Second, Value: 0, Ease: ui.EaseInOut},
}, ui.KeyframeOptions{Run: app.shakes})
tile.Translate(x, 0)

The first time must be zero and later times must increase. Change Run to play the sequence again, or a changed one: interrupted, it starts from the value shown rather than jumping to its first, the first time through. Repeat is how many more times it plays after the first, RepeatForever for as long as it is built, with Alternate playing every other time backward; Delay holds the first value for a while before it plays, and RepeatDelay the value it got to between plays.

Timelines

A Timeline plays several tweens along one playhead, so motion that belongs together is arranged, controlled and tested together: a card arriving while its title fades in, rows following one another, a small bounce after the card lands. AnimateTo moves one value of one element towards a target, and Keyframes plays one value along timed stops; a timeline coordinates many such values, each tween of its own, around one clock.

An element keeps a timeline under a key, as it keeps Keyframes: build it in the view, as everything else, and give each tween the value it shows this frame:

card := ui.Column(c)
tl := card.Timeline("intro", ui.TimelineOptions{Run: app.opened})
x := tl.Tween(ui.Tween{From: -200, To: 0, Duration: 400 * time.Millisecond})
fade := tl.Tween(ui.Tween{From: 0, To: 1, Duration: 300 * time.Millisecond},
	ui.AfterPrevious(-150*time.Millisecond))
card.Translate(x, 0).Opacity(fade)

A tween eases out (EaseOut) unless its Ease says otherwise, as an Animation does. A tween with no placement follows the one before it at once, so here the second would start at 400ms; placements put it elsewhere, as GSAP's position parameter does:

Placement Starts the tween
AfterPrevious(50*time.Millisecond) 50ms after the one before it ends; a negative offset overlaps it, as -150ms above starts the fade at 250ms
WithPrevious(0) where the one before it starts, to move along with it, or an offset after that
AtLabel("rows", 100*time.Millisecond) 100ms after a label
AtTime(500*time.Millisecond) at a time of the timeline

Nothing starts before the timeline does: a placement before its start is at its start.

Labels and stagger

Label marks a time of the timeline for tweens to start at, and an offset from it staggers them by index:

tl := list.Timeline("rows", ui.TimelineOptions{Run: app.shown})
tl.Label("rows", ui.AtTime(300*time.Millisecond))
for i := range app.rows {
	app.rows[i] = tl.Tween(ui.Tween{From: 0, To: 1, Duration: 200 * time.Millisecond},
		ui.AtLabel("rows", time.Duration(i)*40*time.Millisecond))
}
for i, shown := range app.rows {
	rows[i].Opacity(shown).Translate(0, (1-shown)*12)
}

Playing it

Duration is the end of the last tween, Time where the playhead is, and Progress how far along it is, from 0 to 1, for a scrubber. Paused holds the playhead where it is: time a paused timeline spends is not time it played, so letting a pause go plays on from where it held, however long the pause was. Reverse plays it backward from where the playhead is to its start, as a panel that closes the way it opened (Reverse: !app.open), and a timeline played over while reversed plays from its end. Seek and Progress hold it at a time or a part of the timeline for as long as they are given, going on from there when they are not; give one of them at most. What the options say takes effect in the frame it is said in, so a button that pauses or plays the timeline again shows in the frame it was clicked in:

o := ui.TimelineOptions{Run: app.run, Paused: app.paused, Reverse: app.reverse}
if app.scrubbing {
	o.Progress = &app.scrub
}
tl := card.Timeline("intro", o)

Repeat is how many more times the timeline plays after the first, as KeyframeOptions.Repeat is, RepeatForever for as long as the view builds it, with Alternate playing every other time backward; Delay holds the timeline at its first value for a while before it plays, and RepeatDelay the value it got to between plays. Settled reports that it rests, at its end or, in reverse, at its start, and Arrived is true in the one frame in which it came to rest, as Settled(key) and Arrived(key) are for a value.

Change Run to play the timeline again, or a changed one. Building it without changing Run keeps the playhead where it is, so a view may rearrange its tweens from frame to frame: the timeline plays the ones it arranged this frame. Arranging a longer one goes on from where the playhead is, and it is not Settled until the timeline it arranged has played. A timeline goes with its element's state when the view stops building the element. An element asks for each of its timelines once per build, and the *Timeline it returns is good while the view builds that frame.

While the desktop asks for less motion, a timeline that would play is at once where it would come to rest, every tween at its To, or at its From when it plays back to its start, as values, poses and Keyframes are, and it asks for no frame. What the view does with the playhead itself still works, as a drag still moves a Draggable element: Seek and Progress put it where a scrubber asks, and Paused holds it there. DisableAnimations does not change a timeline, which plays on its own as Keyframes do; Seek jumps it. A timeline draws frames only while it plays, timed by c.Now().

Scrolling

InView() reports whether some of an element showed in the frame before, within the scroll containers around it, to bring it in as it scrolls into view. A ScrollState (see Scrolling) gives how far its container scrolled, for effects that follow the scrolling:

card := ui.Box(c).Key(it.ID).Height(120)
shown := float32(0)
if card.InView() {
	shown = 1
}
card.Opacity(card.AnimateTo("shown", shown, ui.Smooth))

header.Opacity(1 - min(app.scroll.Y/80, 1)) // fades as the page scrolls

ScrollState.ScrollTo(x, y, animation) scrolls a container smoothly (see Scrolling).

Content that changes

ContentTransition animates an element's content as it changes: the text of a Text, the icon of an Icon, the picture of an Image. By default the old content fades out as the new fades in, and an icon shrinks away as the new one grows in. Numeric rolls the characters of a line of text that change, as a counter's digits, those that stay staying: up, or with Down down, as SwiftUI's numericText:

ui.Text(c, strconv.Itoa(app.count)).FontSize(32).Bold().
	ContentTransition(ui.ContentTransition{Numeric: true, Down: app.decreased})
ui.Icon(c, playOrPause).ContentTransition(ui.ContentTransition{})

Content transitions run in frames that only paint. While the desktop asks for less motion, content fades without moving.

Loops and your own motion

Loop returns the progress of an animation that starts over every period, for spinners and pulses (see Drawing and animation). To animate otherwise, compute from c.Now() and call c.AnimationFrame() in every frame that moves.

Less motion and frames

When the desktop asks for less motion (c.Preferences().ReduceMotion), values and poses go to their targets at once and sequences end at their last value. What changes without moving still changes smoothly: colors (AnimateColor), a pose's Fade, content fading across, and transitions' colors and fades. Loop goes on, as the system's spinners do.

Animations draw frames only while something moves, at the display's rate, and none while other windows cover the window. Frames moving values the view reads run the view; poses, content transitions, and drawings that animate from their Draw only paint.