Introducing spryte
Every debuff game draws through a renderer. Until now it lived inside @debuffjp/core. Today it's its own library, spryte: MIT, no dependencies, and usable in any project, with or without debuff.
npm install spryteimport { createRenderer } from "spryte";
const draw = createRenderer(canvas, { smooth: false }); // pixel art
draw.resize(canvas.width, canvas.height);
function frame() {
draw.begin("#111", 320, 180, canvas.width / 320); // clear colour, logical size, pixels per unit
draw.sprite(hero, 160, 90, { rotation: 0.2, tint: "#cffb2d" });
draw.text("hello", 8, 8);
draw.end();
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);What it is
spryte is an immediate-mode 2D renderer. You don't build a scene graph of display objects: each frame, you draw what you want, in order, and spryte batches it onto the GPU.
- Sprites, shapes and text:
sprite,rect,circle,line,text(CSS fonts or bitmap fonts), with transforms, clips, tint, alpha and blend modes. - Bulk drawing:
draw.sprites()draws one image at thousands of positions from aFloat32Array, with optional per-copy scale, rotation and colour. On WebGL2 the arrays go to the GPU as they are, with no JavaScript per sprite. See Particles and bullets with draw.sprites. - Render targets and shaders: draw into offscreen images, then draw them through your own fragment effects. See Post-processing with spryte.
- WebGL2 with a Canvas 2D fallback, loaded only on browsers that need it. Context loss is handled.
- About 10 kB of JavaScript (brotli) for a small app.
How fast
On our benchmark, against PixiJS 8, same machine, assets and simulation, after a check that both draw the same pixels:
| spryte | PixiJS 8 | ||
|---|---|---|---|
| 200k particles, every property changing | 7.7 ms | 28.9 ms (ParticleContainer) | 3.8× faster |
| 50k rotated, tinted sprites, one call each | 9.8 ms | 32.3 ms (Sprite) | 3.3× faster |
| 200k sprites, position only, bulk | 4.4 ms | 8.9 ms (ParticleContainer) | 2.0× faster |
| JavaScript at startup | 10.1 kB | 87.8 kB | about 9× smaller |
That's one desktop laptop. On the first phone we tested (an iPhone), the gap held for general sprites and particles, and closed for position-only bulk drawing. Full method and caveats are on the benchmarks page, and the story of how we got there, including the numbers we had to walk back, is in the research write-up: How spryte got 2–3.8× faster than PixiJS.
We also started 2dbench, an open benchmark any 2D web renderer can join. If you have a phone, tablet or laptop that isn't in its community results yet, you can run it in your browser and submit the results in a couple of taps.
Why a separate library
- For debuff games, nothing changes.
@debuffjp/coredepends on spryte and re-exports it, sodraw.sprite()in your scenes works as before, and you get the new features (draw.sprites, render targets, shaders) too. - For everyone else, it's usable on its own. An editor, a visualisation, another engine or a plain canvas app can use spryte without taking a game framework.
- It keeps us honest. A standalone renderer has to stand on its own benchmarks and tests. spryte's fast paths are checked pixel for pixel against its simple path on a real GPU, and it went through an adversarial review that found and fixed six bugs before 0.1.1.
What's next
More devices in the data (especially mid-range Android), more scenes in 2dbench (tilemaps, text, churn), and features when real games need them. Code, docs and issues are at github.com/debuffjp/spryte, and the API reference is on this site.