Camera binding recipe
Two recipes for the common “camera drives both the canvas and some HTML” setup: a one-call canvas↔camera↔renderer wire-up, and projecting a DOM element onto the world using the renderer’s live view matrix.
The pan-zoom example is a live demo of the pan/zoom
camera behavior both recipes on this page drive (it wires the underlying
createViewport/bindViewport pair by hand — see “Without bindCamera” below —
but the resulting camera behavior is exactly what bindCamera gives you in one
call).
bindCamera — canvas → viewport → renderer in one call
Section titled “bindCamera — canvas → viewport → renderer in one call”bindCamera(renderer, canvas, opts) creates a CameraViewport sized to the
canvas, binds pan/zoom/pinch input to it via
bindViewport, and subscribes the viewport’s
onChange to push the resolved camera into renderer.setCamera. It also
attaches a ResizeObserver/DPR-change hook that re-clamps zoom and resizes the
viewport frame, so a monitor swap or window drag between displays doesn’t
leave a stale zoom clamp.
import { createRenderer, initGPU } from "insomni";import { bindCamera } from "insomni";
const gpu = await initGPU();const renderer = createRenderer(gpu, canvas);
const { viewport, binding, destroy } = bindCamera(renderer, canvas, { minZoom: 0.25, maxZoom: 20,});
function frame(now: number) { binding.update(dt); // step damping/fling — same contract as bindViewport renderer.render(layers); requestAnimationFrame(frame);}
// teardowndestroy(); // unwires the ResizeObserver + the underlying bindViewport bindingbindCamera returns { viewport, binding, destroy }:
viewport— theCameraViewportthe renderer’s camera is now driven from (same object you’d get fromcreateViewport).binding— theCameraViewportBindingfrom the underlyingbindViewportcall —update(dt),setTarget,jumpTo,screenToWorld/worldToScreen, etc. all work as documented on Cameras & viewport.destroy()— tears down both the resize/DPR hook and the input binding.
opts is BindViewportOptions (see
the field table), plus minZoom /
maxZoom may be a function (() => number) instead of a fixed number —
useful when the sensible zoom floor depends on live content bounds (e.g. “never
zoom out past fit-to-content”). The resolved bound is re-read on every clamp,
including the DPR-change re-clamp bindCamera wires up automatically.
Without bindCamera
Section titled “Without bindCamera”The same wiring, spelled out, if you need to customize a step bindCamera
doesn’t expose yet:
import { createViewport, viewportFrame } from "insomni/viewport";import { bindViewport } from "insomni";
const viewport = createViewport({ frame: viewportFrame(canvas.width, canvas.height),});
const binding = bindViewport(viewport, canvas, { minZoom: 0.25, maxZoom: 20 });
viewport.onChange(() => renderer.setCamera(viewport.camera));DOM overlay via the world→CSS view matrix
Section titled “DOM overlay via the world→CSS view matrix”To pin an HTML element (a tooltip, a label, a selection handle) to a world position as the camera pans/zooms, project the point through the renderer’s live view matrix rather than re-deriving camera math in the overlay code.
renderer.getViewMatrix() returns a Mat3: the world→CSS-pixel view matrix
for the renderer’s current camera (CSS px, not device px — already
DPR-normalized). Apply it with transformPoint and place the overlay with a
CSS transform:
import { transformPoint } from "insomni";
function syncOverlay(worldX: number, worldY: number, el: HTMLElement) { const view = renderer.getViewMatrix(); const { x, y } = transformPoint(view, worldX, worldY); el.style.transform = `translate(${x}px, ${y}px)`;}
function frame() { renderer.render(layers); syncOverlay(marker.x, marker.y, markerEl); requestAnimationFrame(frame);}Position markerEl with position: absolute; left: 0; top: 0; inside a
container that shares the canvas’s CSS box, then let the transform do the
placement — translate() avoids layout thrash on every frame. If you already
have a CameraViewportBinding from bindCamera/bindViewport, its
worldToScreen(wx, wy) does the same projection using the binding’s
interpolated camera (mid-smoothing), which is preferable while the camera is
still damping toward a target — getViewMatrix() reflects whatever camera
state was last pushed to the renderer, so use whichever one matches what’s
actually on screen for your setup.
See also
Section titled “See also”- Cameras & viewport —
createViewport,bindViewport, frames, pan bounds. - Spaces & cameras — world / ui / device coordinate spaces.
plans/game-sim-wishlist/phase-3-camera-unification.md— the frozenbindCameradesign this page documents.