Skip to content

Layers & groups

A Layer is the universal drawable: a bag of shapes backed by one UberPack. A Group is a lightweight, shareable transform container that shapes (or a whole layer) can be tagged with. Both mirror the authoring surface the rest of insomni is built on.

Preview of the Layers & groups demo
Layers & groups A faint grid Layer draws first, marks draw on top, and a shared Group rotates four rects as one rigid body.
import { createLayer, rgba } from "insomni";
const layer = createLayer({ space: "world", label: "marks" });
layer
.pushRect({ x: 0, y: 0, width: 40, height: 24, fill: rgba(0.2, 0.6, 1, 1) })
.pushCircle({ cx: 60, cy: 12, radius: 8, fill: rgba(1, 0.4, 0.2, 1) });
renderer.render([layer]);

createLayer(options?) mints a Layer over a fresh UberPack. (new Layer(pack, options?) is available if you want to supply your own pack.)

| Option | Type | Default | Notes | | ----------- | ------------------------- | ----------- | --------------------------------------------------------------- | | space | LayerSpace | "world" | Coordinate space: "world" or "ui". | | clip | FrameRect | — | Device-pixel scissor crop for this layer. | | clipRect | FrameRect | — | v1 alias for clip (clip wins when both set). | | atlas | GlyphAtlas | — | MSDF glyph atlas. Required to call any text method. | | group | Group | — | Layer-wide default group (see Groups). | | zIndex | number | undefined | Composite z-band (see Z-order). | | cache | CacheHint | "auto" | Bake policy (see Cache hints). | | label | string | — | Human-readable label for debug tooling. | | visible | boolean | true | A false layer is excluded from drawing. | | debugData | Record<string, unknown> | — | Static metadata surfaced in the debug probe. |

The same fields are exposed as mutable properties on the Layer (plus read-only space, pack, and atlas).

Every push appends one instance and returns this for chaining. Each accepts the kind’s record plus the optional group / emphasisKey tags from GroupedShape.

| Method | Record type | | -------------- | ----------------------------------------------------------------------- | | pushRect | GroupedRectRecord | | pushCircle | GroupedCircleRecord | | pushEllipse | GroupedEllipseRecord | | pushSegment | GroupedSegmentRecord | | pushLine | GroupedSegmentRecord (alias of pushSegment) | | pushCurve | GroupedCurveRecord | | pushArc | GroupedArcRecord | | pushTriangle | GroupedTriangleRecord | | pushPolyline | GroupedPolylineShape (CPU-tessellated into triangles) | | pushPolygon | TriangulatedPolygon \| PolygonShape (earcut-triangulated, hole-aware) | | pushSprite | SpriteShape (textured quad, post-main pass) |

Loop the matching single push, applying a shared options.group to any element that does not already carry its own group:

addRects, addCircles, addEllipses, addLines, addTriangles, addPolygons, addSprites — each (shapes, options?: { group?: Group }).

For hot paths there are pre-packed bulk methods that repack a flat v1-stride buffer and submit one draw command (see Packing): addSegmentsPacked, addCurvesPacked, addArcsPacked — each (data, opts: { count, opaque, group? }).

Require a layer created with { atlas }, or they throw:

| Method | Returns | Notes | | -------------------------------------- | ----------------- | -------------------------------------------------------------------------- | | pushText(shape, flags?) | GlyphMetricsOut | Full shaper (kerning, multi-line, wrap). | | pushString(shape) | this | Fast single-line, no-kerning append. | | pushAnchoredString(shape, flags?) | this | World-anchored, screen-sized; re-projects per frame (a pan never repacks). | | pushStringsBulkInto(batch, options?) | this | Structure-of-arrays bulk label fast path. |

| Member | Notes | | ---------------------- | ------------------------------------------------------------------------------- | | clear() | Reset the pack (and glyph/sprite packs) for the next frame; capacity is reused. | | destroy() | Drop CPU buffers; do not push or render again. | | setClipRect(rect?) | Set/clear the device-pixel crop; returns this. | | shapeCount | Packed rect/circle/ellipse instance count. | | triangleCount | Packed triangle instance count (polygons + polylines). | | effectiveLocalBounds | Union world AABB of all packed shapes + glyphs, or null. | | pack | The underlying UberPack. | | glyphPack | This layer’s glyph instances, or null. |

A layer is a flat z-band — layer N is entirely above layer N−1 (painter’s order), with no per-shape interleave across layers.

  • zIndex sorts ascending (lower = drawn first = below). undefined keeps array insertion order and sorts above every explicit band. Equal keys preserve insertion order (the sort is stable).
  • A pure zIndex change between frames is a composite-only invalidation; the renderer folds the z-order into its view fingerprint, so a reorder demotes a partial frame to a clean full repaint instead of ghosting.
  • visible: false excludes the layer entirely; flipping it forces a full repaint next frame so the hidden layer’s pixels do not linger.

To interleave shapes from two “layers,” put them in the same layer — intra- layer overlap resolves via transparency and depth.

CacheHint controls whether the renderer backs a layer with a cached RTT snapshot:

| Hint | Behavior | | ------------------ | ------------------------------------------------------------------------------------------------------ | | "auto" (default) | A cheap static heuristic (instanceCount + glyphs × 8) bakes expensive layers, keeps cheap ones live. | | "always" | Always bake; re-bake only when content or view changes. Pinned (exempt from budget eviction). | | "never" | Always rasterize live. |

The hint is subordinate to z-soundness: a bake can only composite under all live geometry or over it, so a layer whose bake would sit between two live layers stays live even under "always". Caching accelerates the bottom and top of the z-stack. The renderer applies the hint each frame; the manual escape hatches are cacheLayer / uncacheLayer.

Every push record extends GroupedShape, which adds two optional tags:

| Field | Type | Notes | | ------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | group | Group | The shape’s transform group (see below). A per-push group overrides the layer’s default group. | | emphasisKey | number | Per-instance emphasis key. 0 (default) is the exempt sentinel — never dimmed. A key ≥ 1 opts into setEmphasis dimming and forces the transparent path. |

The exported grouped records are GroupedRectRecord, GroupedCircleRecord, GroupedEllipseRecord, GroupedSegmentRecord, GroupedCurveRecord, GroupedArcRecord, GroupedTriangleRecord, GroupedTextShape, and GroupedAnchoredStringShape.

A Group is a value object holding one Mat3 transform and an optional parent. Groups chain: a child’s effective transform is the compounded product of every ancestor’s transform, root-to-leaf.

import { createGroup, resolveGroupTransform, translation, scaling } from "insomni";
const root = createGroup({ transform: translation(100, 50) });
const child = createGroup({ transform: scaling(2, 2), parent: root });
// Tag shapes with the group — geometry transforms live, no repack on a move.
layer.pushRect({ x: 0, y: 0, width: 10, height: 10, fill, group: child });
// Mutating the transform between frames takes effect without re-packing:
root.transform = translation(120, 50);
resolveGroupTransform(child); // => multiply(root.transform, child.transform)

| Member | Type | Notes | | ----------- | --------------- | ----------------------------------------------------------------------------------------- | | transform | Mat3 | Default IDENTITY. Mutable — update between frames; takes effect without re-packing. | | parent | Group \| null | Default null. Immutable after creation; transforms compound up the chain. |

createGroup(options?) mints a fresh group.

Walks the parent chain and returns the compounded transform (root applied first, leaf last); returns IDENTITY for a null group.

A Group is CPU-side draw metadata — it is never packed into the instance bytes (no repack on a group move). Instead the renderer composes the group’s resolved transform into the layer’s space view-projection and routes the command to a dedicated per-(space × group) camera slot. A group-identity change is a DrawCommand merge boundary, so two shapes in different groups never coalesce into one draw (they need different slots). This is why mutating group.transform between frames moves the geometry live with no re-packing cost.