Skip to content

Sprites ​

draw.sprite(frameOrImage, x, y, options?) draws an image, or part of one, at a position. It's the call most games make most often, and it doesn't allocate.

ts
// whole image, centred on (160, 90)
draw.sprite(hero, 160, 90);
// rotated and tinted
draw.sprite(hero, 160, 90, { rotation: 0.3, tint: "#f0f" });

Images and frames ​

The first argument is either an image (HTMLImageElement, HTMLCanvasElement, OffscreenCanvas, ImageBitmap or a render target) or a Frame: a rectangle of an image, { image, x, y, w, h }.

ts
import type { Frame } from "spryte";

const sheet = await load("/hero-sheet.png"); // load() from Getting started

// A 4-frame walk cycle on a 16×16 grid
const walk: Frame[] = [0, 1, 2, 3].map((i) => ({ image: sheet, x: i * 16, y: 0, w: 16, h: 16 }));

function render(time: number) {
  const frame = walk[Math.floor(time / 100) % walk.length]!;
  draw.sprite(frame, player.x, player.y);
}

A texture atlas works the same way: build one Frame per region from your atlas data, all pointing at the same image.

Options ​

OptionDefault
origin[0.5, 0.5]Anchor as a fraction of the frame size. [0, 0] is the top-left corner, [0.5, 1] the bottom centre.
scaleX, scaleY1Scale on each axis.
rotation0Radians, around the origin.
flipX, flipYfalseMirror on an axis, around the origin.
alpha1Opacity, multiplied with the global draw.alpha.
tintwhiteColour multiplier.
flash00 to 1 mix toward white, for hit flashes.
smoothdraw.smoothLinear filtering for this sprite only.
ts
// A character standing on the ground, facing left, flashing after a hit
draw.sprite(frame, x, groundY, {
  origin: [0.5, 1],
  flipX: facing < 0,
  flash: hitTimer > 0 ? 1 : 0,
});

// A coin that pulses
const pulse = 1 + Math.sin(t * 8) * 0.1;
draw.sprite(coin, x, y, { scaleX: pulse, scaleY: pulse });

// A ghost: half transparent, tinted blue
draw.sprite(ghost, x, y, { alpha: 0.5, tint: "#88aaff" });

tint takes any spryte colour: "#rgb", "#rgba", "#rrggbb", "#rrggbbaa", a number 0xRRGGBBAA, or any CSS colour. See colours.

Reuse one options object ​

sprite() reads the options and doesn't keep them, so reuse one object in a hot loop instead of creating one per sprite:

ts
import type { SpriteDrawOptions } from "spryte";

const opts: SpriteDrawOptions = { rotation: 0, alpha: 1 };

for (const enemy of enemies) {
  opts.rotation = enemy.angle;
  opts.alpha = enemy.fade;
  draw.sprite(enemyFrame, enemy.x, enemy.y, opts);
}

Calling draw.sprite(img, x, y) with no options doesn't allocate either.

Draw order ​

Call order is draw order: later calls paint over earlier ones. There's no z-index or scene graph, so draw layers in the order you want them:

ts
draw.begin("#000", 320, 180, scale);
drawBackground();
for (const e of enemies) draw.sprite(enemyFrame, e.x, e.y);
draw.sprite(playerFrame, player.x, player.y);
drawHud();
draw.end();

If you need depth sorting, sort your own array before the loop. spryte batches your calls automatically; draw.drawCalls shows how many GPU calls a frame took.

Pixel snapping ​

With smooth off, unrotated sprites are placed on whole pixels. Nearest sampling at fractional positions makes edges gain and lose a pixel between frames, which reads as flicker. Rotated sprites aren't snapped. Pixel art has the details.

Changing pixels ​

If you draw from a canvas and change its pixels, call draw.invalidate(canvas) to re-upload it. Images that never change need nothing.

Next: Bulk sprites