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();Walkthrough
Section titled “Walkthrough”1. Acquire a device — initGPU()
Section titled “1. Acquire a device — initGPU()”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 + sizelayer.pushCircle({ cx, cy, radius, fill }); // center + radiusOther 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).
4. Draw — renderer.render([layer])
Section titled “4. Draw — renderer.render([layer])”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.