# Animation

![Animation](animation.png)

Source: [`examples/animation`](https://github.com/matjam/bunyip/tree/main/examples/animation) (main.go)

This program shows both halves of animation in one scene. Keyframe clips
from [anim](../pkg/anim.md) drive plain components on the
[entity component system](../pkg/ecs.md): sprite positions, sizes,
rotations and tints in 2D, and transforms in 3D. A flipbook plays frames
from a sprite sheet. A hero sphere crossfades between three clips from
buttons, and a `Finished` event sends it back to idle when a one-shot
clip ends.

Three robot arms show the skeletal side, played by
[gfx.AnimPlayer](../pkg/gfx.md#AnimPlayer) over a model's node
hierarchy. One plays a clip and logs an animation event, one reaches for
a moving target through two-bone inverse kinematics in its `PostPose`
hook, and one blends a slow swing into a fast stride by a slider through
a one-dimensional blend space. The model is a glTF document this program
builds in memory, which is the same thing
[gltf.Load](../pkg/gltf.md#Load) returns from a file.

The two systems are worth telling apart. The `anim` clips animate
components of entities and are advanced by a system on the world; the
`gfx.AnimPlayer` animates a model's skeleton and is advanced by the game.
[The animation guide](../guides/animation.md) covers both.

Run it:

```bash
go run ./examples/animation -seconds 3 -shot out.png
```

The flags are `-seconds N`, `-shot file.png` and `-headless` to render
without a window.

## Components and state

`sprite2D` and `mesh3D` are the game's own components saying how to draw
an entity. The animation itself needs neither: a clip writes into
`gfx.Sprite` and `gfx.Transform`, which are engine components, and the
drawing reads them.

```go
// Command animation shows the anim package on 2D and 3D entities alike:
// keyframe clips drive sprite positions, sizes, rotations and tints and
// 3D transforms; a flipbook plays sprite-sheet frames; buttons
// crossfade the hero cube between clips, with a Finished event sending
// it back to idle; and three robot arms from a generated glTF model show
// a skeletal clip with an animation event, two-bone IK reaching for a
// moving target, and a 1D blend space mixing a slow swing into a fast
// one by a slider. A sphere above them carries three morph targets
// blended in the vertex shader, driven by two sliders and a sine, which
// costs no upload however often the weights change. Escape quits.
package main

import (
	"flag"
	"fmt"
	"image"
	"image/color"
	"math"
	"os"

	"golang.org/x/image/font/gofont/goregular"

	"github.com/matjam/bunyip/anim"
	"github.com/matjam/bunyip/ecs"
	"github.com/matjam/bunyip/engine"
	"github.com/matjam/bunyip/gfx"
	"github.com/matjam/bunyip/gltf"
	"github.com/matjam/bunyip/input"
	"github.com/matjam/bunyip/lin"
	"github.com/matjam/bunyip/tween"
	"github.com/matjam/bunyip/ui"
)

// Components that say how to draw an entity.
type sprite2D struct{ Tex *gfx.Texture }
type mesh3D struct {
	Mesh *gfx.Mesh
	Mat  gfx.Material
}
```

The game holds the three hero clips so the buttons can play them, the
three skeletal players, and the two queries the drawing walks.

```go
type game struct {
	seconds float64
	shot    string

	font   *gfx.Font
	ui     *ui.Context
	world  *ecs.World
	dot    *gfx.Texture
	walker *gfx.Texture
	cube   *gfx.Mesh
	sphere *gfx.Mesh
	hero   ecs.Entity
	idle   *anim.Clip
	jump   *anim.Clip
	spin   *anim.Clip
	speed  float32
	log    []string
	yaw    float32

	// Three arms of one skeletal model: one swings a clip with an
	// event, one reaches for a target by IK, and one blends the swing
	// into a faster stride by a pace parameter.
	arms   *gfx.Model
	swing  *gfx.AnimPlayer
	reach  *gfx.AnimPlayer
	ikOn   bool
	target lin.Vec3 // the reaching arm's goal, relative to its base
	stride *gfx.AnimPlayer
	blend  *anim.Blend
	pace   float32

	// A sphere with three morph targets, driven straight from sliders and
	// a sine. Up to gfx.MaxGPUMorphTargets open at once blend in the
	// vertex shader, so changing them every frame uploads nothing.
	face  *gfx.Model
	faceW [3]float32

	sprites  *ecs.Query2[gfx.Sprite, sprite2D]
	meshes   *ecs.Query2[gfx.Transform, mesh3D]
	shotDone bool
}
```

## Init: resources and the world

The textures and meshes are created first, then a world with two cached
queries. Everything after this point spawns entities into that world.

```go
func (g *game) Init(ctx *engine.Context) error {
	var err error
	if g.font, err = ctx.Gfx.NewFont(goregular.TTF, 15, gfx.FontOptions{}); err != nil {
		return err
	}
	g.ui = ui.New(ctx.Gfx, ui.DarkTheme(g.font))
	if g.dot, err = ctx.Gfx.NewTexture(circle(32), gfx.TextureOptions{Linear: true}); err != nil {
		return err
	}
	if g.walker, err = ctx.Gfx.NewTexture(walkerSheet(), gfx.TextureOptions{}); err != nil {
		return err
	}
	cv, ci := gfx.CubeMesh()
	if g.cube, err = ctx.Gfx.NewMesh(cv, ci); err != nil {
		return err
	}
	sv, si := gfx.SphereMesh(16, 32)
	if g.sphere, err = ctx.Gfx.NewMesh(sv, si); err != nil {
		return err
	}
	g.speed = 1
	w := ecs.NewWorld()
	g.world = w
	g.sprites = w.Query2[gfx.Sprite, sprite2D]()
	g.meshes = w.Query2[gfx.Transform, mesh3D]()
```

## Init: the 2D clips

`anim.NewClip` takes a name, a mode and any number of tracks.
`anim.Loop` restarts, `anim.PingPong` runs back and forth and
`anim.Once` stops at the end and raises a `Finished` event.

A track names what it animates and carries the keyframes:
`anim.Position2`, `anim.Size2`, `anim.Rotation2` and `anim.Tint` write
into a `gfx.Sprite`. `anim.At(t, v)` is a keyframe at a time in seconds,
and `anim.AtEased(t, v, tween.OutQuad)` eases the segment that ends at
it with a function from [tween](../pkg/tween.md). Rotations are in
radians and colours are `gfx.Color` in linear space.

A clip is a value shared by every entity playing it. The six bouncing
dots share one `bounce` clip and differ only in `p.Time`, the point each
one starts from, which is the cheapest way to stagger a crowd.

`anim.Player{}` is the component that plays a clip, and
`anim.PlayerOf(w, e)` returns a pointer to it. The flipbook is a
different component, `anim.Flipbook`, holding a sheet, the frames to
play, a rate and whether to loop; it needs no player and no clip.

```go
	// 2D: dots that bounce, pulse and fade, each offset in time.
	bounce := anim.NewClip("bounce", anim.Loop,
		anim.Position2(anim.Vec2s(anim.At(0, lin.V2(0, 0)), anim.AtEased(0.6, lin.V2(0, -120), tween.OutQuad), anim.AtEased(1.2, lin.V2(0, 0), tween.OutBounce))),
		anim.Tint(anim.Colors(anim.At(0, gfx.RGB(255, 120, 80)), anim.At(0.6, gfx.RGB(255, 230, 120)), anim.At(1.2, gfx.RGB(255, 120, 80)))),
	)
	for i := range 6 {
		e := w.SpawnWith(gfx.Sprite{Size: lin.V2(40, 40), Color: gfx.White}, sprite2D{g.dot}, anim.Player{})
		p := anim.PlayerOf(w, e)
		p.Play(bounce)
		p.Time = float32(i) * 0.2
		w.Add(e, offset{lin.V2(60+float32(i)*60, 200)})
	}
	pulse := anim.NewClip("pulse", anim.PingPong,
		anim.Size2(anim.Vec2s(anim.At(0, lin.V2(30, 30)), anim.AtEased(0.8, lin.V2(90, 90), tween.InOutSine))),
		anim.Rotation2(anim.Floats(anim.Num(0, 0), anim.Num(0.8, math.Pi/2))),
	)
	e := w.SpawnWith(gfx.Sprite{Size: lin.V2(30, 30), Color: gfx.RGB(120, 200, 255), Origin: lin.V2(0.5, 0.5)}, sprite2D{g.dot}, anim.Player{}, offset{lin.V2(480, 200)})
	anim.PlayerOf(w, e).Play(pulse)

	// A flipbook walker from a generated four-frame sheet.
	sheet := gfx.NewSheet(g.walker, 16, 16)
	w.SpawnWith(gfx.Sprite{Size: lin.V2(64, 64), Color: gfx.White}, sprite2D{g.walker}, offset{lin.V2(560, 180)},
		anim.Flipbook{Sheet: sheet, Frames: []int{0, 1, 2, 3}, FPS: 8, Loop: true})
```

## Init: the 3D clips and the hero

The same track functions exist for 3D: `anim.Position`, `anim.Rotation`
and `anim.Scale` write into a `gfx.Transform`. Rotations are quaternions,
so the keyframes are built with `lin.AxisAngle` and interpolated the
short way round.

The ring of cubes shares one clip per entity built in the loop, each
starting at a different angle and offset in time. The hero has three
clips: `idle` loops, and `jump` and `spin` are `anim.Once`, so they end
and report it.

The rotation track in `idle` looks redundant, holding the identity at
both ends. It is there so a crossfade from `spin` back to `idle` has a
rotation to blend towards; a clip that does not animate a channel leaves
it wherever the last clip put it.

```go
	// 3D: a ring of cubes orbiting and tumbling, and a hero cube with
	// clips to crossfade between.
	for i := range 8 {
		a := float32(i) / 8 * 2 * math.Pi
		orbit := anim.NewClip("orbit", anim.Loop,
			anim.Position(anim.Vec3s(
				anim.At(0, lin.V3(3*float32(math.Cos(float64(a))), 0, 3*float32(math.Sin(float64(a))))),
				anim.At(2, lin.V3(3*float32(math.Cos(float64(a)+math.Pi)), 1, 3*float32(math.Sin(float64(a)+math.Pi)))),
				anim.At(4, lin.V3(3*float32(math.Cos(float64(a))), 0, 3*float32(math.Sin(float64(a))))),
			)),
			anim.Rotation(anim.Quats(anim.At(0, lin.QuatIdentity()), anim.At(2, lin.AxisAngle(lin.V3(1, 1, 0).Norm(), math.Pi)), anim.At(4, lin.AxisAngle(lin.V3(1, 1, 0).Norm(), 2*math.Pi)))),
		)
		e := w.SpawnWith(gfx.Transform{Scale: lin.V3(0.4, 0.4, 0.4)}, mesh3D{g.cube, gfx.Material{BaseColor: gfx.RGB(uint8(120+15*i), 160, uint8(220-15*i)), Roughness: 0.4}}, anim.Player{})
		p := anim.PlayerOf(w, e)
		p.Play(orbit)
		p.Time = float32(i) * 0.5
	}
	g.idle = anim.NewClip("idle", anim.Loop,
		anim.Position(anim.Vec3s(anim.At(0, lin.V3(0, 0.5, 0)), anim.AtEased(1, lin.V3(0, 0.8, 0), tween.InOutSine), anim.AtEased(2, lin.V3(0, 0.5, 0), tween.InOutSine))),
		anim.Scale(anim.Vec3s(anim.At(0, lin.V3(1, 1, 1)), anim.At(1, lin.V3(1.05, 0.95, 1.05)), anim.At(2, lin.V3(1, 1, 1)))),
		anim.Rotation(anim.Quats(anim.At(0, lin.QuatIdentity()), anim.At(2, lin.QuatIdentity()))),
	)
	g.jump = anim.NewClip("jump", anim.Once,
		anim.Position(anim.Vec3s(anim.At(0, lin.V3(0, 0.5, 0)), anim.AtEased(0.4, lin.V3(0, 3, 0), tween.OutQuad), anim.AtEased(0.8, lin.V3(0, 0.5, 0), tween.InQuad))),
		anim.Scale(anim.Vec3s(anim.At(0, lin.V3(1.3, 0.7, 1.3)), anim.At(0.2, lin.V3(0.8, 1.4, 0.8)), anim.At(0.8, lin.V3(1.2, 0.8, 1.2)), anim.At(1, lin.V3(1, 1, 1)))),
	)
	g.spin = anim.NewClip("spin", anim.Once,
		anim.Rotation(anim.Quats(anim.At(0, lin.QuatIdentity()), anim.At(0.5, lin.AxisAngle(lin.V3(0, 1, 0), math.Pi)), anim.AtEased(1, lin.AxisAngle(lin.V3(0, 1, 0), 2*math.Pi), tween.OutBack))),
		anim.Position(anim.Vec3s(anim.At(0, lin.V3(0, 0.5, 0)), anim.At(1, lin.V3(0, 0.5, 0)))),
	)
	g.hero = w.SpawnWith(gfx.Transform{Position: lin.V3(0, 0.5, 0)}, mesh3D{g.sphere, gfx.Material{BaseColor: gfx.RGB(255, 200, 90), Metallic: 0.6, Roughness: 0.3}}, anim.Player{})
	anim.PlayerOf(w, g.hero).Play(g.idle)
```

## Init: the skeletal arms

`ctx.Gfx.LoadModel` uploads a glTF document. `model.Parts` are its
drawable pieces, whose materials the game may replace, and
`model.NewAnimPlayer` returns one player per animated instance, so three
players over one model is three arms in different poses from one upload.

`AddEvent("swing", 1, "hit")` marks a time in a clip; `OnEvent` is called
as playback crosses it, on every loop, which is how a footstep sound or a
hit box is triggered from the animation rather than from a timer.

`PostPose` runs after the pose is computed and before it is drawn.
`anim.SolveTwoBoneIK(p, shoulder, elbow, hand, target, pole)` turns three
nodes so the end node reaches a point in model space, with the middle
joint bending towards the pole vector. The node indices come from
`model.NodeIndex`, looked up once here rather than by name every frame.

`anim.NewBlend` with a `BlendSpace1D` mixes clips by a named parameter:
the swing at zero, the stride at one. Between them both clips run at one
shared phase, so the arm neither stutters nor doubles back.

```go
	// Skeletal: the arms come from a glTF document built in memory; a
	// file loads the same way through gltf.Load. The left arm plays the
	// swing clip and logs its "hit" event; the right arm's PostPose
	// solves two-bone IK towards an orbiting target.
	if g.arms, err = ctx.Gfx.LoadModel(armDocument()); err != nil {
		return err
	}
	g.arms.Parts[0].Material = gfx.Material{BaseColor: gfx.RGB(200, 90, 80), Roughness: 0.5}
	g.arms.Parts[1].Material = gfx.Material{BaseColor: gfx.RGB(240, 180, 90), Roughness: 0.5}
	g.swing = g.arms.NewAnimPlayer()
	g.swing.AddEvent("swing", 1, "hit")
	g.swing.OnEvent = func(e gfx.AnimEvent) { g.say(fmt.Sprintf("event %q at %.1fs of %s", e.Name, e.Time, e.Clip)) }
	g.swing.Play("swing", true)
	g.reach = g.arms.NewAnimPlayer()
	g.reach.Play("swing", true)
	g.ikOn = true
	shoulder, elbow, hand := g.arms.NodeIndex("shoulder"), g.arms.NodeIndex("elbow"), g.arms.NodeIndex("hand")
	g.reach.PostPose = func(p *gfx.AnimPlayer) {
		if g.ikOn {
			anim.SolveTwoBoneIK(p, shoulder, elbow, hand, g.target, lin.V3(0, 0.8, 2))
		}
	}
	// The third arm plays a 1D blend space: the two-second swing at pace
	// 0, the one-second stride at pace 1. In between, both clips run at
	// one shared phase, so the arm neither stutters nor doubles back.
	g.stride = g.arms.NewAnimPlayer()
	g.blend = anim.NewBlend(&anim.BlendSpace1D{Parameter: "pace", Clips: []anim.BlendPoint1D{
		{Clip: "swing", At: 0}, {Clip: "stride", At: 1},
	}})
	g.pace = 0.5

```

## Init: the systems

`anim.System` advances every `anim.Player` and `anim.Flipbook` in the
world. The second system reads this step's `anim.Finished` events and
crossfades the hero back to idle when one of its one-shot clips ends,
which is how a jump returns to a stance without the button knowing what
follows it.

```go
	w.AddSystem("anim", anim.System)
	// When a one-shot clip finishes, fade the hero back to idle.
	w.AddSystem("return", func(w *ecs.World, dt float64) {
		for _, ev := range w.Events[anim.Finished]() {
			if ev.Entity == g.hero {
				anim.PlayerOf(w, g.hero).CrossFade(g.idle, 0.3)
				g.say("finished " + ev.Clip.Name + ", back to idle")
			}
		}
	})
	return nil
}
```

`offset` is the game's own component holding a 2D entity's anchor. The
clip animates the sprite's position relative to the origin, and the
drawing adds the anchor, so one clip serves six dots in six places.

```go
// offset is a 2D entity's anchor; the clip's position is relative to it.
type offset struct{ At lin.Vec2 }

func (g *game) say(s string) {
	g.log = append(g.log, s)
	if len(g.log) > 5 {
		g.log = g.log[1:]
	}
}
```

## Shutdown

Every mesh, texture, model and font is destroyed on the goroutine that
created it. The world and its entities are ordinary memory and need
nothing.

```go
func (g *game) Shutdown(ctx *engine.Context) {
	g.face.Destroy()
	g.arms.Destroy()
	g.cube.Destroy()
	g.sphere.Destroy()
	g.dot.Destroy()
	g.walker.Destroy()
	g.font.Destroy()
}
```

## Update: speeds, the world and the players

`ecs.World.Each` walks every entity with an `anim.Player` and writes the speed
multiplier from the slider. `g.world.Update(ctx.Delta)` runs the systems,
which advances the clips and the flipbook.

The three skeletal players are advanced by hand, because they are not
components: `Advance` takes seconds, so multiplying by the speed is how
the same slider reaches them. The blend space is set and then advanced
through its player.

The crossfade at frame 30 exists so a timed run has the hero in the
middle of a jump when the screenshot is taken.

```go
func (g *game) Update(ctx *engine.Context) error {
	if ctx.Input.KeyPressed(input.KeyEscape) || (g.seconds > 0 && ctx.Time >= g.seconds) {
		ctx.Quit()
	}
	if g.shot != "" && !g.shotDone && (g.seconds == 0 || ctx.Time >= g.seconds/2) {
		ctx.Screenshot(g.shot)
		g.shotDone = true
	}
	if g.seconds > 0 && ctx.Frame == 30 {
		anim.PlayerOf(g.world, g.hero).CrossFade(g.jump, 0.2) // something to see in a screenshot
	}
	g.world.Each(func(e ecs.Entity, p *anim.Player) { p.Speed = g.speed })
	g.world.Update(ctx.Delta)
	t := float32(ctx.Time)
	g.target = lin.V3(0.9*float32(math.Cos(float64(t)*1.3)), 0.9+0.5*float32(math.Sin(float64(t)*0.7)), 0.7*float32(math.Sin(float64(t)*1.3)))
	g.swing.Advance(ctx.Delta * float64(g.speed))
	g.reach.Advance(ctx.Delta * float64(g.speed))
	g.blend.Set("pace", g.pace)
	g.blend.Advance(g.stride, ctx.Delta*float64(g.speed))
	// The snout breathes on its own so a screenshot catches it moving.
	// New weights every update cost nothing: the shader blends them.
	g.faceW[2] = 0.3 + 0.3*float32(math.Sin(float64(t)*1.6))
	if err := g.face.SetMorphWeights(0, g.faceW[:]); err != nil {
		return err
	}
	g.yaw += float32(ctx.Delta) * 0.2
	return nil
}
```

## Draw: the 3D scene

The camera, the light and the ground are set up as in any 3D scene. The
mesh query draws every animated entity with `DrawMeshAt`, which takes the
transform the clip just wrote.

`DrawModelAnimated` draws a model with a player's current pose, so the
three arms are three calls with three players and one model. The small
glowing sphere marks where the reaching arm is aiming, drawn at the
target plus that arm's own base position, because the target is in the
arm's model space.

```go
func (g *game) Draw(ctx *engine.Context) error {
	gr := ctx.Gfx
	w := g.world
	gr.SetCamera(gfx.OrbitCamera(lin.V3(0, 0.8, 0), g.yaw, 0.45, 9))
	gr.SetLight(gfx.Light{Direction: lin.V3(-0.4, -1, -0.5), Color: gfx.Color{R: 2.2, G: 2.1, B: 1.9, A: 1},
		Sky: gfx.Sky{Zenith: gfx.Color{R: 0.25, G: 0.3, B: 0.45, A: 1}, Ground: gfx.Color{R: 0.1, G: 0.1, B: 0.08, A: 1}}, Shadows: true, ShadowDistance: 25})
	gr.DrawMesh(g.cube, gfx.Material{BaseColor: gfx.RGB(150, 150, 160), Roughness: 0.9}, lin.Translate(lin.V3(0, -0.6, 0)).Mul(lin.Scale(lin.V3(9, 0.2, 9))))
	g.meshes.Each(func(e ecs.Entity, t *gfx.Transform, m *mesh3D) {
		gr.DrawMeshAt(m.Mesh, m.Mat, *t)
	})
	gr.DrawModelAnimated(g.arms, gfx.At(-2.2, -0.5, 0), g.swing)
	gr.DrawModelAnimated(g.arms, gfx.At(2.2, -0.5, 0), g.reach)
	gr.DrawModelAnimated(g.arms, gfx.At(0, -0.5, -2.4), g.stride)
	gr.DrawMesh(g.sphere, gfx.Material{BaseColor: gfx.RGB(120, 220, 140), Emissive: 0.4},
		lin.Translate(g.target.Add(lin.V3(2.2, -0.5, 0))).Mul(lin.Scale(lin.V3(0.08, 0.08, 0.08))))
```

## Draw: the 2D entities

`gr.ScreenSpace()` returns sprite drawing to view coordinates, undoing
any 2D camera. Nothing here sets one, so the call states what the sprites
expect rather than changing anything: positions in view units with the
origin at the top left.

The sprite query copies each `gfx.Sprite` before drawing it, adds the
anchor from the `offset` component, and fills in `UV1` when the clip left
it zero. Copying rather than writing back keeps the anchor out of the
component the clip owns.

```go
	// 2D entities draw at their offset plus the animated position.
	gr.ScreenSpace()
	g.sprites.Each(func(e ecs.Entity, s *gfx.Sprite, d *sprite2D) {
		draw := *s
		if o, ok := w.Get[offset](e); ok {
			draw.Pos = draw.Pos.Add(o.At)
		}
		if draw.UV1 == (lin.Vec2{}) {
			draw.UV1 = lin.V2(1, 1)
		}
		gr.Draw(d.Tex, draw)
	})
```

## Draw: the panel

The buttons call `CrossFade(clip, seconds)`, which blends from the
current pose into the new clip over that time instead of snapping. The
sliders and the checkbox edit the game's own values, which `Update` then
pushes into the players. The last two sliders are the morph target
weights, which `Update` hands to the model every frame: with three
targets, well inside `gfx.MaxGPUMorphTargets`, the blend happens in the
vertex shader. Updated weights travel with the draw; the geometry does
not need to be blended on the CPU or uploaded again.

```go
	u := g.ui
	u.Begin(ctx.Input, func() {
		u.Panel("Animation", ui.Rect{X: 12, Y: ctx.Height - 392, W: 300, H: 380}, func() {
			u.Label("Hero clip: " + anim.PlayerOf(w, g.hero).Clip.Name)
			u.Row(3, func() {
				if u.Button("Idle") {
					anim.PlayerOf(w, g.hero).CrossFade(g.idle, 0.3)
				}
				if u.Button("Jump") {
					anim.PlayerOf(w, g.hero).CrossFade(g.jump, 0.15)
				}
				if u.Button("Spin") {
					anim.PlayerOf(w, g.hero).CrossFade(g.spin, 0.15)
				}
			})
			u.Slider("Speed", &g.speed, 0, 3)
			u.Slider("Back arm pace (swing to stride)", &g.pace, 0, 1)
			u.Checkbox("Right arm reaches by IK", &g.ikOn)
			// Three morph targets, blended in the vertex shader: the
			// sliders move every frame and upload nothing.
			names := g.face.MorphTargets(0)
			for i := range g.faceW[:2] {
				u.Slider("Morph "+names[i], &g.faceW[i], 0, 1)
			}
			for _, l := range g.log {
				u.Label(l)
			}
		})
	})
	return nil
}
```

## The generated art

`circle` draws the soft dot and `walkerSheet` the four frames of the
flipbook, four 16 by 16 figures whose legs alternate, laid out in one
row, which is what `gfx.NewSheet(tex, 16, 16)` cuts up.

```go
func circle(size int) image.Image {
	img := image.NewNRGBA(image.Rect(0, 0, size, size))
	r := float64(size) / 2
	for y := range size {
		for x := range size {
			d := math.Hypot(float64(x)+0.5-r, float64(y)+0.5-r)
			a := math.Max(0, math.Min(1, r-d))
			img.SetNRGBA(x, y, color.NRGBA{255, 255, 255, uint8(255 * a)})
		}
	}
	return img
}
```

```go
// walkerSheet draws four 16×16 frames of a little figure whose legs
// alternate.
func walkerSheet() image.Image {
	img := image.NewRGBA(image.Rect(0, 0, 64, 16))
	for f := range 4 {
		set := func(x, y int, c color.RGBA) { img.SetRGBA(f*16+x, y, c) }
		for y := 2; y < 7; y++ {
			for x := 5; x < 11; x++ {
				set(x, y, color.RGBA{250, 220, 180, 255})
			}
		}
		for y := 7; y < 12; y++ {
			for x := 4; x < 12; x++ {
				set(x, y, color.RGBA{80, 160, 220, 255})
			}
		}
		stride := []int{0, 1, 0, -1}[f]
		for y := 12; y < 16; y++ {
			for _, x := range []int{5 + stride, 6 + stride, 9 - stride, 10 - stride} {
				set(x, y, color.RGBA{40, 40, 90, 255})
			}
		}
	}
	return img
}
```

## The morph target sphere

`faceDocument` builds the blend shapes. A morph target is a delta per
vertex over the mesh's rest geometry, and a weight says how much of it
to add; the three here pull the crown to a point, squash the sphere wide
and push a snout out of the front, and any mixture of them is a
position. Normals get deltas too, so the lighting follows the shape.

A file's blend shapes arrive in exactly this form through `gltf.Load`,
including the sparse accessors Blender writes for them. Three targets is
well inside `gfx.MaxGPUMorphTargets`, so the model's deltas go into a
storage buffer when it loads and every draw blends them in the vertex
shader. Changing the sliders updates the weights without reuploading the
mesh's vertices or target deltas.

```go
// faceDocument builds a sphere with three morph targets as a glTF
// document in memory: one pulls its crown into a point, one squashes it
// wide and one pushes a snout out of the front. A file's blend shapes
// arrive the same way through gltf.Load.
func faceDocument() *gltf.Document {
	sv, si := gfx.SphereMesh(16, 32)
	prim := gltf.Primitive{Indices: si, Material: -1}
	for _, v := range sv {
		prim.Positions = append(prim.Positions, v.Pos)
		prim.Normals = append(prim.Normals, v.Normal)
		prim.UVs = append(prim.UVs, v.UV)
	}
	// Each target is a delta per vertex, weighted by how much of the
	// shape it belongs to, so the three blend smoothly against each other.
	shape := func(delta func(p lin.Vec3) lin.Vec3) gltf.MorphTarget {
		t := gltf.MorphTarget{Positions: make([]lin.Vec3, len(sv)), Normals: make([]lin.Vec3, len(sv))}
		for i, v := range sv {
			t.Positions[i] = delta(v.Pos)
			// The normal follows the stretch: a rough approximation, which
			// is all a blend shape's normals ever are.
			t.Normals[i] = delta(v.Normal).Mul(0.5)
		}
		return t
	}
	prim.Targets = []gltf.MorphTarget{
		shape(func(p lin.Vec3) lin.Vec3 { return lin.V3(-p.X*0.6, max(p.Y, 0)*1.2, -p.Z*0.6) }),
		shape(func(p lin.Vec3) lin.Vec3 { return lin.V3(p.X*0.5, -p.Y*0.45, p.Z*0.5) }),
		shape(func(p lin.Vec3) lin.Vec3 { return lin.V3(0, 0, max(p.Z, 0)*0.9) }),
	}
	return &gltf.Document{
		Meshes: []gltf.Mesh{{Name: "face", TargetNames: []string{"point", "squash", "snout"},
			Primitives: []gltf.Primitive{prim}}},
		Nodes:     []gltf.Node{{Name: "face", Parent: -1, Rotation: lin.QuatIdentity(), Scale: lin.V3(1, 1, 1), Mesh: 0, Skin: -1}},
		Instances: []gltf.Instance{{Name: "face", Mesh: 0, Node: 0, Skin: -1, World: lin.Identity()}},
	}
}
```

## Building a glTF document in memory

`armDocument` builds the two-bone arm the three players share. It is
worth reading as a description of what a loaded model actually is: a mesh
with positions, normals and texture coordinates; a node hierarchy with a
parent, children, a local translation, rotation and scale; instances
tying meshes to nodes with their world transforms; and animations, each a
duration and a list of channels writing one path of one node from times
and values.

The rest pose here is both bones straight up, and the clips rotate about
Z by degrees converted with `lin.Radians`. The `swing` clip takes two
seconds, `stride` one, which is what the blend space mixes.

```go
// armDocument builds a two-bone arm as a glTF document in memory: a box
// per bone, nodes shoulder, elbow and hand, a "swing" clip that rocks
// both joints over two seconds and a "stride" clip that rocks them
// wider in one. Straight up is the rest pose.
func armDocument() *gltf.Document {
	cv, ci := gfx.CubeMesh()
	prim := gltf.Primitive{Indices: ci, Material: -1}
	for _, v := range cv {
		prim.Positions = append(prim.Positions, lin.V3(v.Pos.X*0.18, (v.Pos.Y+0.5)*0.8, v.Pos.Z*0.18))
		prim.Normals = append(prim.Normals, v.Normal)
		prim.UVs = append(prim.UVs, v.UV)
	}
	id, one := lin.QuatIdentity(), lin.V3(1, 1, 1)
	doc := &gltf.Document{
		Meshes: []gltf.Mesh{{Name: "bone", Primitives: []gltf.Primitive{prim}}},
		Nodes: []gltf.Node{
			{Name: "shoulder", Parent: -1, Children: []int{1}, Rotation: id, Scale: one, Mesh: 0, Skin: -1},
			{Name: "elbow", Parent: 0, Children: []int{2}, Translation: lin.V3(0, 0.8, 0), Rotation: id, Scale: one, Mesh: 0, Skin: -1},
			{Name: "hand", Parent: 1, Translation: lin.V3(0, 0.8, 0), Rotation: id, Scale: one, Mesh: -1, Skin: -1},
		},
	}
	doc.Instances = []gltf.Instance{
		{Name: "shoulder", Mesh: 0, Node: 0, Skin: -1, World: doc.Nodes[0].Local()},
		{Name: "elbow", Mesh: 0, Node: 1, Skin: -1, World: doc.Nodes[0].Local().Mul(doc.Nodes[1].Local())},
	}
	aboutZ := func(deg float32) lin.Vec4 {
		q := lin.AxisAngle(lin.V3(0, 0, 1), lin.Radians(deg))
		return lin.V4(q.X, q.Y, q.Z, q.W)
	}
	doc.Animations = []gltf.Animation{
		{Name: "swing", Duration: 2, Channels: []gltf.Channel{
			{Node: 0, Path: gltf.PathRotation, Times: []float32{0, 1, 2}, Values: []lin.Vec4{aboutZ(-35), aboutZ(35), aboutZ(-35)}},
			{Node: 1, Path: gltf.PathRotation, Times: []float32{0, 1, 2}, Values: []lin.Vec4{aboutZ(20), aboutZ(-50), aboutZ(20)}},
		}},
		{Name: "stride", Duration: 1, Channels: []gltf.Channel{
			{Node: 0, Path: gltf.PathRotation, Times: []float32{0, 0.5, 1}, Values: []lin.Vec4{aboutZ(-70), aboutZ(60), aboutZ(-70)}},
			{Node: 1, Path: gltf.PathRotation, Times: []float32{0, 0.5, 1}, Values: []lin.Vec4{aboutZ(45), aboutZ(-90), aboutZ(45)}},
		}},
	}
	return doc
}
```

## main

```go
func main() {
	seconds := flag.Float64("seconds", 0, "exit after this many seconds")
	shot := flag.String("shot", "", "write a screenshot to this PNG")
	headless := flag.Bool("headless", false, "render without a window, for screenshots")
	flag.Parse()
	err := engine.Run(engine.Config{Title: "Bunyip animation", Width: 960, Height: 640, Resizable: true, Headless: *headless},
		&game{seconds: *seconds, shot: *shot})
	if err != nil {
		fmt.Fprintln(os.Stderr, "animation:", err)
		os.Exit(1)
	}
}
```

## What to try

- Add an `anim.Tint` track to the 2D sprite's `pulse` clip in `Init` and
  animate its colour alongside its size. `Tint` targets `gfx.Sprite`;
  the 3D hero would need a custom property track for its material.
- Change the pole vector passed to `SolveTwoBoneIK` in `Init` and see the
  elbow swing to the other side.
- Add a third point to the blend space in `Init` and give the slider
  another clip to reach.
- Change the flipbook's `FPS` in `Init` to alter its walk rate. The speed
  slider changes `anim.Player` and the skeletal players; the flipbook
  still advances on the world's unscaled delta.
- Add an event to the `stride` clip in `Init` and log it, then blend the
  pace and watch when the event still fires.
