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
// 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 },
},
},
},
})// 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
},
})// 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
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
}>
}): SpriteSheetOne 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;perDirectionclips take one local row per entry ofdirections(SpriteAnimation).
Make a sprite by name: sheet.make
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
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 assprite.get(SpriteAnimation)). Need control? Name a clip and use.anim. - Looking for
collider:? Sheets are art only — putShape2D(+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
Shape2Daspects)