Math, color & animation
insomni ships a small, dependency-free math and animation toolkit used throughout the renderer and exported from the package root. Everything is tree-shakable free functions and tiny stateful objects — import only what you use.
lerpColor and in OKLab with lerpColorOklab. The easing curves below drive the tween and transition primitives covered later on this page.
smoothDamp glides, a spring overshoots, linear snaps. Vectors
Section titled “Vectors”Vec2 is { x: number, y: number }. The helpers are plain functions that return
new vectors (no mutation).
| Function | Signature | Result |
| ----------- | ------------------ | ------------------------------------- |
| vec2 | (x, y) => Vec2 | Construct a vector. |
| add | (a, b) => Vec2 | a + b. |
| sub | (a, b) => Vec2 | a - b. |
| scale | (v, s) => Vec2 | v * s. |
| length | (v) => number | Euclidean magnitude. |
| normalize | (v) => Vec2 | Unit vector ({0,0} if zero-length). |
| dot | (a, b) => number | Dot product. |
import { vec2, add, scale, normalize, length } from "insomni";
const dir = normalize(sub(target, origin));const step = add(origin, scale(dir, length(dir) * 0.5));Matrices
Section titled “Matrices”Mat3 is a column-major 3×3 affine matrix, a readonly 9-tuple. The renderer uses
it for view-projection; you’ll mostly need it for custom transforms.
| Function | Description |
| ----------------------------- | --------------------------------------------------------- |
| IDENTITY | The identity matrix constant. |
| translation(tx, ty) | Translation matrix. |
| scaling(sx, sy = sx) | Scale matrix (uniform if sy omitted). |
| rotation(radians) | Rotation about the origin. |
| scalingAt(sx, sy, cx, cy) | Scale around an arbitrary center. |
| rotationAt(radians, cx, cy) | Rotate around an arbitrary center. |
| multiply(a, b) | a · b — applies b first, then a. |
| invert(m) | Inverse, or null if singular. |
| transformPoint(m, x, y) | Apply the matrix to a point → Vec2. |
| projection(width, height) | Orthographic pixel → NDC: (0,0)→(-1,1), (w,h)→(1,-1). |
import { multiply, translation, rotationAt, transformPoint } from "insomni";
const m = multiply(translation(100, 50), rotationAt(Math.PI / 4, cx, cy));const p = transformPoint(m, x, y);Bézier
Section titled “Bézier”| Function | Description |
| ------------------------------ | ------------------------------------------------------------------------------------------ |
| cubicEval(p0, p1, p2, p3, t) | Evaluate a cubic Bézier at t (not clamped) → Vec2. |
| cubicAABB(p0, p1, p2, p3) | Exact axis-aligned bounding box over t ∈ [0, 1] → AABB ({ minX, minY, maxX, maxY }). |
cubicAABB solves the derivative roots per axis, so it’s the tight box, not the
control-point hull.
Color is { r, g, b, a } with channels in [0, 1].
Construct
Section titled “Construct”| Function | Description |
| ------------------------ | -------------------------------------------------------------- |
| rgba(r, g, b, a = 1) | From channels. |
| hex(value) | 24-bit hex int, e.g. hex(0xff6600). Alpha is 1. |
| cssHex(value) | CSS string, e.g. cssHex("#ff6600") or cssHex("#ff660080"). |
| hsl(h, s, l, a = 1) | HSL → RGB. All inputs [0, 1]; hue wraps. |
| hsv(h, s, v, a = 1) | HSV → RGB. All inputs [0, 1]; hue wraps. |
| hslToRgb / hslToRgba | Aliases of hsl. |
Blend & adjust
Section titled “Blend & adjust”| Function | Description |
| ------------------------- | --------------------------------------------- |
| lerpColor(a, b, t) | Channel-wise linear interpolation. |
| mixColor | Alias of lerpColor (shader mix() naming). |
| withAlpha(color, alpha) | Copy with a replaced alpha. |
| fade(color, factor) | Multiply the existing alpha. |
| lighten(color, t) | Mix toward WHITE by t. |
| darken(color, t) | Mix toward BLACK by t. |
Constants: BLACK, WHITE, TRANSPARENT.
import { cssHex, withAlpha, lighten, BLACK } from "insomni";
const accent = cssHex("#3b82f6");const ghost = withAlpha(accent, 0.15);const hover = lighten(accent, 0.2);Color science
Section titled “Color science”Perceptual spaces (OKLab / OKLCH)
Section titled “Perceptual spaces (OKLab / OKLCH)”sRGB ↔ OKLab / OKLCH conversions plus space-aware interpolation. OKLab is the
modern default (CSS Color 4 color-mix uses it).
| Function | Description |
| ----------------------------- | -------------------------------------------------------- |
| srgbToOklab(color) | sRGB Color → { L, a, b }. |
| oklabToSrgb(lab, alpha = 1) | OKLab → sRGB Color (gamut-clipped). |
| oklabToOklch(lab) | OKLab → { L, C, h } (hue in turns [0, 1)). |
| oklchToOklab(lch) | OKLCH → OKLab. |
| lerpInSpace(a, b, t, space) | Interpolate two colors in a BlendSpace. |
| lerpColorOklab(a, b, t) | Shorthand for lerpInSpace(..., "oklab") (recommended). |
| lerpColorRgb(a, b, t) | Shorthand for the naive sRGB lerp. |
BlendSpace is "srgb" | "linear-rgb" | "oklab" | "oklch" | "hsl". oklch
preserves hue along the path (great for staying “in family”); oklab is the
perceptually-uniform default.
import { lerpInSpace, cssHex } from "insomni";
const ramp = (t: number) => lerpInSpace(cssHex("#001"), cssHex("#fb0"), t, "oklab");WCAG contrast
Section titled “WCAG contrast”WCAG 2.1 contrast math and a lightness search that nudges a color to meet a threshold against a fixed background.
| Function | Description |
| ---------------------------------------------- | -------------------------------------------------------------------------- |
| relativeLuminance(color) | WCAG relative luminance. |
| contrastRatio(a, b) | Symmetric ratio in [1, 21]. |
| wcagMinContrast(opts?) | Required ratio for WcagTextOptions. |
| meetsContrast(fg, bg, opts?) | Whether fg on bg clears the threshold. |
| findAccessibleColor(target, fixed, minRatio) | Nearest shade of target meeting minRatio (preserves hue + saturation). |
| colorAtContrast(target, fixed, ratio) | Nearest shade landing at ratio — equalizes label weight. |
WcagTextOptions is { fontSizePx?, bold?, level? }. level is "AA" / "AAA"
(WcagLevel) with the large-text exception, or a raw number for an absolute ratio.
import { findAccessibleColor, cssHex } from "insomni";
const safe = findAccessibleColor(cssHex("#9aa"), cssHex("#fff"), 4.5);APCA contrast
Section titled “APCA contrast”APCA (the perceptual algorithm proposed for WCAG 3) returns a signed lightness
contrast Lc (roughly [-108, +106]): positive for dark-on-light, negative for
light-on-dark.
| Function | Description |
| ------------------------------------------ | ----------------------------------------------------------- | --- | -------------------------------- |
| apcaContrast(fg, bg) | Signed Lc. 0 for pairs too close to read. |
| apcaPolarity(fg, bg) | "normal" (dark-on-light) or "reverse" (ApcaPolarity). |
| apcaFontLookup(fontSizePx, weight = 400) | Minimum | Lc | recommended for that text. |
| findApcaColor(target, bg, minLc) | Nearest shade of target reaching minLc against bg. |
| colorAtApca(target, bg, lc) | Nearest shade landing at | Lc | — equalizes perceptual weight. |
import { apcaContrast, findApcaColor, cssHex } from "insomni";
const lc = apcaContrast(cssHex("#333"), cssHex("#fff")); // ~ +90const label = findApcaColor(cssHex("#888"), cssHex("#111"), 75);Scalars
Section titled “Scalars”| Symbol | Description |
| --------------------------------- | --------------------------------------------- |
| TAU, PI, HALF_PI | 2π, π, π/2. |
| DEG_TO_RAD, RAD_TO_DEG | Conversion constants. |
| clamp(v, min, max) | Clamp into a range. |
| clamp01(v) | Clamp into [0, 1]. |
| lerp(a, b, t) | Linear interpolation. |
| inverseLerp(a, b, value) | Where value sits between a and b. |
| remap(v, a0, a1, b0, b1) | Map from one range to another (unclamped). |
| smoothstep(edge0, edge1, v) | Hermite smoothstep (clamped). |
| smootherstep(edge0, edge1, v) | Perlin 5th-order smoothstep. |
| degToRad(deg) / radToDeg(rad) | Angle conversion. |
| wrap(value, modulus) | Wrap into [0, modulus) (handles negatives). |
| sign(value) | -1 / 0 / 1. |
| approxEqual(a, b, eps = 1e-6) | Tolerance compare. |
Random
Section titled “Random”Seeded PRNGs and sampling helpers. Each PRNG is a factory (seed) => Rng, where
Rng is () => number returning a uniform [0, 1) float. Distribution helpers
take the Rng as their first argument — one stream feeds many distributions.
(Deterministic and fast; not cryptographic.)
| Generator | Notes |
| ---------------------- | ---------------------------------------------- |
| mulberry32(seed) | Tiny, fast, period 2³². |
| splitmix32(seed) | Avalanches small seeds well; good for seeding. |
| xorshift32(seed) | Minimal, fastest. |
| sfc32(a, b?, c?, d?) | 128-bit state, high quality. |
| Distribution | Description |
| ----------------------------------- | --------------------------------------------- |
| uniform(rng, min, max) | Float in [min, max). |
| uniformInt(rng, min, max) | Integer in [min, max]. |
| gaussian(rng, mean = 0, std = 1) | Normal (Box–Muller). |
| exponential(rng, lambda = 1) | Exponential. |
| logNormal(rng, mu = 0, sigma = 1) | Log-normal. |
| bates(rng, n = 1) | Mean of n uniforms. |
| triangular(rng, min, max, mode) | Triangular distribution. |
| pick(rng, arr) | One element uniformly. |
| shuffle(rng, arr) | Fisher–Yates; returns a new array. |
| sample(rng, arr, count) | count distinct elements. |
| weighted(rng, arr, weights) | Pick by weight. |
| onUnitCircle(rng) | Point on the unit circle → [x, y]. |
| inUnitDisk(rng) | Point inside the unit disk (uniform density). |
import { mulberry32, gaussian, pick } from "insomni";
const rng = mulberry32(42);const jitter = gaussian(rng, 0, 0.5);const color = pick(rng, palette);Animation & scheduling
Section titled “Animation & scheduling”Invalidator
Section titled “Invalidator”createInvalidator(initialDirty = true) is a dirty-tracking primitive for
on-demand rendering: check dirty before a render and clear() after.
| Member | Description |
| --------------------------- | ------------------------------------------------------- |
| dirty | True if anything invalidated since the last clear(). |
| invalidate() | Mark dirty. |
| onInvalidate(cb) | Subscribe to invalidation edges (e.g. wake a rAF loop). |
| clear() | Clear the flag. |
| track(source) | Auto-invalidate on source.onChange. |
| trackElement(el, events?) | Auto-invalidate on DOM events (pointer/wheel/keydown). |
| dispose() | Release all tracked subscriptions. |
import { createInvalidator } from "insomni";
const inv = createInvalidator();inv.track(viewport); // re-render on camera changeinv.trackElement(canvas); // ...and on input
function loop() { if (inv.dirty) { inv.clear(); renderer.render(layers); } requestAnimationFrame(loop);}animateToward
Section titled “animateToward”animateToward(opts) is a minimal “ease toward a target” numeric, frame-rate
independent (1 - exp(-speed·dt)). Pair it with an Invalidator to keep frames
flowing until it settles.
AnimateTowardOptions: initial (default 0), speed (default 12),
epsilon (default 0.002), invalidator. The returned AnimatedValue exposes
value, target, active, setTarget(value, immediate?), and step(dt).
import { animateToward } from "insomni";
const opacity = animateToward({ speed: 8, invalidator: inv });opacity.setTarget(hovered ? 1 : 0);
// per frameopacity.step(dt);draw(opacity.value);Motion primitives
Section titled “Motion primitives”Each is a tiny stateful object with a step(dt) method that returns the new
value; compose by running one per scalar dimension. Common vocabulary: value,
target, settled.
| Factory | Model |
| ----------------------------- | ---------------------------------------------------------------------------- |
| tween(opts) | Duration-driven interpolation with an easing; retargetable. |
| spring(opts) | Physical m·ẍ + c·ẋ + k(x-target) spring (semi-implicit Euler, substepped). |
| springFromDuration(spec) | Designer-friendly { duration, bounce } → { stiffness, damping, mass }. |
| smoothDamp(opts) | Critically-damped second-order filter (Unity SmoothDamp). |
| decay(opts) | Pure exponential velocity decay (fling / inertial scroll). |
| transition(opts) | Fixed-duration, generic-T motion; also a ReadableSignal. |
| tweenTo(opts) | Fire-and-forget tween that owns its own rAF clock. |
| createVelocityTracker(opts) | 2D pointer velocity estimator for fling handoff. |
smoothDamp
Section titled “smoothDamp”The workhorse behind bindViewport. One knob — smoothTime (default 0.1,
0 = instant) — approximates time to converge; built-in velocity makes it good
for tracking a moving target and absorbing fling impulses.
import { smoothDamp } from "insomni";
const sd = smoothDamp({ smoothTime: 0.15, initialValue: 0 });sd.setTarget(100);sd.step(dt); // returns the new valuesd.setVelocity(500); // inject a fling impulseSmoothDamp members: value, velocity, target, settled, smoothTime,
maxSpeed, step(dt), setTarget, setValue, setVelocity, reset.
tween & easings
Section titled “tween & easings”tween({ from, to, duration, easing?, delay? }) returns a Tween (value,
from, to, duration, elapsed, done, step, reset, sampleAt,
retarget). tweenTo({ from, to, duration, onChange, ... }) is the imperative
version that ticks itself and returns a TweenHandle (active, cancel); number
and Color values interpolate by default (Color via OKLab). Supply a custom
TweenScheduler to plug into a host loop, or use defaultTweenScheduler().
The easings module exports Easing ((t) => number), linear, the full
easeIn/Out/InOut family across quad/cubic/quart/quint/expo/sine/circ/back/elastic/bounce,
and cubicBezier(x1, y1, x2, y2) (CSS-compatible).
import { tweenTo, easeOutCubic, rgba } from "insomni";
tweenTo({ from: rgba(1, 0, 0), to: rgba(0, 0, 1), duration: 0.4, easing: easeOutCubic, onChange: (c) => paint(c),});spring & decay
Section titled “spring & decay”spring(opts) integrates a physical spring; tune by stiffness / damping /
mass or derive them from springFromDuration({ duration, bounce }) (bounce: 0
= critically damped, > 0 overshoots, < 0 overdamped). decay(opts) is pure
exponential velocity decay with a projectedDistance() for targeting a boundary.
Both expose value / velocity / settled plus step, setValue,
setVelocity, and reset (spring also has setTarget).
transition
Section titled “transition”transition<T>(opts) is a fixed-duration, retargetable motion over any T (pass
interpolate for non-numbers). It is also a ReadableSignal<T> — get / peek
/ subscribe work in a reactive context — while step(dt) is the imperative
driver. TransitionOptions: initial, duration, easing?, interpolate?,
equals?, invalidator?. This is what powers the fade in the tooltip and menu
UI primitives.
See also
Section titled “See also”- Cameras & viewport —
smoothDampand the velocity tracker drivebindViewport. - Interactions —
Invalidator,transition, andColorfeed the UI primitives. - Spaces & cameras — where
Mat3projection andVec2coordinates are used.