SpriteAnimation
Sprite-sheet animation, as an aspect on a Sprite — accessor node.anim. Clips are
declared as data (a frame cell size plus named frame-index lists) and then driven by name; once
defined, playback advances entirely in native code.
At a glance
const hero = new Sprite({ texture: tex, anchor: [0.5, 1] })
.aspect(SpriteAnimation, {
size: [32, 48], // one frame cell, in texture pixels
fps: 10,
clips: {
idle: [0, 1],
walk: [4, 5, 6, 7],
die: { frames: [8, 9, 10], fps: 6, loop: false },
},
})
hero.anim.play('walk')
hero.addEventListener('completed', clip => { if (clip === 'die') hero.destroy() })Configuring
sprite.aspect(SpriteAnimation, {
size?: [w, h] // frame cell size in texture pixels; default = full texture
cols?: number // grid columns override; default floor(texture.width / cellW)
origin?: [x, y] // pixel origin of the grid (a sheet region); default [0, 0]
fps?: number // default fps for clips; default 12
loop?: boolean // default loop for clips; default true
directions?: string[] // facing names in texture-row order (see perDirection)
clips?: Record<string, Clip> // Clip = number[] | { frames, fps?, loop?, perDirection? }
})Frame indices count row-major through the sheet's grid: the grid has
floor(texture.width / cellW) columns, index 0 is the top-left cell, and indices continue
left-to-right, then down. A clip is either a plain index array (inherits fps/loop) or an
object with per-clip overrides.
Directional sheets (one texture row per facing): set directions in row order — compass
tokens S/SE/E/NE/N/NW/W/SW — and mark clips perDirection: true. Each expands to
one native clip per facing (walk_SE, …) with the base row-0 frames shifted down by the row.
the native clips are sliced from the texture's pixel size, so they're defined when the
sprite has a texture at attach time. Without one (a SpriteSheet's lazy
load), the define is deferred — play() calls queue by name and start once the texture arrives
(unknown names then throw at define time).
passing size also sets the sprite's world size to the cell size, so one cell draws
at 1 texel = 1 world unit.
sprite.anim.define(): this // rebuild native clips after mutating anim.clipsPlayback
sprite.anim.play(name: string, dir?: string | Vec2Like): this
sprite.anim.stop(): this
sprite.anim.speed = 2 // playback rate multiplier (write-only)
sprite.anim.current // name of the playing clip (expanded, e.g. 'walk_SE')
sprite.anim.direction // current facing, or null before the first directional play
sprite.anim.frame // current frame index (read-only)Re-playing the already-active clip is a no-op — safe to call play('walk') every frame from
input code without restarting the animation.
For a perDirection clip, dir picks the facing: a direction name, or a movement vector
(Y-up; the nearest compass row wins — play('walk', inputVector) is the whole walk loop). The
last direction sticks, so a later play('idle') keeps facing; a zero vector keeps it too.
Events
Animation events are emitted on the node, with the clip name as the argument:
hero.addEventListener('loopReached', clip => {}) // a looping clip wrapped around (each loop)
hero.addEventListener('completed', clip => {}) // a non-looping clip finished