Skip to content

Quick start

This is a complete, copy-pasteable first frame: acquire a GPU device, build a renderer, push a couple of shapes into a Layer, and draw it in a requestAnimationFrame loop.

import { initGPU, createRenderer, createLayer, hex, rgba } from "insomni";
const canvas = document.querySelector("canvas")!;
// 1. Acquire a GPUDevice (with retry) and a TypeGPU root.
const gpu = await initGPU();
// 2. Build a fully wired renderer bound to the canvas.
const renderer = createRenderer(gpu, canvas, {
background: rgba(0.02, 0.04, 0.07, 1),
});
// 3. Author shapes into a layer. Pushes chain.
const layer = createLayer();
layer
.pushRect({ x: 20, y: 20, width: 120, height: 80, fill: hex("#4f46e5") })
.pushCircle({ cx: 220, cy: 60, radius: 40, fill: rgba(0.9, 0.2, 0.3, 1) });
// 4. Draw one frame per RAF tick.
function frame() {
renderer.render([layer]);
requestAnimationFrame(frame);
}
frame();

initGPU() returns a GPUHandle carrying the adapter, the GPUDevice, and the cached TypeGPU root. It retries adapter acquisition and throws if WebGPU is unavailable. It is async, so it must be awaited (top-level await, or inside an async function).

2. Build the renderer — createRenderer(gpu, canvas, options?)

Section titled “2. Build the renderer — createRenderer(gpu, canvas, options?)”

createRenderer is the batteries-included factory: it assembles the uber-shader, the render pipelines, and (since order-independent transparency is on by default) the A-buffer, then returns a wired Renderer2D. All options are optional — here we set only the clear background. You can also pass an initial camera, dpr, format, and a config block.

The bare new Renderer2D(...) constructor exists too, but it is a dependency-injection seam that requires you to supply pre-built pipelines. Application code should use the factory.

3. Author a layer — createLayer() and push methods

Section titled “3. Author a layer — createLayer() and push methods”

A Layer is the universal drawable: a bag of shapes backed by one UberPack. createLayer() mints an empty one (defaulting to space: "world"). Each pushXxx call appends one instance and returns this, so pushes chain. The real push methods take explicit field names:

layer.pushRect({ x, y, width, height, fill }); // top-left + size
layer.pushCircle({ cx, cy, radius, fill }); // center + radius

Other primitives include pushEllipse, pushSegment / pushLine, pushCurve, pushArc, pushTriangle, pushPolygon, pushPolyline, and pushSprite. Text (pushText / pushString / pushAnchoredString) requires a layer created with an atlas — see Layers & groups. Colors come from helpers like rgba(r, g, b, a) and hex("#rrggbb") (channels are 0–1).

render(layers) draws one whole frame: every layer’s pack is uploaded into a single storage buffer and the merged draw commands run in one render pass (opaque first, then transparent). It draws in array order — render([a, b]) puts b above a. There is no separate build / draw / endFrame step; one render call per frame is the entire loop.

For a fully static scene you could call render once. Wrapping it in requestAnimationFrame is the normal pattern and is what lets the renderer take its cheap damage-tracked partial-redraw path on idle frames.

Read Core concepts to understand layers, packing, spaces, groups, and the frame loop — then dive into the Core renderer section.