LeCodesdocs

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

TypeScript
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

TypeScript
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.

Note

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).

Note

passing size also sets the sprite's world size to the cell size, so one cell draws at 1 texel = 1 world unit.

TypeScript
sprite.anim.define(): this    // rebuild native clips after mutating anim.clips

Playback

TypeScript
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:

TypeScript
hero.addEventListener('loopReached', clip => {})   // a looping clip wrapped around (each loop)
hero.addEventListener('completed', clip => {})     // a non-looping clip finished

See also

  • Sprite — the host node; manual frame / setFramePx
  • Texture2D — loading the sheet
  • Node2DaddEventListener