Skip to content

Drawing primitives

Everything you draw with insomni goes into a Layer. A layer exposes a pushXxx / addXxx method per primitive kind; each call packs one record into the layer’s flat instance buffer and appends (or merges into) a draw command. There is no scene graph of retained shape objects — you push records every frame, or build a layer once and retain it.

Primitive catalogue Every drawable kind once — one shape per cell, from pushRect to pushPolyline.

Each kind below is one of these packed records. Stroke styling — caps, joins, and dashes — is where polylines and curves diverge from the SDF shapes:

Strokes — caps, joins & dashes How pushPolyline ends, corners, and dashes a stroke.

The renderer draws SDF primitives (rect / circle / ellipse / segment / curve / arc) directly from their packed parameters, so they stay pixel-crisp at any zoom. Polygons and polylines are CPU-tessellated into triangles. Sprites are a textured sibling pass drawn after the main geometry.

Layer.pushRect(shape) / Layer.addRects(shapes, options?). A rounded, optionally rotated rectangle. (x, y) is the top-left corner; width/height are full extents.

FieldTypeDefaultNotes
x, ynumberTop-left corner.
width, heightnumberFull extents.
fillColortransparentInterior fill.
strokeColortransparentBorder color.
strokeWidthnumber0Border thickness; straddles the edge.
cornerRadiusnumber0Rounded corners.
rotationnumber0Radians, about the rect center.

Layer.pushCircle(shape) / Layer.addCircles(shapes, options?).

FieldTypeDefaultNotes
cx, cynumberCenter.
radiusnumberRadius.
fillColortransparentInterior fill.
strokeColortransparentBorder color.
strokeWidthnumber0Border thickness.

Layer.pushEllipse(shape) / Layer.addEllipses(shapes, options?).

FieldTypeDefaultNotes
cx, cynumberCenter.
rx, rynumberRadii.
fillColortransparentInterior fill.
strokeColortransparentBorder color.
strokeWidthnumber0Border thickness.
rotationnumber0Radians, about the center.

Layer.pushSegment(shape) is the dedicated straight-line primitive: a compact record with butt caps and a 1D SDF — the cheapest stroke, preferred for high-count edges (tree branches, edge bundles). Joins between segments are the caller’s responsibility.

FieldTypeDefaultNotes
x1, y1numberStart point.
x2, y2numberEnd point.
colorColorStroke color (required).
widthnumber1Thickness in layer-local units.

Layer.pushLine(shape) is a back-compat alias for pushSegment — a LineShape is just a straight stroked segment, so it collapses onto the segment path. Layer.addLines(shapes, options?) bulk-appends segments.

Layer.pushCurve(shape). A cubic Bézier stroke through four control points, with optional per-end taper. The fragment solves distance-to-curve, so it stays crisp at any zoom.

FieldTypeDefaultNotes
p0p3Vec2Cubic control points (p0 start, p3 end).
colorColorStroke color (required).
widthnumber1Uniform thickness; overridden by the taper widths.
widthStartnumberwidthThickness at p0.
widthEndnumberwidthThickness at p3.
capLineCap"round"End cap. Round caps are free; butt/square discard past the endpoint.

Layer.pushArc(shape). A thick-stroked circular arc with butt caps, rendered SDF-based. theta0 / theta1 are absolute angles in radians; the rendered geometry uses the absolute span (direction is irrelevant). For a full ring pass theta1 = theta0 + 2π.

FieldTypeDefaultNotes
cx, cynumberCenter.
radiusnumberArc radius.
theta0numberStart angle (radians).
theta1numberEnd angle (radians).
colorColorStroke color (required).
widthnumber1Stroke thickness.

Layer.pushTriangle({ p1, p2, p3, fill }) / Layer.addTriangles(...). A flat-filled triangle — the substrate every polygon and polyline tessellates into. p1/p2/p3 are Vec2; fill is a Color.

Layer.pushPolygon(shape) / Layer.addPolygons(shapes, options?) accept two forms:

  • { points, holes?, fill } — a hole-aware filled polygon. points is the outer ring; holes are inner rings. Rings are flattened and triangulated with earcut on the CPU, then each triangle is appended through the triangle kind.
  • { triangles } — a pre-triangulated polygon: the caller supplies the triangle list directly.
import { createLayer, rgba } from "insomni";
const layer = createLayer();
layer.pushPolygon({
points: [
{ x: 0, y: 0 },
{ x: 100, y: 0 },
{ x: 100, y: 80 },
{ x: 0, y: 80 },
],
holes: [
[
{ x: 30, y: 20 },
{ x: 70, y: 20 },
{ x: 50, y: 60 },
],
],
fill: rgba(0.2, 0.5, 0.9, 1),
});

Layer.pushPolyline(shape) tessellates a stroked polyline on the CPU into triangles. This is the path with full cap / join / dash control.

FieldTypeDefaultNotes
pointsreadonly Vec2[]Vertices.
colorColorStroke color (required).
widthnumber1Thickness.
capLineCap"butt""butt" | "round" | "square".
joinLineJoin"miter""miter" | "bevel" | "round".
closedbooleanfalseClose the loop.
miterLimitnumber4Miter falls back to bevel past this.
dashPatternreadonly number[]— (solid)Alternating [dash, gap, …] lengths.
dashOffsetnumber0Offset into the dash pattern.

LineCap ("butt" | "round" | "square") and LineJoin ("miter" | "bevel" | "round") are the polyline styling unions. The curve kind expresses its cap via the cap field (also a LineCap); the integer constants CURVE_CAP_ROUND (0), CURVE_CAP_BUTT (1), and CURVE_CAP_SQUARE (2) are the packed-buffer encoding of those names, exported for callers that build curve buffers by hand.

Two helpers generate ring point lists you can stroke through pushPolyline — handy for dashed/dotted SDF-shape borders (the SDF rect/circle/ellipse pipeline does not support dashes natively, but a tessellated ring does):

  • polylineEllipseRing({ cx, cy, rx, ry, rotation?, segments? })Vec2[]
  • polylineRectRing({ x, y, width, height, cornerRadius?, cornerSegments? })Vec2[]

Both return an unclosed loop — pass closed: true to the polyline:

import { polylineEllipseRing, rgba } from "insomni";
const ring = polylineEllipseRing({ cx: 50, cy: 50, rx: 40, ry: 25 });
layer.pushPolyline({
points: ring,
color: rgba(0.9, 0.3, 0.3, 1),
width: 2,
closed: true,
dashPattern: [6, 4],
});

A textured quad cannot join the shape instance buffer (it carries an external texture binding), so sprites accumulate in a per-layer SpritePack and draw in a sibling pass after the main geometry, stacked by submission order. Adjacent sprites sharing a texture coalesce into one draw command.

Layer.pushSprite(sprite) / Layer.addSprites(sprites):

FieldTypeDefaultNotes
posVec2Position of the sprite anchor.
sizeVec2Size in the layer’s space.
textureTexture | TextureRegionSource image or sub-region.
tintColoropaque whiteMultiplicative tint.
rotationnumber0Radians, about the anchor.
anchorVec2{ x: 0.5, y: 0.5 }Normalized anchor within the sprite.
opaquebooleanfalseRoute to the opaque pass; set only when the texture is fully opaque.

The SpritePack (SPRITE_FLOATS = 16, SPRITE_BYTES = 64) plus the SpriteShape and SpriteCommand types are exported for callers who pack sprite buffers directly.

loadTexture(owner, src, options?) loads an image from a URL or any ImageBitmapSource into a caller-owned Texture (premultiplied alpha, matching the renderer’s blend state). owner is the renderer / root. LoadTextureOptions: format (default "rgba8unorm") and label. The returned Texture is never destroyed for you — call texture.destroy() when done.

import { loadTexture } from "insomni";
const tex = await loadTexture(renderer, "/icons.png");
layer.pushSprite({ pos: { x: 0, y: 0 }, size: { x: 32, y: 32 }, texture: tex });

A TextureRegion is a borrowed UV sub-rectangle of a Texture:

  • texture.region(x, y, w, h) — by pixel coordinates.
  • texture.regionUV(u0, v0, u1, v1) — by normalized UV.

Two atlas helpers wrap region lookup:

  • SpriteAtlas — named regions. new SpriteAtlas(texture, { name: { x, y, w, h }, … }), then atlas.region("name"). Region definitions use the SpriteAtlasRegionDef shape.
  • GridAtlas — a uniform grid. new GridAtlas(texture, { cell: [w, h], rows?, cols? }) (GridAtlasOptions), then atlas.frame(index) or atlas.frame(row, col).
import { GridAtlas } from "insomni";
const atlas = new GridAtlas(tex, { cell: [16, 16] });
layer.pushSprite({
pos: { x: 0, y: 0 },
size: { x: 16, y: 16 },
texture: atlas.frame(3),
});

SamplerDescriptor (filter / addressMode / mipmapFilter) is exported for layers that override the default sprite sampler.

For very high counts, Layer exposes pre-packed bulk uploads that skip per-record object allocation: addSegmentsPacked(data, { count, opaque, group? }), addCurvesPacked(...), and addArcsPacked(...). Each consumes a flat buffer at the stride exported as SEGMENT_FLOATS (6), CURVE_FLOATS (12), and ARC_FLOATS (8) floats per record, and is byte-identical to looping the matching single push. See Packing for the buffer layouts.

Several per-shape scalars (size, rotation, stroke width, corner radius) pack into IEEE-754 half-precision to keep the instance record small. The packing helpers are exported for callers who build instance buffers directly:

FunctionPurpose
packF16(value)Pack a float to its 16-bit half pattern (ties-to-even).
unpackF16(bits)Inverse of packF16.
pack2xF16(lo, hi)Pack two floats into one little-endian u32 (pack2x16float).
packUnorm8x4(r, g, b, a)Pack four [0,1] components into a unorm8x4 u32.