Skip to main content
Version: Next

Spritesheet & Animation

A spritesheet packs many frames into a single image. Instead of loading dozens of separate files, you load one sheet (one network request, one decode, one texture on WebGL) and draw sub-regions of it per item.

Support comes in three layers, each building on the previous one:

  1. sprite — a source rectangle on any ImageItem (the primitive)
  2. SpriteSheet — maps grid positions like (col, row) to source rectangles
  3. SpriteAnimator — flips through frames over time

All renderers (renderer-canvas, renderer-webgl, renderer-skia, renderer-server) support sprites with identical results.

Drawing a Sheet Region (sprite)​

Every ImageItem accepts an optional sprite property — a pixel rectangle inside the image. When set, only that region is drawn, and the aspect ratio is derived from the frame instead of the whole image.

const sheet = await engine.images.load("/assets/dragon.png");

// Draw the 144x128 frame located at pixel (288, 0) of the sheet
engine.drawImage({
x: 5,
y: 3,
size: 2,
img: sheet,
sprite: { x: 288, y: 0, w: 144, h: 128 },
});
PropertyTypeDescription
x, ynumberTop-left corner of the frame in sheet pixels.
w, hnumberFrame size in sheet pixels.
tip

sprite also works with drawStaticImage for cached static content — but note that animation is not possible on the static path, since the cache freezes whatever frame was set at build time.

Grid Sheets (SpriteSheet)​

Most sheets are uniform grids. SpriteSheet does the pixel math for you — it holds no image reference, it is a pure calculator.

import { SpriteSheet } from "@canvas-tile-engine/core";

// A 432x512 sheet with 3 columns x 4 rows of 144x128 frames
const sheet = new SpriteSheet({ frameWidth: 144, frameHeight: 128, columns: 3 });

// By grid position
engine.drawImage({ x: 5, y: 3, img, sprite: sheet.frame(2, 1) });

// By linear index (left-to-right, top-to-bottom; requires `columns`)
engine.drawImage({ x: 7, y: 3, img, sprite: sheet.frameByIndex(7) });

Options:

OptionTypeDefaultDescription
frameWidthnumberRequiredWidth of a single frame in pixels.
frameHeightnumberRequiredHeight of a single frame in pixels.
columnsnumber-Number of columns; needed for frameByIndex.
marginnumber0Outer offset from the sheet edges.
spacingnumber0Gap between adjacent frames.

Methods:

MethodReturnsDescription
frame(col, row)SpriteRectSource rect of the frame at a grid position.
frameByIndex(index)SpriteRectSource rect by linear index.
framesInRow(row, startCol, endCol)SpriteRect[]Consecutive frames of one row — ideal for animations.
WebGL & frame bleeding

On the WebGL renderer, zooming can sample a sliver of the neighboring frame at frame edges (texture bleeding). If you see this, author your sheet with a few pixels of spacing between frames and pass it to SpriteSheet.

Animation (SpriteAnimator)​

SpriteAnimator plays a list of frames at a fixed fps. Its callback fires only when the frame index changes, so renders happen at the animation's fps — not at 60fps.

import { SpriteAnimator, SpriteSheet } from "@canvas-tile-engine/core";

const sheet = new SpriteSheet({ frameWidth: 144, frameHeight: 128, columns: 3 });

const item = { x: 5, y: 3, size: 2, img, sprite: sheet.frame(0, 0) };
engine.drawImage(item);

const animator = new SpriteAnimator({
frames: sheet.framesInRow(0, 0, 2), // frames (0,0) -> (2,0)
fps: 8,
});

animator.start((frame) => {
item.sprite = frame; // mutate the drawn item...
engine.render(); // ...and repaint
});

Because the callback just mutates the item and re-renders, animation works identically on every renderer — there is no renderer-specific animation code.

SpriteAnimation options:

OptionTypeDefaultDescription
framesSpriteRect[]RequiredFrames in play order.
fpsnumberRequiredPlayback speed in frames per second.
loopbooleantrueRestart from the first frame after the last.

Methods:

MethodDescription
start(onFrame, onComplete?)Start playback; fires immediately with the first frame.
stop()Stop playback; the last applied frame stays drawn.
isRunning()Whether the loop is currently scheduled.
frameAt(elapsedMs)Pure timing math — the frame for a given elapsed time (useful in tests).
// One-shot animation (e.g. an explosion)
const boom = new SpriteAnimator({ frames: explosionFrames, fps: 12, loop: false });
boom.start(
(frame) => {
item.sprite = frame;
engine.render();
},
() => engine.removeDrawHandle(handle), // clean up when finished
);
Full example

See examples/vanilla-js-examples/spritesheet in the repository for a complete runnable demo — four flap-cycle animations from a single dragon sheet, with click-to-pause.

pnpm dev:example --example=vanilla-js-spritesheet