Using spryte on its own: fast 2D drawing on any canvas
DEV Community

Using spryte on its own: fast 2D drawing on any canvas

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

npm install spryte
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:

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.

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, 320x180 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

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.

Read on DEV Community ↗ ← Back to News

Comments

No comments yet. Start the discussion.