Skip to content

Using spryte on its own ​

spryte is the renderer inside debuff, but it doesn't need debuff. If you have a canvas (an editor, a visualisation, your own engine, a tiny game), it gives you fast 2D drawing with a small, explicit API. Here's everything a standalone setup needs.

Install and create ​

bash
npm install spryte
ts
import { createRenderer } from "spryte";

const canvas = document.querySelector("canvas")!;
const draw = createRenderer(canvas, { smooth: false }); // smooth: false for pixel art
await draw.ready;                                       // instant on WebGL2; waits for the fallback otherwise
console.log(draw.kind);                                 // "webgl2", or "2d" on the Canvas 2D fallback

createRenderer uses WebGL2 when it can. Otherwise it loads the Canvas 2D backend, a separate small chunk that WebGL2 browsers never download. Pass { backend: "webgl2" } to require WebGL2, or "2d" to force the fallback.

Size the canvas for sharp pixels ​

spryte draws in logical units you choose, and you own the canvas's backing size. Size it for the display, then tell spryte:

ts
function resize(): number {
  const dpr = devicePixelRatio || 1;
  const w = Math.round(canvas.clientWidth * dpr);
  const h = Math.round(canvas.clientHeight * dpr);
  if (canvas.width !== w || canvas.height !== h) {
    canvas.width = w;
    canvas.height = h;
    draw.resize(w, h);
  }
  return dpr;
}

The frame loop ​

Each frame: begin, your drawing in paint order, then end.

ts
function frame() {
  const dpr = resize();
  const w = canvas.clientWidth;
  const h = canvas.clientHeight;
  draw.begin("#111", w, h, dpr); // clear colour, logical width and height, backing pixels per unit
  draw.rect(10, 10, 40, 20, "#cffb2d");
  draw.sprite(hero, w / 2, h / 2, { rotation: t, tint: "#ffffff" });
  draw.text(`${draw.kind} · ${draw.drawCalls} draw calls`, 8, 8, { font: "14px sans-serif" });
  draw.end();
  requestAnimationFrame(frame);
}
requestAnimationFrame(frame);

For a fixed pixel-art resolution, draw at, say, 320×180 logical units with scale = canvas.width / 320, and let the browser letterbox the canvas.

Images ​

spryte draws HTMLImageElement, HTMLCanvasElement, OffscreenCanvas and ImageBitmap objects. Each is uploaded to the GPU the first time you draw it, and cached per object.

  • Make sure an image has loaded before drawing it: await img.decode(), or use createImageBitmap.
  • For a sprite sheet, pass a Frame, { image, x, y, w, h }, to draw part of an image.
  • If you change a canvas you draw from, call draw.invalidate(thatCanvas) so spryte re-uploads it.

Context loss ​

On phones, the browser can take the GPU away from a backgrounded tab. spryte handles this: while draw.contextLost is true, drawing is skipped, and when the context comes back it rebuilds its GPU state and re-uploads images on first use. The one thing that doesn't come back is the contents of render targets, so redraw those each frame or when contextLost turns false.

Cleaning up ​

ts
draw.dispose(); // frees every GPU object and listener

After dispose(), using that Draw throws a clear error rather than failing silently. Disposing twice is harmless.

Debugging ​

  • draw.kind: which backend you're on.
  • draw.quads and draw.drawCalls: how much you drew this frame, and in how many GPU calls.
  • draw.raw: the underlying WebGL2RenderingContext or CanvasRenderingContext2D, if you need to step outside spryte (restore any state you change).

Next: Particles and bullets with draw.sprites, and Post-processing with spryte. The full API reference is on this site.