Skip to content

Spaces & cameras

Each Layer declares a coordinate space that decides how its geometry is transformed to the screen. The renderer keeps one camera slot per space, and groups add extra slots on demand.

Coordinate spaces A world-space scene drifts under the camera while a space:"ui" overlay stays pinned to the canvas.
type LayerSpace = "world" | "ui";
SpaceCoordinatesCamera applied?
"world" (default)World unitsYes — the live camera (pan / zoom / rotate).
"ui"CSS pixelsNo — maps CSS px directly to NDC (overlays, text, chrome).

A layer’s space selects which view-projection its draws use. world content moves with the camera; ui content is screen-fixed (and is never frustum-culled).

The renderer packs one Camera uniform per space into a single GPU buffer at aligned offsets, with one bind group per slot. The two base slots are fixed:

SlotSpace
0world
1ui

Slots beyond the base two are a growable pool reused frame-to-frame for the distinct groups present that frame. A group-tagged draw routes to a dedicated per-(space × group) slot whose uploaded matrix is spaceVP ∘ resolveGroupTransform(group) — so a group transform applies live with no repack. Because each group needs its own slot, a group-identity change is a DrawCommand merge boundary.

CameraState is the live camera the renderer transforms world content through; all fields are optional and default to { x: 0, y: 0, zoom: 1, rotation: 0 }.

FieldDefaultNotes
x / y0World coordinate shown at the viewport center.
zoom1Zoom around the viewport center.
rotation0Rotation in radians around the viewport center.

cameraStateForBounds(bounds, viewportWidth, viewportHeight, options?) computes the camera that fits a Bounds2D into the viewport.

import { cameraStateForBounds } from "insomni";
const cam = cameraStateForBounds(
{ minX: 0, minY: 0, maxX: 800, maxY: 600 },
canvas.clientWidth,
canvas.clientHeight,
{ padding: 0.9 }, // leave 10% slack; 1 fills the viewport
);
renderer.setCamera(cam);

Bounds2D is { minX, minY, maxX, maxY }. FitCameraToBoundsOptions carries the optional padding factor in (0, 1]. The function throws on non-finite or inverted bounds.

HelperSignatureReturns
viewFromCamera(cam, viewportWidth, viewportHeight)camera + viewportMat3 mapping world → CSS-pixel screen, anchored at the viewport center.
viewFromCameraInRect(cam, rx, ry, rw, rh)camera + arbitrary rectMat3 mapping world → screen within that rect (world (cam.x, cam.y) lands at the rect center).
cameraViewportRect(viewport)CameraViewportThe viewport’s absoluteFrame as a FrameRect.

viewFromCamera is the special case of viewFromCameraInRect covering the full target. The renderer uploads exactly this matrix to the world camera slot, and the damage project hook below reuses it so projection stays byte-consistent.

project converts an authoring-space AABB into a CSS-pixel FrameRect for use as a damage / scissor region. It is the bridge between a changed layer’s bounds and the damage system.

import { project } from "insomni";
const rect = project(minX, minY, maxX, maxY, "world", {
camera: { x: 0, y: 0, zoom: 1, rotation: 0 },
group: null,
dpr: window.devicePixelRatio,
viewportWidth: canvas.clientWidth,
viewportHeight: canvas.clientHeight,
});

ProjectOptions:

FieldTypeNotes
cameraResolvedCameraStateRead only for space: "world".
groupGroup | nullCompounded transform applied before the camera (world only).
dprnumberRead only by the world branch: device dims are recovered as viewportWidth/Height × dpr to build the camera projection over the device-px baseline.
viewportWidth / viewportHeightnumberViewport extent in CSS px.

Per-space behavior:

  • "ui" — returned verbatim in CSS px (camera + dpr ignored).
  • "world" — resolve the group transform, transform all four AABB corners through it and the camera view (world → CSS px via the device-px baseline), take the screen-space min/max (correct for any rotation).

viewportAabb(viewport, rect?) returns the world-space AABB of a CameraViewport (the world rectangle currently visible), inflated by an AA epsilon to absorb SDF edge falloff. Pass a rect to query a sub-rect of the viewport; omit it for the full viewport.

import { viewportAabb } from "insomni";
const visibleWorld = viewportAabb(viewport); // { minX, minY, maxX, maxY }

Rotation is ignored — under a rotated camera the result is a conservative over-approximation of what is on screen.