Cameras & viewport
A viewport owns the camera that maps world coordinates to screen pixels. It
holds a Frame (the pixel-space rect it draws into) plus a camera
state (x, y, zoom, rotation) and exposes pan / zoom / coordinate-conversion
methods. The renderer reads a viewport’s resolved camera each frame to build its
view-projection matrix; interaction nodes use it to transform pointer events into
world space.
createViewport tracks the camera and bindViewport wires pointer gestures to it. There are two layers to the API:
createViewportis the imperative state container. CallpanBy/zoomAt/setCameradirectly and the camera jumps instantly. This is what the renderer and hit-tests read.bindViewportwraps a viewport with input handling — pointer drag, wheel zoom, two-finger pinch, momentum fling, and critically-damped smoothing. It drives the underlying viewport for you.
createViewport
Section titled “createViewport”import { createViewport } from "insomni/viewport";import { viewportFrame } from "insomni";
const viewport = createViewport({ frame: viewportFrame(canvas.width, canvas.height), camera: { x: 0, y: 0, zoom: 1 },});CameraViewportOptions
Section titled “CameraViewportOptions”| Field | Type | Notes |
| ----------- | ------------------------ | --------------------------------------------------------------------------- |
| frame | Frame | Pixel-space rect. Interpreted relative to parent if given. |
| parent | ViewportLike | Optional parent; frame is then in the parent’s space. |
| camera | CameraState | Initial camera. Missing fields default to identity (0 / 1). |
| panBounds | CameraPanBoundsOptions | Constrain panning to keep content in view. See Pan bounds. |
CameraState is { x?, y?, zoom?, rotation? } — x / y are the world point
shown at the viewport center, zoom scales around the center, rotation is in
radians. The resolved (all-fields-present) form is ResolvedCameraState.
CameraViewport
Section titled “CameraViewport”createViewport returns a CameraViewport:
| Member | Description |
| ------------------------------------ | ----------------------------------------------------------------- |
| mode | Always "camera". |
| frame | Frame relative to parent (or absolute if no parent). |
| absoluteFrame | Frame in root/canvas space — walks the parent chain on each read. |
| clipRect | Mutable FrameRect equal to absoluteFrame, refreshed in place. |
| camera | Snapshot of the resolved camera state. |
| panBy(dxPx, dyPx) | Pan by pixel deltas in the viewport’s frame space. |
| zoomAt(anchorSx, anchorSy, factor) | Zoom around a screen-pixel anchor (absolute frame space). |
| reset() | Restore the initial camera. |
| setFrame(frame) | Replace the frame (same space as the original). |
| setCamera(state) | Set the camera; omitted fields keep their current value. |
| jumpCamera(state) | Alias for setCamera. |
| screenToWorld(sx, sy) | Screen pixel → world Vec2. Rotation is not applied. |
| worldToScreen(wx, wy) | World → screen pixel Vec2. |
| setPanBounds(options \| null) | Replace or clear the pan-bound config. |
| onChange(cb) | Subscribe to any view-state change; returns an unsubscribe fn. |
panBy drags the content: moving the pointer right shifts the world-view left.
zoomAt is uniform — a ZoomFactor of { x, y } with mismatched axes collapses
to the geometric mean.
// Pan 40px right, then zoom 2× around the canvas center.viewport.panBy(40, 0);viewport.zoomAt(canvas.width / 2, canvas.height / 2, 2);
// Convert a click to world space.const world = viewport.screenToWorld(event.offsetX * dpr, event.offsetY * dpr);ZoomFactor
Section titled “ZoomFactor”type ZoomFactor = number | { x?: number; y?: number };A bare number is a uniform multiplicative factor (>1 zooms in). The per-axis
object form exists for symmetry with pinch gestures, but camera zoom is uniform:
mismatched x / y collapse to sqrt(x * y).
Pan bounds
Section titled “Pan bounds”panBounds keeps a world-space content rect in (or near) the viewport. When
zoomed in, panning stops at the content edges plus a margin; when zoomed out far
enough that content fits, the content can’t leave the viewport. Zoom itself is
never restricted.
CameraPanBoundsOptions
Section titled “CameraPanBoundsOptions”| Field | Type | Description |
| ----------- | ----------------- | ------------------------------------------------------------------------- |
| content | FrameRect | World-space rect to keep in view. Required — there’s no implicit content. |
| overshoot | PanBoundsMargin | Pan-past-content allowance when content is larger than the viewport. |
| drift | PanBoundsMargin | Content drift allowance when content fits inside the viewport. |
type PanBoundsMargin = number | { x?: number; y?: number };A PanBoundsMargin is a fraction of the visible span, applied symmetrically
to both edges. A bare number applies to both axes; the object form sets them per
axis. PanBoundsMargins is the { overshoot?, drift? } pair that both
CameraPanBoundsOptions and setPanBounds accept.
const viewport = createViewport({ frame: viewportFrame(w, h), panBounds: { content: { x: 0, y: 0, width: 1000, height: 600 }, overshoot: 0.1, // allow 10% rubber-band past the edges when zoomed in drift: 0, // pin content centered when it fits },});
// Swap the lock later (e.g. after loading new data):viewport.setPanBounds({ content: newBounds });viewport.setPanBounds(null); // remove the lock entirelyworldCullBounds
Section titled “worldCullBounds”function worldCullBounds(viewport: CameraViewport, rect?: FrameRect): FrameRect;Returns the world-space AABB visible inside the viewport’s frame (or a sub-rect of
it). Useful for hit-testing and manual culling — Renderer2D already applies
render-time culling automatically. Rotation is ignored, so the returned rect is a
conservative over-approximation when the camera is rotated.
const visible = worldCullBounds(viewport);const items = quadtree.query(visible); // only build layers for what's on screenbindViewport
Section titled “bindViewport”bindViewport(viewport, element, options) attaches pointer / wheel / pinch input
to a DOM element and returns a CameraViewportBinding. It manages an internal
smoothDamp per camera axis plus a velocity
tracker for release flings — you call update(dt) once per frame to advance the
animation.
import { bindViewport } from "insomni";
const binding = bindViewport(viewport, canvas, { minZoom: 0.25, maxZoom: 20, smoothTime: 0.1,});
function frame(now: number) { const dt = (now - last) / 1000; last = now; binding.update(dt); // step damping + flings, push to the viewport renderer.render(layers); requestAnimationFrame(frame);}
// laterbinding.destroy();BindViewportOptions
Section titled “BindViewportOptions”| Field | Type | Default | Description |
| ------------ | -------------------------- | ------- | ------------------------------------------------------------ |
| drag | boolean | true | Pointer drag pan. |
| wheel | boolean | true | Mouse-wheel zoom. |
| pinch | boolean | true | Two-finger pinch (touch). |
| smoothTime | number | 0.1 | SmoothDamp time constant (seconds). 0 = instant. |
| fling | boolean \| FlingOptions | on | Drag-release momentum. false disables. |
| minZoom | number | 0.1 | Minimum camera zoom. |
| maxZoom | number | 10 | Maximum camera zoom. |
| initial | CameraState | — | Initial camera written to the viewport on bind. |
| onZoom | (g: ZoomGesture) => void | — | Custom wheel/pinch handler; see ZoomGesture. |
FlingOptions
Section titled “FlingOptions”| Field | Type | Default | Description |
| --------------- | -------- | ------- | ------------------------------------------------------------------ |
| friction | number | 4 | Exponential decay rate (1/s). Travel ≈ velocity / friction. |
| minVelocity | number | 50 | Release speed (px/s) needed to start a fling. |
| windowSeconds | number | 0.08 | Sample window the velocity tracker uses to estimate release speed. |
CameraViewportBinding
Section titled “CameraViewportBinding”| Member | Description |
| --------------------------- | --------------------------------------------------------------------- |
| mode | Always "camera". |
| interacting | true while a drag / pinch is in progress. |
| flinging | true while a release fling is coasting. |
| animating | true while any damper is still converging (e.g. after setTarget). |
| enabled | Read/write — toggles the underlying interaction node. |
| camera | Interpolated camera currently applied (ResolvedCameraState). |
| target | The state the damping is moving toward. |
| setTarget(state) | Set the damp target; omitted fields keep their current target. |
| jumpTo(state) | Jump immediately, skipping interpolation. Cancels any fling. |
| update(dt) | Step damping/fling and push to the viewport. Call per frame. |
| stopAnimation() | Cancel pending smoothing and fling. |
| resetToInitial() | Jump back to the first target captured after binding. |
| screenToWorld(cssX, cssY) | Element-local CSS px → world Vec2, using the interpolated camera. |
| worldToScreen(wx, wy) | World → element-local CSS px Vec2. |
| destroy() | Detach all listeners and tear down the interaction node. |
ViewportBinding is an alias for CameraViewportBinding.
ZoomGesture
Section titled “ZoomGesture”onZoom fires before the binding’s default uniform zoom, for both wheel and
pinch. Call preventDefault() to suppress the default — useful when the scene
wants to reinterpret the gesture (e.g. adjusting row spacing in a phylogram
instead of scaling). For pinch, the pan (translation) component is still applied
even when zoom is prevented.
| Field | Type | Description |
| ------------------ | -------------------- | ------------------------------------------------------ |
| source | "wheel" \| "pinch" | Which input produced the gesture. |
| scale | number | Multiplicative factor for this tick (>1 zoom in). |
| anchorX | number | Anchor in element-local CSS px (point that stays put). |
| anchorY | number | — |
| preventDefault() | () => void | Suppress the default uniform-camera zoom. |
bindViewport(viewport, canvas, { onZoom(g) { if (g.source === "wheel" && altKeyHeld) { g.preventDefault(); // take over the gesture adjustRowSpacing(g.scale); } },});Frames
Section titled “Frames”A Frame is an immutable { x, y, width, height } rect with corner / center
accessors and non-mutating transform helpers. Viewports take a Frame; many
layout helpers return one.
import { createFrame, viewportFrame } from "insomni";
const root = viewportFrame(800, 600); // { x:0, y:0, width:800, height:600 }const plot = root.padded({ left: 48, bottom: 32 }); // inset on two sidesconst inner = createFrame({ x: 10, y: 10, width: 100, height: 100 });Frame members
Section titled “Frame members”| Member | Description |
| ----------------------------------------------------- | ----------------------------------------------------- |
| x / y / width / height | The rect. width / height are floored at 0. |
| topLeft / topRight / bottomLeft / bottomRight | Corner Vec2s. |
| center | Geometric center Vec2. |
| rect | Plain FrameRect object (e.g. for layer.pushRect). |
| padded(padding) | Nested frame inset on each side. |
| translated(dx, dy) | Frame moved without resizing. |
| resized(width, height) | Same origin, new size. |
Padding accepts a number (all sides), { all?, top?, right?, bottom?, left? },
or { ratio } (a fraction of the shorter side). createFrame(rect) wraps a plain
rect; viewportFrame(width, height) is the (0, 0)-origin convenience for the
outermost container. ZERO_RECT is the shared empty FrameRect.
See also
Section titled “See also”- Spaces & cameras — world / ui / device coordinate spaces.
- Interactions — the interaction manager
bindViewportis built on. - Renderer — how a viewport’s camera feeds the render pipeline.