> Cinder 0.1.0 documentation: cinder.anim API
> Canonical page: https://cinder.ash10.dev/docs/api/anim/ · Source: `docs/api/anim.md` in the engine repository (engine commit 47c9352, imported 2026-11-09)
> Index of every page for agents: https://cinder.ash10.dev/llms.txt

# `cinder.anim`


## `cinder.anim`

Source: `cinder/anim/__init__.mojo`

Skeletal animation: skeletons, clip sampling, blend trees, animators with crossfades, root motion and events, and a job-parallel system publishing joint palettes to the ECS.


## `cinder.anim.animator`

Source: `cinder/anim/animator.mojo`

Animator: states (blend trees) with crossfades, animation events and root motion. `update(dt)` advances the active state (and the one fading out), blends them, extracts root motion and collects events; `pose` then holds the local pose and `palette()` the skinning matrices. Events are authored per state at a normalised phase in [0, 1) and fire once each time the phase passes them, including across loop wraps.

### struct `AnimationEvent`

```mojo
struct AnimationEvent(Copyable, Movable)
```

An event that fired during `Animator.update`: the index of the state that owns it and the event's name.

Fields:

- `state`: Int
- `name`: String


### struct `AnimationState`

```mojo
struct AnimationState(Copyable, Movable)
```

A named animator state: a blend tree, whether its phase loops, and its events. Build it with `AnimationState(name, tree^, looping)` and register it with `Animator.add_state`, which validates the tree; add events afterwards with `Animator.add_event`.

Fields:

- `name`: String
- `tree`: BlendTree
- `looping`: Bool
- `events`: List[_StateEvent]

- **`__init__`** `__init__( out self, name: String, var tree: BlendTree, looping: Bool = True )`

### struct `Animator`

```mojo
struct Animator(Movable)
```

Plays the states of one skeleton with crossfades, events and root motion. Owns its skeleton, clips and poses. Author states with `add_clip_state` / `add_state`, start one with `play` (`play(state, restart=True)` replays a finished non-looping state), then call `update(dt)` once per frame (it raises until a state has been played). Read the result from `pose`, `palette()` / `palette_into` or `bone_matrices()`. Nothing is synchronised: `AnimationSystem` updates each animator from a single job at a time.

Fields:

- `skeleton`: Skeleton
- `clips`: List[Clip]
- `states`: List[AnimationState]
- `pose`: Pose
- `current`: _Layer
- `previous`: _Layer
- `fade_remaining`: Float32
- `fade_duration`: Float32
- `speed`: Float32 — Global playback rate.
- `root_motion`: Bool — When set, the root bone's horizontal translation is removed from the pose and reported through `take_root_motion`.
- `root_bone`: Int
- `finished`: Bool — A non-looping state reached its end (its phase holds 1). Cleared when `play` switches state or restarts one (`restart=True`).

- **`__init__`** `__init__(out self, var skeleton: Skeleton, var clips: List[Clip])`
- **`add_clip_state`** `add_clip_state( mut self, name: String, clip: Int, looping: Bool = True, speed: Float32 = 1.0, ) raises -> Int`  
  A state that plays one clip.
- **`add_state`** `add_state(mut self, var state: AnimationState) raises -> Int`  
  Validates `state.tree` against the animator's clips, appends the state and returns its index. Raises when the tree has no root, names a clip that does not exist, or has a blend node without children or with a parameter out of range (see `BlendTree.validate`).
- **`add_event`** `add_event(mut self, state: Int, phase: Float32, name: String) raises`  
  Adds an event `name` to `state` that fires each time the state's phase passes `phase`. `phase` is normalised to the state's cycle and must be in [0, 1). Raises for an unknown state or a phase outside that range.
- **`find_state`** `find_state(self, name: String) -> Int`  
  Index of the first state called `name`, or -1 when there is none.
- **`set_parameter`** `set_parameter(mut self, name: String, value: Float32) raises`  
  Sets a blend parameter on every state that declares it.
- **`play`** `play( mut self, state: Int, fade_seconds: Float32 = 0.0, start_phase: Float32 = 0.0, restart: Bool = False, ) raises`  
  Switches to `state` at `start_phase`, crossfading from the current one over `fade_seconds`, and clears `finished`. Playing the state that is already current is a no-op (so calling `play` every frame is safe), even when that state is non-looping and has `finished`. Pass `restart=True` to start it again from `start_phase` anyway, for example to replay a finished jump; with a fade the old playthrough fades out as the new one starts. Raises for an unknown state.
- **`current_state`** `current_state(self) -> Int`  
  Index of the playing state (the incoming one during a crossfade), or -1 before the first `play`.
- **`phase`** `phase(self) -> Float32`  
  Normalised phase of the current state: in [0, 1) while looping; a non-looping state holds 1 once it has finished.
- **`fade_weight`** `fade_weight(self) -> Float32`  
  Weight of the incoming state during a crossfade (1 when none is running).
- **`update`** `update(mut self, dt: Float32) raises`  
  Advances time, evaluates and blends the layers, then applies root motion.
- **`enable_root_motion`** `enable_root_motion(mut self, bone: Int = 0) raises`  
  Starts extracting the horizontal motion of `bone` (the hierarchy root by default).
- **`take_root_motion`** `take_root_motion(mut self) -> Vec3`  
  Horizontal root displacement accumulated since the last call (model space).
- **`take_events`** `take_events(mut self) -> List[AnimationEvent]`  
  Returns the events fired since the last call and clears the list. Only the current state fires events; a state that is fading out fires none.
- **`palette`** `palette(self) -> List[Mat4]`  
  Skinning matrices for the current pose in palette (skin joint) order. Allocates a new list; `palette_into` is the allocation-free form.
- **`palette_into`** `palette_into(mut self, mut out: List[Mat4])`  
  Skinning matrices for the current pose into `out` (no allocation once sized).
- **`bone_matrices`** `bone_matrices(self) -> List[Mat4]`  
  Every bone's matrix in model space for the current pose (attachments, IK, debug).
- **`bone_matrices_into`** `bone_matrices_into(self, mut out: List[Mat4])`  
  Every bone's model-space matrix for the current pose into `out` (resized; no allocation once large enough).


## `cinder.anim.blend`

Source: `cinder/anim/blend.mojo`

Blend trees: clips, 1D and 2D blend nodes, evaluated phase-synchronised. A tree runs on a normalised phase in [0, 1). Every leaf clip plays at `phase * duration`, so blended cycles (walk and run) stay in step; the phase advances at the weighted average of the leaves' cycle rates. Leaf weights come from the blend parameters: 1D nodes interpolate between the two nearest thresholds, 2D nodes use gradient-band interpolation (Johansen 2009), which is smooth, sums to one and reproduces each child exactly at its own point.

- `comptime NODE_CLIP = 0` — `BlendNode.kind` of a leaf that samples one clip.
- `comptime NODE_BLEND_1D = 1` — `BlendNode.kind` of a node that blends its children along one parameter.
- `comptime NODE_BLEND_2D = 2` — `BlendNode.kind` of a node that blends its children over two parameters (gradient bands).
- `comptime MAX_CHILDREN = 8` — Children per blend node.
### struct `BlendNode`

```mojo
struct BlendNode(Copyable, Movable)
```

One node of a `BlendTree`: a clip leaf or a 1D/2D blend over child node indices. Created by `BlendTree.add_clip`, `add_blend_1d` and `add_blend_2d`; indices a kind does not use stay -1 and its lists stay empty.

Fields:

- `kind`: Int
- `clip`: Int — Clip index (leaf).
- `speed`: Float32 — Playback rate multiplier (leaf).
- `parameter`: Int — Blend parameter index (1D: one scalar; 2D: `parameter` is x and `parameter2` is y).
- `parameter2`: Int
- `children`: List[Int]
- `points`: List[Vec2] — Per child: the parameter value (1D uses x) where the child has full weight.

- **`__init__`** `__init__(out self, kind: Int)`

### struct `BlendTree`

```mojo
struct BlendTree(Copyable, Movable)
```

Nodes are stored flat; `root` names the entry node. Build with `add_clip`, `add_blend_1d` and `add_blend_2d`, then `set_root`.

Fields:

- `nodes`: List[BlendNode]
- `root`: Int
- `parameter_names`: List[String]
- `parameters`: List[Float32]

- **`__init__`** `__init__(out self)`
- **`add_parameter`** `add_parameter(mut self, name: String, initial: Float32 = 0.0) -> Int`  
  Declares a blend parameter with an `initial` value and returns its index, which `add_blend_1d` / `add_blend_2d` take. Duplicate names are not rejected; `set_parameter` updates the first match.
- **`set_parameter`** `set_parameter(mut self, name: String, value: Float32) raises`  
  Sets the first parameter called `name` to `value`. Raises when the tree has no such parameter.
- **`add_clip`** `add_clip(mut self, clip: Int, speed: Float32 = 1.0) -> Int`  
  Adds a leaf that plays clip index `clip` at `speed` times its natural rate and returns the node index. The clip index is checked by `validate`, not here.
- **`add_blend_1d`** `add_blend_1d(mut self, parameter: Int) -> Int`  
  Adds a 1D blend node driven by parameter index `parameter` and returns the node index. Attach children with `add_child`; the parameter index is checked by `validate`.
- **`add_blend_2d`** `add_blend_2d(mut self, parameter_x: Int, parameter_y: Int) -> Int`  
  Adds a 2D blend node driven by parameters `parameter_x` and `parameter_y` and returns the node index. Attach children with `add_child`; the parameter indices are checked by `validate`.
- **`add_child`** `add_child(mut self, node: Int, child: Int, at: Vec2) raises`  
  Attaches `child` to a blend node with full weight at parameter value `at` (1D nodes use `at.x()`). Children of a 1D node must be added in increasing order. Raises when an index is out of range, `node` is a clip, the node is full, a 1D threshold does not increase, or the edge would close a cycle (`child` is `node` or one of its ancestors): a tree must stay acyclic for evaluation to terminate. A node may still be shared by several parents.
- **`set_root`** `set_root(mut self, node: Int) raises`  
  Makes `node` the entry node of the tree. Raises when the index is out of range.
- **`validate`** `validate(self, clip_count: Int) raises`  
  Checks that the tree can be evaluated with `clip_count` clips and raises on the first problem: no root (or one out of range), a clip leaf naming a missing clip, a blend node without children or with a child index out of range, a blend parameter index out of range (2D nodes need both), or a cycle (a node that is its own descendant; `add_child` already refuses these, but `nodes` is public). `Animator.add_state` calls it.
- **`leaf_weights_into`** `leaf_weights_into(self, mut weights: List[Float32])`  
  Weight per node (leaves carry the blended weight, inner nodes their subtree's), summing to one over the leaves, written into `weights`.
- **`leaf_weights`** `leaf_weights(self) -> List[Float32]`  
  Per-node weights for the current parameters (see `leaf_weights_into`) in a new list; allocates.
- **`cycle_rate`** `cycle_rate(self, clips: List[Clip]) -> Float32`  
  Phase advance per second: the weighted average of each leaf's speed / duration.
- **`cycle_rate_with`** `cycle_rate_with( self, clips: List[Clip], mut weights: List[Float32] ) -> Float32`  
  `cycle_rate` using the caller's weight scratch (no allocation once sized).
- **`evaluate`** `evaluate( self, clips: List[Clip], skeleton: Skeleton, phase: Float32, mut pose: Pose, mut scratch: Pose, )`  
  Blends every weighted leaf at `phase` into `pose` (allocates a weight list; the animator uses `evaluate_with`).
- **`evaluate_with`** `evaluate_with( self, clips: List[Clip], skeleton: Skeleton, phase: Float32, mut pose: Pose, mut scratch: Pose, mut weights: List[Float32], )`  
  Blends every weighted leaf at `phase` into `pose`. `scratch` is a pose of the same skeleton for the per-leaf samples and `weights` the per-node weight scratch; callers reuse both across frames so nothing is allocated.


## `cinder.anim.sampling`

Source: `cinder/anim/sampling.mojo`

Clip sampling: step, linear and cubic-spline keyframes into a local pose. `Pose` is the skeleton's local transforms plus morph weights. Sampling starts from the rest pose so channels that are missing from a clip keep the skeleton's values (glTF semantics).

- `comptime PATH_TRANSLATION = 0` — `ClipChannel.path` of a translation channel (3 components).
- `comptime PATH_ROTATION = 1` — `ClipChannel.path` of a rotation channel (quaternion x, y, z, w).
- `comptime PATH_SCALE = 2` — `ClipChannel.path` of a scale channel (3 components).
- `comptime PATH_WEIGHTS = 3` — `ClipChannel.path` of a morph target weights channel (one component per target).
- `comptime INTERP_LINEAR = 0` — `ClipChannel.interpolation`: linear between keys (slerp for rotations).
- `comptime INTERP_STEP = 1` — `ClipChannel.interpolation`: each key's value holds until the next key.
- `comptime INTERP_CUBIC = 2` — `ClipChannel.interpolation`: glTF cubic spline; each key stores in-tangent, value and out-tangent.
### struct `Pose`

```mojo
struct Pose(Copyable, Movable)
```

A local pose: one parent-space transform per bone plus the morph target weights. `Pose(skeleton)` starts at the rest pose with zero weights. A pose is tied to the skeleton it was built for: sampling, blending and `reset` index by bone and never resize.

Fields:

- `local`: List[Transform]
- `morph_weights`: List[Float32]

- **`__init__`** `__init__(out self, skeleton: Skeleton)`
- **`__init__`** `__init__( out self, var local: List[Transform], var morph_weights: List[Float32] )`
- **`reset`** `reset(mut self, skeleton: Skeleton)`  
  Restores the rest pose of `skeleton` and zeroes the morph weights in place (no allocation). `skeleton` must be the one the pose was built for.
- **`copy_from`** `copy_from(mut self, other: Pose)`  
  Element-wise copy (same skeleton), no allocation.
- **`blend_with`** `blend_with(mut self, b: Pose, t: Float32)`  
  self = lerp(self, b, t) component-wise.

### def `sample_component`

```mojo
def sample_component( ch: ClipChannel, pair: _KeyPair, component: Int ) -> Float32
```

One output component at the located time.


### def `sample_rotation`

```mojo
def sample_rotation(ch: ClipChannel, pair: _KeyPair) -> Quat
```

Rotations interpolate along the shorter arc (slerp for linear, normalised spline for cubic, as the glTF specification describes).


### def `sample_clip`

```mojo
def sample_clip(clip: Clip, skeleton: Skeleton, time: Float32, mut pose: Pose)
```

Writes every channel of `clip` at `time` (seconds, unclamped: before the first key or after the last key the end values hold) into `pose`. Channels whose node is not in the skeleton are ignored; `pose` must have been created for `skeleton`.


### def `wrap_time`

```mojo
def wrap_time(time: Float32, duration: Float32) -> Float32
```

Loops `time` into [0, duration).



## `cinder.anim.skeleton`

Source: `cinder/anim/skeleton.mojo`

Skeletons: a bone hierarchy with rest pose, the skin's joint list and inverse bind matrices. A skeleton is built from a glTF skin (every joint plus the ancestors needed to reach a common root, so animated parents move their children) or read from a cooked `.cskel` file. Clips keep glTF node indices, which `node_to_bone` maps to bones.

- `comptime SKELETON_MAGIC = "CSKL"` — Four-byte tag at the start of a cooked `.cskel` file.
- `comptime SKELETON_VERSION = 1` — Cooked skeleton format version; `read_skeleton` rejects any other.
- `comptime MAX_BONES = 256` — Upper bound for a palette (the renderer's joints buffer grows in 1024-matrix chunks).
### struct `Skeleton`

```mojo
struct Skeleton(Copyable, Movable)
```

Bone hierarchy, rest pose and skin data of one glTF skin. Bones are ordered parents first. Build one with `skeleton_from_gltf` or `read_skeleton` (both validate it); `palette` and `world_transforms` turn a local pose into matrices.

Fields:

- `names`: List[String]
- `parents`: List[Int] — Parent bone per bone (-1 for roots); parents always precede children.
- `rest`: List[Transform] — Rest pose in parent space.
- `node_to_bone`: List[Int] — glTF node index -> bone index (-1 when the node is not part of the skeleton).
- `joints`: List[Int] — Skin joint i -> bone index (palette order).
- `inverse_bind`: List[Mat4]
- `mesh_node`: Int — glTF node that carries the skinned mesh (-1 when unknown); the palette is expressed in its rest space so the render instance keeps the node's world transform.
- `mesh_node_inverse`: Mat4
- `morph_count`: Int — Morph target weights animated on the mesh node (0 when none).

- **`__init__`** `__init__(out self)`
- **`bone_count`** `bone_count(self) -> Int`  
  Number of bones (skin joints plus the ancestors needed to reach a common root).
- **`joint_count`** `joint_count(self) -> Int`  
  Number of skin joints, which is the length of the skinning palette.
- **`find_bone`** `find_bone(self, name: String) -> Int`  
  Index of the first bone called `name`, or -1 when there is none.
- **`bone_of_node`** `bone_of_node(self, node: Int) -> Int`  
  Bone index of glTF node `node`, or -1 when the node is out of range or not part of the skeleton.
- **`world_transforms_into`** `world_transforms_into( self, local: List[Transform], mut out: List[Mat4] )`  
  Bone-space matrices from a local pose (parents first), in model space, written into `out` (resized; no allocation once large enough).
- **`world_transforms`** `world_transforms(self, local: List[Transform]) -> List[Mat4]`  
  Model-space bone matrices for the local pose `local` in a new list; allocates (see `world_transforms_into`).
- **`palette_into`** `palette_into( self, local: List[Transform], mut worlds: List[Mat4], mut out: List[Mat4], )`  
  Skinning matrices in palette order into `out`; `worlds` is scratch for the bone matrices.
- **`palette`** `palette(self, local: List[Transform]) -> List[Mat4]`  
  Skinning matrices in palette order: inverse(mesh node) * bone world * inverse bind.
- **`validate`** `validate(self) raises`  
  Checks the invariants and raises on the first violation: 1 to `MAX_BONES` bones, one name and rest transform per bone, every parent before its child (or -1), node map and joints inside the bone range (or -1), and one inverse bind matrix per joint.

### def `skeleton_from_gltf`

```mojo
def skeleton_from_gltf(asset: GltfAsset, skin: Int) raises -> Skeleton
```

Builds the skeleton of `asset.skins[skin]`: the joints and every ancestor up to the outermost ancestor shared by all joints (so animated parents are evaluated), in parent-before-child order. The mesh node is the first node using the skin.


### def `write_skeleton`

```mojo
def write_skeleton(skeleton: Skeleton) -> List[UInt8]
```

Serialises `skeleton` to the cooked `.cskel` format: magic, version, bones (name, parent, rest transform), node map, joints with inverse bind matrices, mesh node and its inverse, morph count. Indices are stored plus one so -1 round-trips as 0. Does not validate.


### def `read_skeleton`

```mojo
def read_skeleton(var data: List[UInt8]) raises -> Skeleton
```

Parses a cooked `.cskel` buffer (consumed) into a validated skeleton. Raises on a wrong magic or version, implausible bone, node or joint counts, data that ends early, trailing bytes, or a result that fails `Skeleton.validate`.



## `cinder.anim.system`

Source: `cinder/anim/system.mojo`

The animation system: animators stored in a pool, advanced in parallel on the job system, and published to the ECS as `JointPalette` components for the render bridge. Animators live in the system (they own clips and poses); entities refer to them with `AnimatorRef`. `update(dt, jobs)` runs one job per animator: each job touches only its own animator, so no synchronisation is needed beyond the completion counter.

- `comptime ANIMATOR_REF_COMPONENT_ID = 8` — ECS component id of `AnimatorRef`.
### struct `AnimatorRef`

```mojo
struct AnimatorRef(Component)
```

Links an entity to `AnimationSystem.animators[index]`.

Fields:

- `index`: Int

- **`component_id`** `component_id() -> Int`

### struct `AnimationSystem`

```mojo
struct AnimationSystem(Movable)
```

Owns every animator, updates them in parallel and publishes their palettes to the ECS. Entities refer to animators by index through `AnimatorRef`; `updates` counts completed `update` calls. Jobs receive raw pointers into `animators`, so the system must stay in place and the list must not change while `update` runs.

Fields:

- `animators`: List[Animator]
- `updates`: Int

- **`__init__`** `__init__(out self)`
- **`add`** `add(mut self, var animator: Animator) -> Int`  
  Takes ownership of `animator` and returns its index for `AnimatorRef`. Call between frames, never while `update` is running.
- **`__len__`** `__len__(self) -> Int`
- **`clear`** `clear(mut self)`  
  Drops every animator (a level restart); `AnimatorRef`s become stale.
- **`update`** `update( mut self, dt: Float32, jobs: Optional[Pointer[JobSystem, MutUntrackedOrigin]], ) raises`  
  Advances every animator by `dt`. With a job system (and more than one animator) each animator is its own job; otherwise they run inline, one after the other. Either way every animator is updated, and when any of them raised (an animator that was never played raises "update before play") the call raises afterwards, naming the first one that failed; the others have still advanced. `updates` counts only calls that did not raise.
- **`publish`** `publish(mut self, mut world: World) raises -> Int`  
  Writes a `JointPalette` for every entity with an `AnimatorRef`, in place when the component exists (no steady-state allocation). Returns the count.
