Guide: building a game/sim with insomni
insomni isn’t a game engine — there’s no scene graph, no entity system, no
asset pipeline. But the pieces a 2D game or simulation loop reaches for over
and over (camera + input, sprite atlases, draw-order-by-Y, shader effects,
particles, a minimap, deterministic screenshot tests) all exist in the core
renderer or its siblings. This guide is a map from “I’m building a
top-down/isometric game or a live sim” to the real API and the example page
that exercises it.
Camera binding
Section titled “Camera binding”A viewport owns the camera that maps world coordinates to screen pixels.
bindCamera(renderer, canvas, opts)
is the batteries-included one-call path: it creates a CameraViewport sized to
the canvas, wires pointer drag/wheel zoom/pinch input, subscribes camera
updates to renderer.setCamera, and re-clamps zoom on DPR/resize changes. See
the Camera binding recipe for the full
{ viewport, binding, destroy } contract and minZoom/maxZoom as
number | () => number.
For full control over the wiring — or to see exactly what bindCamera wraps —
createViewport is the underlying imperative
state container (panBy/zoomAt/jumpTo) and
bindViewport wires the input by hand. See
Cameras & viewport and the
pan-zoom example, which still uses the manual
createViewport + bindViewport + renderer.setCamera(viewport.camera) form.
Sprites and atlases
Section titled “Sprites and atlases”Layer.pushSprite({ pos, size, texture, tint?, rotation?, anchor?, opaque? })
draws one textured, tinted quad. texture accepts either a whole Texture or
a TextureRegion — a sub-rect of a shared atlas, via texture.region(x, y, w, h),
texture.regionUV(...), or the SpriteAtlas / GridAtlas helpers — so a
single loaded PNG can back many sprites without separate texture binds. See
Primitives → Sprites and textures
and → Texture regions and atlases
for the full field table and GridAtlas/SpriteAtlas API, the
sprite-atlas example for a GridAtlas over a
real 20x15 item-icon sheet driving 300 pushSprite calls off one shared
texture bind, and the push-custom example (via
layer.pushCustom/pushSprite) for a layer
that pushes sprites at specific submission points.
Y-depth (draw order by Y)
Section titled “Y-depth (draw order by Y)”createLayer({ sort: "y", sortBias? }) depth-sorts a layer’s sprites by anchor
pos.y + sortBias (ascending — smaller y drawn first/further back) instead of
push order, so lower-on-screen sprites draw over higher ones — the classic
top-down/isometric trick. It only reorders that layer’s sprites; shapes and
glyphs in the same layer still draw in push order, and layers themselves are
still flat z-bands drawn in push/array order.
sortBias is an additive nudge on the ranking key, useful for pinning one
sprite consistently above/below others at the same y (a shadow, a held
item). See the y-sort example: the identical set of
orbiting sprite tokens, pushed in the identical order, split-screen —
push-order occlusion glitches on the left as tokens cross paths, sort:"y"
stays correct on the right. See Layers & groups for
how push order and zIndex resolve outside of sort:"y".
Effects
Section titled “Effects”createTextureEffect(renderer, { texture, rect, fragment }) wraps a textured
quad with a custom WGSL fragment shader — glow, outline, distortion — and
returns a CustomDrawable you drop straight into render([...]). See
Custom shaders and the
sprite-shine example, which layers two
different createTextureEffect shaders (an animated sweep and a static
outline) over one loaded PNG.
Particles
Section titled “Particles”The insomni/particles entry point (ParticleSystem, see
packages/insomni/src/particles/system.ts) is a GPU compute-driven,
fixed-capacity particle pool with force kernels (gravity, drag, noise, flow
fields, boids). See the particles example: a
pointer-following ember fountain wiring up gravity/drag/noise forces and
createV3Drawable end to end.
The flow-field example is a different,
CPU-driven pattern worth knowing too — thousands of particles advected through
a vector field by pushing plain segments each frame, no GPU compute involved.
Use it for the interaction pattern (pointer-repelled field, per-frame layer
rebuild) while the CPU cost of updating a few thousand instances per frame is
a non-issue; reach for insomni/particles once that cost becomes the
bottleneck.
Minimap
Section titled “Minimap”createNavigator(options) builds a
minimap/overview with a draggable indicator rectangle tracking the live
camera. See Specialized layers → Navigator
and the minimap example, which mirrors a full
scene at small scale and keeps the indicator in sync as you pan and zoom.
Headless goldens
Section titled “Headless goldens”insomni-node runs the core renderer headless on
Dawn/WebGPU — no browser, no canvas — and reads a frame back as raw pixels or
a PNG. That’s the basis for deterministic golden-image tests: build a scene
with the ordinary insomni API (createLayer, pushRect, etc.), call
await api.frame([scene]), then await api.toPNG() and diff it against a
checked-in reference image. See packages/insomni-node/examples/smoke.mjs
for the full working script (render two rounded rects, write a PNG) and the
insomni-node package page for setup/platform notes.