insomni
v0.2.0-alpha.2
Published
A WebGPU rendering engine.
Readme
insomni
insomni is an opinionated 2D WebGPU renderer. It is optimized for data-heavy, interactive scenes — geometry is packed CPU-side into flat float arrays and uploaded to the GPU each frame. SDF-based primitives cover the common cases; complex polygons fall back to earcut triangulation.
Alpha. The core rendering loop is stable but some APIs carry known limitations (see Alpha limitations below).
Installation
npm install insomni
# or
pnpm add insomniWebGPU must be available in the environment. Chrome 113+ and Safari 18+ ship it behind no flag.
Quick start
import { initGPU, createRenderer, createLayer, rgba } from "insomni";
// 1. Acquire the GPU device.
const gpu = await initGPU();
// 2. Build a renderer wired to a canvas element.
const canvas = document.querySelector("canvas") as HTMLCanvasElement;
const renderer = createRenderer(gpu, canvas);
renderer.setBackground(rgba(0.05, 0.06, 0.09, 1));
// 3. Create a layer and push some primitives.
const scene = createLayer(); // "world" space by default
scene.pushRect({
x: 40,
y: 36,
width: 240,
height: 128,
fill: rgba(0.13, 0.15, 0.2, 1),
cornerRadius: 16,
});
scene.pushCircle({ cx: 160, cy: 100, radius: 24, fill: rgba(0.36, 0.7, 1, 1) });
scene.pushLine({ x1: 40, y1: 170, x2: 280, y2: 170, color: rgba(1, 1, 1, 0.2), width: 1 });
// 4. Render each frame.
function frame(now: DOMHighResTimeStamp) {
renderer.render([scene]);
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
// 5. Handle resize.
window.addEventListener("resize", () => {
const dpr = window.devicePixelRatio;
renderer.setDpr(dpr);
renderer.resize(canvas.clientWidth, canvas.clientHeight);
});
// 6. Tear down.
function dispose() {
renderer.destroy();
gpu.destroy();
}Coordinate spaces
Every Layer lives in one of two coordinate spaces, set once at construction:
| Space | Meaning |
| ------------------- | ----------------------------------------------------------------------------------------------------------------- |
| "world" (default) | Camera-transformed world coordinates. Pan, zoom, and fitCameraToBounds all apply. |
| "ui" | CSS pixel coordinates fixed to the canvas. Unaffected by the camera. Useful for HUD overlays, axes, and tooltips. |
const hud = createLayer({ space: "ui" });
hud.pushRect({ x: 8, y: 8, width: 120, height: 32, fill: rgba(0, 0, 0, 0.6), cornerRadius: 6 });Layer and Group model
Layer is the universal drawable. It owns a CPU-side pack of shape commands and an optional coordinate space. You create one with createLayer(), populate it with push* calls, and hand it to renderer.render([...layers]). Layer order in the array sets draw order — earlier layers are painted first.
const background = createLayer();
const foreground = createLayer();
// background is drawn first, foreground on top:
renderer.render([background, foreground]);Group is a lightweight transform container. Any primitive whose group field references a Group instance is transformed by that group's transform matrix at draw time — no CPU repacking needed. Mutating group.transform and re-rendering is enough to move, rotate, or scale an entire cluster of shapes as a rigid body.
import { createGroup, rotation } from "insomni";
const orbit = createGroup();
orbit.transform = rotation(Date.now() * 0.001);
layer.pushRect({ x: -20, y: -20, width: 40, height: 40, fill: rgba(1, 0.5, 0), group: orbit });
layer.pushCircle({ cx: 80, cy: 0, radius: 10, fill: rgba(0, 0.8, 1), group: orbit });Dynamic vs. static (cached) layers
By default every layer is dynamic: on each frame the CPU pack is re-uploaded to GPU. This is fine for most scenes.
For large, unchanging geometry (grids, axis decorations, terrain) you can bake a layer to a cached RTT texture:
// Bake the layer once — runs an MSAA render pass and freezes it into a texture.
renderer.cacheLayer(grid);
// Every subsequent render() composites the baked texture instead of re-drawing the geometry.
renderer.render([grid, dynamic]);
// Must be called before re-baking with new content.
renderer.uncacheLayer(grid);
grid.clear();
// ... rebuild grid ...
renderer.cacheLayer(grid);cacheLayer requires createRenderer (not a bare new Renderer2D). The bake freezes the world-space camera; a subsequent pan or zoom will NOT re-bake automatically — call uncacheLayer + cacheLayer to refresh.
OIT — Order-independent transparency
OIT is on by default. Text (MSDF glyphs) and transparent sprites z-interleave with shapes correctly regardless of draw order. The A-buffer composites depth-sorted fragments per pixel at the end of each frame.
When OIT is off (config: { oit: false }), glyphs and sprites fall back to a painter-order depth stamp — they will not float unconditionally on top, but they will not depth-sort against transparent geometry either.
You can pass a partial RendererConfig to createRenderer:
const renderer = createRenderer(gpu, canvas, {
config: { oit: false }, // disable OIT (saves ~16 MiB of A-buffer memory)
});Primitive push methods
All methods live on Layer and return this for chaining (except pushText which returns glyph metrics):
| Method | Shape |
| --------------------------- | ------------------------------------------- |
| pushRect(shape) | Axis-aligned rounded rectangle |
| pushCircle(shape) | Circle |
| pushEllipse(shape) | Ellipse |
| pushLine(shape) | Line segment (alias for pushSegment) |
| pushSegment(shape) | Line segment |
| pushCurve(shape) | Cubic Bézier curve |
| pushArc(shape) | Arc |
| pushTriangle(shape) | Single triangle |
| pushPolygon(shape) | Polygon (triangulated via earcut) |
| pushStroke(shape) | Analytic SDF stroke path (round joins only) |
| pushSprite(sprite) | Textured sprite (atlas region) |
| pushText(shape) | MSDF glyph text |
| pushAnchoredString(shape) | World-anchored, screen-sized text |
Call layer.clear() to reset the pack before rebuilding each frame.
Camera and resize
// Fit the world camera to a bounding box each resize:
renderer.fitCameraToBounds({ minX: 0, minY: 0, maxX: 800, maxY: 600 }, { padding: 0.9 });
// Manual HiDPI setup:
renderer.setDpr(window.devicePixelRatio);
renderer.resize(canvas.clientWidth, canvas.clientHeight);Frame timing
Pass onFrameTiming in the renderer config to receive per-frame CPU/GPU timing info:
const renderer = createRenderer(gpu, canvas, {
config: {
onFrameTiming: (t) => console.log("frame", t),
},
});Device loss
initGPU exposes two hooks for GPU error handling:
const gpu = await initGPU({
onDeviceLost: (info) => {
// Notified when device is lost unexpectedly (GPU reset, driver crash, TDR).
// insomni does NOT auto-recreate. Tear down your renderer and call initGPU again.
console.error("GPU lost:", info.message);
},
onUncapturedError: (err) => {
// Notified for each uncaptured GPU error (validation, out-of-memory).
console.error("GPU error:", err);
},
logger: myLogger, // optional; defaults to console
});SVG export
The SVG backend consumes the same Layer[] as render():
import { createSVGRenderer } from "insomni";
const svg = createSVGRenderer({ width: renderer.width, height: renderer.height });
svg.setCamera(renderer.getCamera());
svg.setBackground(bg);
svg.render([scene]);
document.body.appendChild(svg.element());Alpha limitations
- Device-loss recovery is notify-only.
onDeviceLostfires but insomni does not auto-recreate the device or rebuild GPU resources. Callers must tear down and callinitGPUagain. - OIT fragment budget is fixed at K=8 per pixel (configurable up to K=16 via
config.oitFragmentsPerPixel). Dense overlapping transparent layers will drop the deepest fragments. - Clipping is rectangular scissor only. There is no stencil-based or path-based clip.
- SDF stroke joins are round only. Passing
join: "miter"orjoin: "bevel"topushStrokefalls back to tessellated polylines. cacheLayerfreezes the world-space camera. A camera move after baking does not re-bake automatically.
Subpath exports
| Import path | Contents |
| -------------------- | ------------------------------------------------------ |
| insomni | Core renderer, Layer, Group, math, interactions |
| insomni/text-ttf | TTF/MSDF text with loadFont() (pulls in opentype.js) |
| insomni/viewport | createViewport, CameraViewport, pan-bound types |
| insomni/internal | Render-debug probes (unstable) |
| insomni/reactivity | Reactive primitives (unstable) |
| insomni/spatial | Spatial indexing helpers (unstable) |
| insomni/particles | Particle system (unstable) |
