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
npm install spryteimport { 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 fallbackcreateRenderer 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, 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 usecreateImageBitmap. - 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 listenerAfter 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.quadsanddraw.drawCalls: how much you drew this frame, and in how many GPU calls.draw.raw: the underlyingWebGL2RenderingContextorCanvasRenderingContext2D, 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.