# anim

`import "github.com/matjam/bunyip/anim"`

Package anim animates entities. A Curve interpolates keyframes of any value type. A Track applies a curve to one property of one component. A Clip bundles tracks with a loop mode. A Player component plays clips on an entity and crossfades between them. The same types animate a 2D sprite's position and tint, a 3D transform's rotation and scale, and any field of your own component. One System drives every player, sprite-sheet Flipbook and skeletal Skeleton in the world.

For skeletons, BlendSpace1D, BlendSpace2D and BlendTree are data that turn parameters (a speed, a strafe direction) into clip weights, and a Blend plays them on a gfx.AnimPlayer with their cycles in step. TwoBoneIK and LookAt are the solvers behind SolveTwoBoneIK and LookAtNode, which adjust the player's pose to place feet and aim heads.

	bounce := anim.NewClip("bounce", anim.Loop,
		anim.Position2(anim.Vec2s(
			anim.At(0, lin.V2(100, 300)),
			anim.AtEased(0.5, lin.V2(100, 100), tween.OutQuad),
			anim.AtEased(1, lin.V2(100, 300), tween.InQuad),
		)),
	)
	e := w.SpawnWith(gfx.Sprite{...}, anim.Player{})
	anim.PlayerOf(w, e).Play(bounce)
	w.AddSystem("anim", anim.System)

## Functions

<a id="LerpColor"></a>

### LerpColor

```go
func LerpColor(a, b gfx.Color, t float32) gfx.Color
```

LerpColor interpolates colours channel by channel.

<a id="LerpFloat"></a>

### LerpFloat

```go
func LerpFloat(a, b, t float32) float32
```

LerpFloat interpolates numbers.

<a id="LerpVec2"></a>

### LerpVec2

```go
func LerpVec2(a, b lin.Vec2, t float32) lin.Vec2
```

LerpVec2 interpolates 2D vectors.

<a id="LerpVec3"></a>

### LerpVec3

```go
func LerpVec3(a, b lin.Vec3, t float32) lin.Vec3
```

LerpVec3 interpolates 3D vectors.

<a id="LookAt"></a>

### LookAt

```go
func LookAt(from, to lin.Vec3, limit float32) lin.Quat
```

LookAt returns the rotation that turns direction from towards direction to along the shortest arc, by at most limit radians; a zero limit means all the way. It is the maths under LookAtNode.

<a id="LookAtNode"></a>

### LookAtNode

```go
func LookAtNode(p *gfx.AnimPlayer, node int, forward, target lin.Vec3, limit float32)
```

LookAtNode turns a node so that forward, the node's own axis that should face things (often +Z or -Z; check the model), points at target, a point in model space, turning by at most limit radians from the pose's own rotation. A head following the player, a turret tracking a ship. Call it from PostPose or after Advance.

<a id="SlerpQuat"></a>

### SlerpQuat

```go
func SlerpQuat(a, b lin.Quat, t float32) lin.Quat
```

SlerpQuat interpolates rotations along the shortest arc; a zero quaternion counts as no rotation.

<a id="SolveTwoBoneIK"></a>

### SolveTwoBoneIK

```go
func SolveTwoBoneIK(p *gfx.AnimPlayer, root, mid, end int, target, pole lin.Vec3)
```

SolveTwoBoneIK turns three of a player's nodes so the end node reaches target, a point in model space, with the middle joint bending towards pole. Call it from the player's PostPose, or after Advance, every frame the target matters.

<a id="System"></a>

### System

```go
func System(w *ecs.World, dt float64)
```

System advances every Player, Flipbook and Skeleton by dt seconds and writes the results into their components. Register it after the systems that decide what to play and before drawing:

	w.AddSystem("anim", anim.System)

<a id="TwoBoneIK"></a>

### TwoBoneIK

```go
func TwoBoneIK(root, mid, end, target, pole lin.Vec3) (upper, lower lin.Quat)
```

TwoBoneIK solves a chain of two bones so its end reaches a target: a leg (hip, knee, foot) planted on uneven ground, an arm (shoulder, elbow, hand) reaching a handle. root, mid and end are the joints' current positions, in any one space; target is where the end should be, in the same space, and pole is a point the middle joint bends towards (in front of a knee, behind an elbow). A target out of reach straightens the chain towards it.

The result is two rotations in that space: turn the middle joint by lower about its own position first, then the root joint by upper about its position, and the end lands on the target. SolveTwoBoneIK does this on an AnimPlayer's nodes.

## Types

<a id="Blend"></a>

<a id="Blend.Tree"></a>

<a id="Blend.Params"></a>

### Blend

```go
type Blend struct {
	// Tree is what is evaluated: a BlendSpace1D, BlendSpace2D or
	// BlendTree.
	Tree Blender
	// Params holds the parameter values by name; Set writes them.
	Params map[string]float32
	// contains filtered or unexported fields
}
```

Blend drives a gfx.AnimPlayer from a blend space or tree: it holds the parameters the game sets, evaluates the tree every Advance and keeps the mixed clips in step by playing them all at one phase of their own length, so a walk's and a run's feet land together. Clips in a blend loop. Make one with NewBlend; a Skeleton with a Blend set drives it from the ECS.

<a id="NewBlend"></a>

#### NewBlend

```go
func NewBlend(tree Blender) *Blend
```

NewBlend makes a Blend over a space or tree with every parameter at 0.

<a id="Blend.Advance"></a>

#### Blend.Advance

```go
func (b *Blend) Advance(p *gfx.AnimPlayer, dt float64)
```

Advance moves the blend on by dt seconds, scaled by the player's speed, sets the player's clips and advances the player. The phase moves at the blended cycle's rate: with walk at 1 s and run at 0.5 s mixed evenly, one cycle takes 0.75 s and each clip is sampled at the same fraction of its own length.

<a id="Blend.Get"></a>

#### Blend.Get

```go
func (b *Blend) Get(name string) float32
```

Get is a parameter's value; unset parameters are 0.

<a id="Blend.Phase"></a>

#### Blend.Phase

```go
func (b *Blend) Phase() float64
```

Phase is how far through their cycle the blended clips are, 0 to 1.

<a id="Blend.Set"></a>

#### Blend.Set

```go
func (b *Blend) Set(name string, v float32)
```

Set gives a parameter a value.

<a id="Blend.SetPhase"></a>

#### Blend.SetPhase

```go
func (b *Blend) SetPhase(phase float64)
```

SetPhase moves the blended clips to a point in their cycle, 0 to 1.

<a id="Blend.Weights"></a>

#### Blend.Weights

```go
func (b *Blend) Weights() []ClipWeight
```

Weights evaluates the tree for the current parameters; the slice is reused by the next call.

<a id="BlendChild"></a>

<a id="BlendChild.At"></a>

<a id="BlendChild.Tree"></a>

### BlendChild

```go
type BlendChild struct {
	At   float32   `json:"at"`
	Tree BlendTree `json:"tree"`
}
```

BlendChild is a subtree placed at a parameter value in its parent.

<a id="BlendPoint1D"></a>

<a id="BlendPoint1D.Clip"></a>

<a id="BlendPoint1D.At"></a>

### BlendPoint1D

```go
type BlendPoint1D struct {
	Clip string  `json:"clip"`
	At   float32 `json:"at"`
}
```

BlendPoint1D is a clip placed at a parameter value.

<a id="BlendPoint2D"></a>

<a id="BlendPoint2D.Clip"></a>

<a id="BlendPoint2D.At"></a>

### BlendPoint2D

```go
type BlendPoint2D struct {
	Clip string   `json:"clip"`
	At   lin.Vec2 `json:"at"`
}
```

BlendPoint2D is a clip placed at a point in a 2D space.

<a id="BlendSpace1D"></a>

<a id="BlendSpace1D.Parameter"></a>

<a id="BlendSpace1D.Clips"></a>

### BlendSpace1D

```go
type BlendSpace1D struct {
	// Parameter names the value the space reads, as set on a Blend.
	Parameter string `json:"parameter"`
	// Clips are the placed clips, in any order.
	Clips []BlendPoint1D `json:"clips"`
}
```

BlendSpace1D places clips along one parameter and blends the two on either side of its value: idle at 0, walk at 1, run at 2, with speed 1.5 half walk and half run. Outside the clips' range the nearest clip plays alone. It is plain data, buildable in code or from JSON.

<a id="BlendSpace1D.Weights"></a>

#### BlendSpace1D.Weights

```go
func (s *BlendSpace1D) Weights(params map[string]float32, out []ClipWeight) []ClipWeight
```

Weights blends the two clips around the parameter's value.

<a id="BlendSpace2D"></a>

<a id="BlendSpace2D.X"></a>

<a id="BlendSpace2D.Y"></a>

<a id="BlendSpace2D.Clips"></a>

### BlendSpace2D

```go
type BlendSpace2D struct {
	// X and Y name the parameters the space reads, as set on a Blend.
	X string `json:"x"`
	Y string `json:"y"`
	// Clips are the placed clips, in any order.
	Clips []BlendPoint2D `json:"clips"`
}
```

BlendSpace2D places clips at points in a plane of two parameters and blends the ones around the current point: a strafe set with forward, back, left and right around an idle at the centre, read from the velocity's x and y. Weights are gradient bands: a clip at the current point plays alone, points on a line between two clips blend them linearly, and clips fall out as the point moves past them. It is plain data, buildable in code or from JSON.

<a id="BlendSpace2D.Weights"></a>

#### BlendSpace2D.Weights

```go
func (s *BlendSpace2D) Weights(params map[string]float32, out []ClipWeight) []ClipWeight
```

Weights blends the clips around the point the two parameters make.

<a id="BlendTree"></a>

<a id="BlendTree.Clip"></a>

<a id="BlendTree.Space1D"></a>

<a id="BlendTree.Space2D"></a>

<a id="BlendTree.Parameter"></a>

<a id="BlendTree.Children"></a>

### BlendTree

```go
type BlendTree struct {
	Clip      string        `json:"clip,omitempty"`
	Space1D   *BlendSpace1D `json:"space1d,omitempty"`
	Space2D   *BlendSpace2D `json:"space2d,omitempty"`
	Parameter string        `json:"parameter,omitempty"`
	Children  []BlendChild  `json:"children,omitempty"`
}
```

BlendTree is a node in a tree of blends. Exactly one part is used, checked in this order: a Clip plays alone, a Space1D or Space2D plays its blend, and Children are subtrees placed along Parameter and mixed like a 1D space, so a crouch amount can fade a standing locomotion space into a crouched one. The whole tree shares one phase, so the clips it mixes stay in step. It is plain data, buildable in code or from JSON.

<a id="BlendTree.Weights"></a>

#### BlendTree.Weights

```go
func (t *BlendTree) Weights(params map[string]float32, out []ClipWeight) []ClipWeight
```

Weights evaluates the node for the parameters.

<a id="Blender"></a>

<a id="Blender.Weights"></a>

### Blender

```go
type Blender interface {
	Weights(params map[string]float32, out []ClipWeight) []ClipWeight
}
```

Blender turns parameters into clip weights: a blend space, a blend tree or a single clip. Weights appends the clips to play to out and returns it; the weights sum to 1 unless there is nothing to play.

<a id="Clip"></a>

<a id="Clip.Name"></a>

<a id="Clip.Tracks"></a>

<a id="Clip.Mode"></a>

<a id="Clip.Length"></a>

### Clip

```go
type Clip struct {
	Name   string
	Tracks []Track
	Mode   LoopMode
	// Length, when positive, overrides the duration in seconds; otherwise
	// the longest track supplies it.
	Length float32
	// contains filtered or unexported fields
}
```

Clip is a named set of tracks that play together. The zero clip has no tracks and finishes on its first update. Mode defaults to Once.

<a id="NewClip"></a>

#### NewClip

```go
func NewClip(name string, mode LoopMode, tracks ...Track) *Clip
```

NewClip bundles tracks into a clip.

<a id="Clip.AddTrack"></a>

#### Clip.AddTrack

```go
func (c *Clip) AddTrack(tracks ...Track)
```

AddTrack appends tracks to the clip and rebuilds what it caches about them. Assigning to Tracks directly works too as long as the number of tracks changes; replacing a track in place needs this call with no arguments for the change to be seen.

<a id="Clip.Apply"></a>

#### Clip.Apply

```go
func (c *Clip) Apply(w *ecs.World, e ecs.Entity, t, weight float32)
```

Apply samples every track at clip time t in seconds with the given weight. It does not wrap t according to Mode; Player handles looping. The tracks are grouped by the component they write, so a clip that animates three fields of one component looks that component up once. Tracks written by hand, which cannot be grouped, are applied last.

<a id="Clip.Duration"></a>

#### Clip.Duration

```go
func (c *Clip) Duration() float32
```

Duration is the clip's length in seconds.

<a id="ClipWeight"></a>

<a id="ClipWeight.Clip"></a>

<a id="ClipWeight.Weight"></a>

### ClipWeight

```go
type ClipWeight struct {
	Clip   string
	Weight float32
}
```

ClipWeight is a clip's share of a blended pose.

<a id="Curve"></a>

<a id="Curve.Keys"></a>

<a id="Curve.Lerp"></a>

### Curve

```go
type Curve[V any] struct {
	Keys []Key[V]
	Lerp Lerper[V]
}
```

Curve interpolates keys over time. Before the first key it holds the first value; after the last it holds the last. The zero curve samples to V's zero value. Keep Keys sorted by Time when editing them directly. A nil Lerp holds the previous key until the next key is reached.

<a id="Colors"></a>

#### Colors

```go
func Colors(keys ...Key[gfx.Color]) Curve[gfx.Color]
```

Colors makes a curve of colours.

<a id="Floats"></a>

#### Floats

```go
func Floats(keys ...Key[float32]) Curve[float32]
```

Floats makes a curve of numbers.

<a id="NewCurve"></a>

#### NewCurve

```go
func NewCurve[V any](lerp Lerper[V], keys ...Key[V]) Curve[V]
```

NewCurve copies the keys and sorts them stably by time in seconds.

<a id="Quats"></a>

#### Quats

```go
func Quats(keys ...Key[lin.Quat]) Curve[lin.Quat]
```

Quats makes a curve of rotations.

<a id="Vec2s"></a>

#### Vec2s

```go
func Vec2s(keys ...Key[lin.Vec2]) Curve[lin.Vec2]
```

Vec2s makes a curve of 2D vectors.

<a id="Vec3s"></a>

#### Vec3s

```go
func Vec3s(keys ...Key[lin.Vec3]) Curve[lin.Vec3]
```

Vec3s makes a curve of 3D vectors.

<a id="Curve.Duration"></a>

#### Curve.Duration

```go
func (c Curve[V]) Duration() float32
```

Duration is the last key's time in seconds, or zero for an empty curve.

<a id="Curve.Field"></a>

#### Curve.Field

```go
func (curve Curve[V]) Field[C any](field func(*C) *V) Track
```

Field makes a track over a component field of the curve's value type. The non-nil accessor must return the field's non-nil address in the component supplied to it. The address is used only during that application; it is not retained across structural changes. Missing components are skipped. Use Property when reading or writing needs conversion or normalization.

	anim.Floats(anim.Num(0, 1), anim.Num(0.3, 0)).Field(
		func(h *Health) *float32 { return &h.Opacity })

Example:

```go
package main

import (
	"fmt"

	"github.com/matjam/bunyip/anim"
	"github.com/matjam/bunyip/ecs"
)

func main() {
	type Light struct{ Intensity float32 }
	w := ecs.NewWorld()
	e := w.SpawnWith(Light{Intensity: 1})
	fade := anim.Floats(anim.Num(0, 1), anim.Num(1, 0)).Field(
		func(l *Light) *float32 { return &l.Intensity })
	fade.Apply(w, e, 0.25, 1)
	l, _ := w.Get[Light](e)
	fmt.Println(l.Intensity)
}
```

Output:

```
0.75
```

<a id="Curve.Property"></a>

#### Curve.Property

```go
func (curve Curve[V]) Property[C any](get func(*C) V, set func(*C, V)) Track
```

Property makes a track over any component field: get reads the field so crossfades can blend from it, set writes the animated value. The entity must already have C; a missing component is skipped. A curve without a Lerper replaces the value even during a crossfade.

	anim.Floats(anim.Num(0, 0), anim.Num(1, 100)).Property(
		func(h *Health) float32 { return float32(h.HP) },
		func(h *Health, v float32) { h.HP = int(v) })

<a id="Curve.Sample"></a>

#### Curve.Sample

```go
func (c Curve[V]) Sample(t float32) V
```

Sample returns the value at t seconds. Easing may overshoot the segment's values; the interpolation parameter is not clamped after easing.

<a id="Finished"></a>

<a id="Finished.Entity"></a>

<a id="Finished.Clip"></a>

### Finished

```go
type Finished struct {
	Entity ecs.Entity
	Clip   *Clip // nil for a Flipbook
}
```

Finished is emitted once when a Player's Once clip or a non-looping Flipbook ends. Skeleton does not emit it; poll gfx.AnimPlayer.Finished.

<a id="Flipbook"></a>

<a id="Flipbook.Sheet"></a>

<a id="Flipbook.Frames"></a>

<a id="Flipbook.FPS"></a>

<a id="Flipbook.Loop"></a>

<a id="Flipbook.Time"></a>

<a id="Flipbook.Done"></a>

### Flipbook

```go
type Flipbook struct {
	Sheet  *gfx.Sheet
	Frames []int
	FPS    float32 // frames per second; nonpositive means 10
	Loop   bool
	Time   float64 // elapsed seconds; keep nonnegative
	Done   bool
}
```

Flipbook plays sprite-sheet frames into the entity's gfx.Sprite. The zero value is inactive: Sheet and Frames must both be supplied.

<a id="Flipbook.Frame"></a>

#### Flipbook.Frame

```go
func (f *Flipbook) Frame() int
```

Frame returns the sheet frame to show now.

<a id="Flipbook.Restart"></a>

#### Flipbook.Restart

```go
func (f *Flipbook) Restart()
```

Restart plays the flipbook from its first frame.

<a id="Key"></a>

<a id="Key.Time"></a>

<a id="Key.Value"></a>

<a id="Key.Ease"></a>

### Key

```go
type Key[V any] struct {
	Time  float32
	Value V
	Ease  tween.Ease
}
```

Key is a value at a time in seconds. Ease shapes the approach to this key from the previous one; nil is linear.

<a id="At"></a>

#### At

```go
func At[V any](time float32, v V) Key[V]
```

At makes a linear key.

<a id="AtEased"></a>

#### AtEased

```go
func AtEased[V any](time float32, v V, ease tween.Ease) Key[V]
```

AtEased makes a key reached along an easing curve.

<a id="Num"></a>

#### Num

```go
func Num(time, v float32) Key[float32]
```

Num makes a linear key for a number curve; untyped constants would otherwise infer int.

<a id="NumEased"></a>

#### NumEased

```go
func NumEased(time, v float32, ease tween.Ease) Key[float32]
```

NumEased makes an eased key for a number curve.

<a id="Lerper"></a>

### Lerper

```go
type Lerper[V any] func(a, b V, t float32) V
```

Lerper interpolates between two values; t runs from 0 to 1.

<a id="LoopMode"></a>

### LoopMode

```go
type LoopMode uint8
```

LoopMode says what a clip does at its end.

<a id="Once"></a>

<a id="Loop"></a>

<a id="PingPong"></a>

```go
const (
	Once     LoopMode = iota // stop at the last key and report Finished
	Loop                     // start over
	PingPong                 // run backwards, then forwards again
)
```

<a id="Player"></a>

<a id="Player.Clip"></a>

<a id="Player.Time"></a>

<a id="Player.Speed"></a>

<a id="Player.Playing"></a>

### Player

```go
type Player struct {
	Clip    *Clip
	Time    float32 // playback clock in seconds; loops fold it back into their cycle
	Speed   float32 // playback rate; zero means 1
	Playing bool
	// contains filtered or unexported fields
}
```

Player is the component that plays clips on an entity. Add an empty one and call Play through PlayerOf. When setting Clip directly, also set Playing to true. The zero Player is stopped.

<a id="PlayerOf"></a>

#### PlayerOf

```go
func PlayerOf(w *ecs.World, e ecs.Entity) *Player
```

PlayerOf returns the entity's Player, adding one when it has none.

<a id="Player.CrossFade"></a>

#### Player.CrossFade

```go
func (p *Player) CrossFade(c *Clip, seconds float32)
```

CrossFade starts a clip while blending out the current one over the given seconds. The fade uses unscaled update time; Speed only scales clip playback. A nonpositive duration or a stopped player uses Play.

Example:

```go
package main

import (
	"fmt"

	"github.com/matjam/bunyip/anim"
	"github.com/matjam/bunyip/ecs"
	"github.com/matjam/bunyip/gfx"
	"github.com/matjam/bunyip/lin"
)

func main() {
	w := ecs.NewWorld()
	w.AddSystem("anim", anim.System)
	idle := anim.NewClip("idle", anim.Loop, anim.Scale(anim.Vec3s(anim.At(0, lin.V3(1, 1, 1)), anim.At(1, lin.V3(1, 1, 1)))))
	jump := anim.NewClip("jump", anim.Once, anim.Scale(anim.Vec3s(anim.At(0, lin.V3(2, 2, 2)), anim.At(1, lin.V3(2, 2, 2)))))
	e := w.SpawnWith(gfx.Transform{}, anim.Player{})
	p := anim.PlayerOf(w, e)
	p.Play(idle)
	w.Update(0.1)
	p.CrossFade(jump, 0.5) // blend from idle's scale to jump's over half a second
	w.Update(0.25)
	t, _ := w.Get[gfx.Transform](e)
	fmt.Printf("%.1f\n", t.Scale.X)
}
```

Output:

```
1.5
```

<a id="Player.Play"></a>

#### Player.Play

```go
func (p *Player) Play(c *Clip)
```

Play starts a clip from the beginning, replacing any playing one.

<a id="Player.Progress"></a>

#### Player.Progress

```go
func (p *Player) Progress() float32
```

Progress is how far through the clip playback is, 0 to 1.

<a id="Player.Stop"></a>

#### Player.Stop

```go
func (p *Player) Stop()
```

Stop halts playback, leaving the entity where the clip left it.

<a id="Skeleton"></a>

<a id="Skeleton.Player"></a>

<a id="Skeleton.Speed"></a>

<a id="Skeleton.KeepRootMotion"></a>

<a id="Skeleton.Blend"></a>

### Skeleton

```go
type Skeleton struct {
	Player *gfx.AnimPlayer
	Speed  float64 // zero means 1
	// KeepRootMotion leaves the transform alone so the game reads
	// Player.RootMotion itself, for a physics-driven body.
	KeepRootMotion bool
	// Blend, when set, chooses and times the player's clips from its
	// parameters every update instead of Play and CrossFade: a
	// locomotion blend space driven by SetParameter. Nil plays whatever
	// the player was told to.
	Blend *Blend
}
```

Skeleton plays a glTF model's animation clips through a gfx.AnimPlayer; draw the entity with gfx.DrawModelAnimated. Events the player crosses are emitted as SkeletonEvent, and with root motion on the player, the movement is applied to the entity's gfx.Transform when it has one.

<a id="Skeleton.Parameter"></a>

#### Skeleton.Parameter

```go
func (s *Skeleton) Parameter(name string) float32
```

Parameter reads a blend parameter; 0 without a Blend or when unset.

<a id="Skeleton.SetParameter"></a>

#### Skeleton.SetParameter

```go
func (s *Skeleton) SetParameter(name string, v float32)
```

SetParameter sets a blend parameter, such as the speed a locomotion space reads; without a Blend it does nothing.

<a id="SkeletonEvent"></a>

<a id="SkeletonEvent.Entity"></a>

<a id="SkeletonEvent.Event"></a>

### SkeletonEvent

```go
type SkeletonEvent struct {
	Entity ecs.Entity
	Event  gfx.AnimEvent
}
```

SkeletonEvent is emitted when a Skeleton's player crosses an event added with AnimPlayer.AddEvent.

<a id="Track"></a>

<a id="Track.Apply"></a>

<a id="Track.Duration"></a>

### Track

```go
type Track interface {
	// Apply samples t seconds into the track and blends into the component.
	Apply(w *ecs.World, e ecs.Entity, t, weight float32)
	// Duration reports the final key time in seconds.
	Duration() float32
}
```

Track applies an animated value to one component of an entity. The weight blends the sampled value with what the component holds, which is how crossfades and layered clips mix; 1 replaces outright.

<a id="Position"></a>

#### Position

```go
func Position(curve Curve[lin.Vec3]) Track
```

Position animates a gfx.Transform's position.

<a id="Position2"></a>

#### Position2

```go
func Position2(curve Curve[lin.Vec2]) Track
```

Position2 animates a gfx.Sprite's position.

<a id="Rotation"></a>

#### Rotation

```go
func Rotation(curve Curve[lin.Quat]) Track
```

Rotation animates a gfx.Transform's rotation.

<a id="Rotation2"></a>

#### Rotation2

```go
func Rotation2(curve Curve[float32]) Track
```

Rotation2 animates a gfx.Sprite's rotation in radians.

<a id="Scale"></a>

#### Scale

```go
func Scale(curve Curve[lin.Vec3]) Track
```

Scale animates a gfx.Transform's scale.

<a id="Size2"></a>

#### Size2

```go
func Size2(curve Curve[lin.Vec2]) Track
```

Size2 animates a gfx.Sprite's size.

<a id="Tint"></a>

#### Tint

```go
func Tint(curve Curve[gfx.Color]) Track
```

Tint animates a gfx.Sprite's colour.

## Examples

Example:

```go
package main

import (
	"fmt"

	"github.com/matjam/bunyip/anim"
	"github.com/matjam/bunyip/ecs"
	"github.com/matjam/bunyip/gfx"
	"github.com/matjam/bunyip/lin"
	"github.com/matjam/bunyip/tween"
)

func main() {
	w := ecs.NewWorld()
	w.AddSystem("anim", anim.System) // after the systems that choose clips, before drawing

	// A 3D entity: rise and spin over two seconds, then hold.
	rise := anim.NewClip("rise", anim.Once,
		anim.Position(anim.Vec3s(anim.At(0, lin.V3(0, 0, 0)), anim.AtEased(2, lin.V3(0, 4, 0), tween.OutCubic))),
		anim.Rotation(anim.Quats(anim.At(0, lin.QuatIdentity()), anim.At(2, lin.AxisAngle(lin.V3(0, 1, 0), lin.Radians(180))))),
	)
	cube := w.SpawnWith(gfx.Transform{}, anim.Player{})
	anim.PlayerOf(w, cube).Play(rise)

	// A 2D entity: a looping bob on a sprite.
	bob := anim.NewClip("bob", anim.PingPong,
		anim.Position2(anim.Vec2s(anim.At(0, lin.V2(100, 100)), anim.At(0.5, lin.V2(100, 80)))),
	)
	sprite := w.SpawnWith(gfx.Sprite{Size: lin.V2(32, 32), Color: gfx.White}, anim.Player{})
	anim.PlayerOf(w, sprite).Play(bob)

	for range 4 {
		w.Update(0.5)
	}
	t, _ := w.Get[gfx.Transform](cube)
	s, _ := w.Get[gfx.Sprite](sprite)
	fmt.Printf("cube at y=%.0f, sprite at y=%.0f\n", t.Position.Y, s.Pos.Y)
	for _, ev := range w.Events[anim.Finished]() {
		fmt.Println("finished:", ev.Clip.Name)
	}
}
```

Output:

```
cube at y=4, sprite at y=100
finished: rise
```
