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.

Preview of the Coordinate spaces demo
Coordinate spaces A world-space scene drifts under the camera while a space:"ui" overlay stays pinned to the canvas.
type LayerSpace = "world" | "ui";

| Space | Coordinates | Camera applied? | | ------------------- | ----------- | ---------------------------------------------------------- | | "world" (default) | World units | Yes — the live camera (pan / zoom / rotate). | | "ui" | CSS pixels | No — 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:

| Slot | Space | | ---- | ------- | | 0 | world | | 1 | ui |

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 }.

| Field | Default | Notes | | ---------- | ------- | ----------------------------------------------- | | x / y | 0 | World coordinate shown at the viewport center. | | zoom | 1 | Zoom around the viewport center. | | rotation | 0 | Rotation 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.

| Helper | Signature | Returns | | ---------------------------------------------------- | ----------------------- | ------------------------------------------------------------------------------------------------- | | viewFromCamera(cam, viewportWidth, viewportHeight) | camera + viewport | Mat3 mapping world → CSS-pixel screen, anchored at the viewport center. | | viewFromCameraInRect(cam, rx, ry, rw, rh) | camera + arbitrary rect | Mat3 mapping world → screen within that rect (world (cam.x, cam.y) lands at the rect center). | | cameraViewportRect(viewport) | CameraViewport | The 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:

| Field | Type | Notes | | ---------------------------------- | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | | camera | ResolvedCameraState | Read only for space: "world". | | group | Group \| null | Compounded transform applied before the camera (world only). | | dpr | number | Read only by the world branch: device dims are recovered as viewportWidth/Height × dpr to build the camera projection over the device-px baseline. | | viewportWidth / viewportHeight | number | Viewport 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.