Sprite sheets (.sprite.ts)

Sprite sheets as data: one .sprite.ts file per image, export default defineSpriteSheet({...}), naming the image's regions, their animation grids and clips, so game code makes sprites and plays clips by name and never hardcodes rects. The sprite editor (lecodes-2d-editor) reads and writes these files; hand edits are preserved. For a sheet driven from code, attach SpriteAnimation to a Sprite directly.

  • name the sprites and animations of one image in a file
  • make a sprite by name from a sprite sheet
  • loop an animated tile such as water or a torch
  • give a character walk and idle clips for every facing
  • preload a sprite sheet's image
  • put a collider on a sprite made from a sheet

Platforms: iOS, Android, desktop and the web player; not on the web (Web Lite); headless tests load and animate it but draw nothing.

At a glance

TypeScript
// hero.sprite.ts
export default defineSpriteSheet({
  image: asset('./hero.png'),
  anchor: [0.5, 1],
  fps: 10,
  sprites: {
    hero: {
      rect: [0, 0, 1344, 1536],
      slice: [7, 8],                       // 7×8 grid of 192px cells
      directions: ['S', 'SE', 'E', 'NE', 'N', 'NW', 'W', 'SW'],
      clips: {
        idle: { frames: [0], perDirection: true },
        walk: { frames: [1, 2, 3, 4, 5, 6], perDirection: true },
      },
    },
  },
})
TypeScript
// props.sprite.ts
export default defineSpriteSheet({
  image: asset('./props.png'),
  anchor: [0.5, 1],
  sprites: {
    tree: { rect: [0, 0, 64, 128] },
    water: { rect: [64, 0, 128, 32], slice: [4, 1] },    // four 32px frames
  },
})
TypeScript
// main.ts
import hero from './hero.sprite'
import props from './props.sprite'

const scene = new Scene2D()
scene.add(props.make('tree', { position: [80, 144], layer: 1 }))
scene.add(props.make('water', { position: [0, 0] }))    // sliced, no clips → loops on its own
const player = hero.make('hero', { clip: 'idle', layer: 1 })
scene.add(player)
player.anim.play('walk', [1, 0])                          // walk, facing east
scene.open()

Define a sheet: defineSpriteSheet

TypeScript
defineSpriteSheet({
  image: string                     // asset('./sheet.png')
  anchor?: [x, y]                   // default pivot for every sprite; [0.5, 1] = feet
  fps?: number                      // default playback fps for sliced sprites / clips (else 12)
  sprites?: Record<string, {
    rect?: [x, y, w, h]             // the region's pixel rect in the image
    slice?: [cols, rows]            // subdivide the rect into an animation grid (opt-in)
    fps?: number                    // per-sprite playback fps override
    clips?: Record<string, Clip>    // named runs over the LOCAL grid (opt-in; see SpriteAnimation)
    directions?: string[]           // facing names in local-row order for perDirection clips
    anchor?: [x, y]                 //   per-sprite override
  }>
}): SpriteSheet

One model: every named entry is a region of the image (a rect), optionally sliced into an animation grid, optionally carrying named clips over that grid.

Static sprites, looping tiles and characters

A sheet entry is a static sprite, a looping animated tile (water, a torch) or a character with clips — each tier opt-in:

  • rect only — a static sprite (props, items).
  • rect + slice — an animated tile: all cells loop in order the moment it's made (water, fire, torches). No clip name to call — it runs itself.
  • rect + slice + clips — a character: clips name frame runs over the sprite's local grid (row-major, index 0 = the rect's top-left cell), driven by name via .anim; perDirection clips take one local row per entry of directions (SpriteAnimation).

Make a sprite by name: sheet.make

TypeScript
sheet.make(name, opts & { clip: string }): Sprite & { anim: SpriteAnimation }  // starts a clip
sheet.make(name, opts?): Sprite
// opts: { position?, layer?, anchor?, clip?, direction? }

make() assembles the node's ART: frame, world size, anchor, animation. An unknown name throws. A sheet never carries world semantics — colliders and bodies belong to the scene: attach Shape2D / Physics2D / Trigger2D to the placed node (an aspects: list in a .scene2d.ts, or .aspect(...) in code).

The world size of a sliced sprite is one cell (rect / counts), known before the texture loads.

Preload the image and read the sheet: sheet.load

TypeScript
sheet.load(): Promise<Texture2D>   // idempotent explicit preload
sheet.texture                      // Texture2D | null (null until loaded)
sheet.spriteNames                  // string[]
sheet.rectOf(name)                 // the region's pixel rect (the whole rect, sliced or not), or null
sheet.def                          // the definition (read-only by convention)

The texture loads lazily on the first make() (or an explicit sheet.load()); sprites created before it resolves get their frame applied on arrival, so layout and Y-sort don't wait for pixels.

Pitfalls

  • Clip frame indices are local to the sprite's rect — index 0 is the rect's top-left cell, not the image's.
  • A sliced sprite with no clips plays one implicit clip, 'all'; make() types it without .anim (it is reachable as sprite.get(SpriteAnimation)). Need control? Name a clip and use .anim.
  • Looking for collider:? Sheets are art only — put Shape2D (+ Physics2D / Trigger2D) on the placed node in the scene.

See also

  • SpriteAnimation — clips, perDirection, vector-resolved playback
  • Sprite — the node make() builds
  • Physics2D — bodies for placed sprites (via Shape2D aspects)