documentation · generative charts api

The API.

one shared surface — the same in 2D · CPU and 3D · GPU

Every chart in Generative Charts is a short recipe against this one API. Both renderers speak the same namescreateChart, line, glow, grow, the draw* living verbs, and the cam / light / lights controls — so you write a chart once and it runs on whichever renderer the library picks. Zero dependencies; the browser is the whole runtime.

← Back to Generative Charts

install

One package. Both renderers.

the library picks — the recipe doesn't change

# one package, both renderers — CPU (canvas) and GPU (WebGL)
npm install @chromatic-coherence/generative-charts

A canvas and a browser are the whole runtime — no build step, no peer dependencies. Both editions are browser-only, so guard SSR imports.

quick start

Lay light. Register life. Start the frame.

the whole shape in a dozen lines

import { createChart, v3 } from "@chromatic-coherence/generative-charts";

// renderer: "auto" (default) — GPU when the browser can, CPU when it can't
const w = createChart({ canvas, controls: true });

// lay static light — batched by colour, no z-buffer
w.line(v3(-100, 0, 0), v3(100, 0, 0), { hue: 265, alpha: 0.5 });
w.glow(v3(0, 0, 0), { hue: 46, size: 40, alpha: 0.8 });

// register life — drawn every frame (this is where live data goes)
w.grow((t, dt, world) => {
  world.drawGlow(v3(Math.cos(t) * 80, 0, Math.sin(t) * 80), 210, 12, 0.9);
});

w.start();
console.log(w.mode); // "3d" or "2d" — which renderer you actually got

design laws

Four laws the API rests on.

why there is no z-buffer

  • Additive light onlythe whole aesthetic is light on dark; the canvas composites in 'lighter' — and the ink edition (theme: "light") is the same maths read in reverse: light on dark becomes ink on paper
  • Additive light commutesdraw order cannot lie — so no z-buffer is needed, ever
  • Batch by colourquantised HSL folds thousands of static lines into a handful of strokes
  • Zero dependenciesone file, no imports — the browser is the only runtime

the surface

Every export, by area.

transcribed from the source of truth

The tables below are the full public surface — each entry is a real export of @chromatic-coherence/generative-charts; both renderers speak it.

vectors & scalars

The maths the engine speaks. V3 is a plain object — no classes, no wrappers.

V3
type V3 = { x: number; y: number; z: number }the vector everything speaks
v3
v3(x?, y?, z?): V3construct a vector (defaults 0,0,0)
add / sub
add(a: V3, b: V3): V3 · sub(a: V3, b: V3): V3vector sum and difference
scale
scale(a: V3, s: number): V3scalar multiply
dot / cross
dot(a, b): number · cross(a, b): V3the products
len / norm
len(a): number · norm(a): V3length and unit vector (zero-safe)
TAU
const TAU = Math.PI * 2one full turn
clamp / lerp
clamp(v, lo, hi) · lerp(a, b, u)the two workhorses
easeInOut
easeInOut(u: number): numbercubic ease, both ends
rnd
rnd(i: number, k: number): numberdeterministic pseudo-random in [0,1) — a fixed field that never churns on resize

creating a world

One canvas, one World. The library owns the camera, the projection, the fog, the frame loop — and WHICH renderer runs: createChart picks CPU or GPU for you.

createChart
createChart(opts: ChartOpts): ChartWorldthe self-aware door in — picks the renderer, budgets GPU contexts, falls back per-chart
ChartOpts.renderer
renderer?: "auto" | "2d" | "3d" = "auto"GPU when available and under budget, CPU otherwise; "3d" prefers GPU but still lands on CPU rather than fail; "2d" forces CPU
ChartOpts.maxGpu
maxGpu?: number = 5page-wide budget of live GPU charts — browsers cap live WebGL contexts, so charts over budget build on the CPU renderer
world.mode
readonly mode: "2d" | "3d"which renderer this world actually runs on
createWorld2d / createWorld3d
createWorld2d(opts): World2D · createWorld3d(opts): World3Dthe direct doors — reach a specific renderer when you must
WorldOpts.canvas
canvas: HTMLCanvasElementthe surface to draw on
WorldOpts.maxDpr
maxDpr?: number = 1.25device-pixel-ratio cap — resolution is the enemy of 60fps additive canvas
WorldOpts.drift
drift?: number = 0.045auto-orbit speed, radians per second
WorldOpts.parallax
parallax?: number = 0.3mouse-parallax strength
WorldOpts.controls
controls?: boolean = falsedrag-to-orbit + ctrl/cmd-wheel zoom (plain wheel keeps scrolling the page)
WorldOpts.theme
theme?: "dark" | "light" = "dark"the INK EDITION — "light" renders the same recipe on paper: lightness inverts, the light pass composites as ink (with a density lift for equal weight), fog fades toward the page. Recipes need no changes — both engines, one option
WorldOpts.camera
camera?: { target?, dist?, pitch?, yaw?, fov? }starting camera overrides

building the static world

Static geometry is BATCHED: colours are quantised (hue to 15°, sat to 25, light to 12, alpha to eighths) so thousands of lines collapse into a handful of strokes.

line
line(a: V3, b: V3, { hue, sat?, light?, alpha, group? })a batched line in HSL — the wireframe workhorse
glow
glow(p: V3, { hue?, white?, size, alpha, group? })a sprite glow — pre-rendered radial sprites, one per 10° of hue, plus white
dust
dust(p: V3, alpha: number)a twinkling grain of screen dust
fade
fade(name: string, target: number)fade a named group — every line and glow belongs to one; alpha eases toward the target

the dark pass (v0.2–v1.0, complete)

Opaque surfaces under the additive light — the law bends: the dark occludes; the light still commutes above it. v0.3: two beats. v0.4: a steerable light. v0.5: light is a place; bodies throw shadows. v0.6: the world reflects. v0.7: occlusion complete. v1.0: walkable. Feature-frozen — the GPU edition is the next engine.

mirrorPlane
createWorld({ …, mirrorPlane: 0, mirrorAlpha?: 0.35 })still water — every face and living light above the plane reflects beneath it, re-shaded at its mirrored place, dimmed
pick
pick(sx, sy, planeY = 0): V3 | nullscreen point → the ground, through the frame's camera basis — a tap becomes a place (the walkable city runs on it)
surface
get surface(): HTMLCanvasElementthe canvas this world draws on — for scenes that wire their own input
lights
world.lights.push({ p: V3, intensity, falloff })point lights — Lambert × inverse-square falloff + a facet specular glint toward the camera; steerable per frame
shadowPlane
createWorld({ …, shadowPlane: 0 })every face, static and living, casts onto the plane along lights[0] (or the directional) — near-black fills in the same painter sort
light
world.light = { dir: V3, ambient, diffuse, signed }steerable like cam — faces shade at render time; the default is pixel-identical to the sky shade; signed:true gives true Lambert with a dark side
occludeLight
createWorld({ …, occludeLight: true })static glows join the painter sort — the dark can hide them (glows only; lines and living light still ride above)
face
face(a: V3, b: V3, c: V3, { hue, sat?, light?, alpha?, group? })a static opaque triangle — painter-sorted far to near each frame, filled before the light passes
drawFace
drawFace(a: V3, b: V3, c: V3, FaceOpts)an opaque triangle that LIVES — laid from a grow hook, gathered into this frame's dark pass, sorted with the static faces
slope shade
light × (0.55 + 0.45 · |normal.y|)computed once at lay time — a face is lit by how much it looks at the sky; winding never matters

living geometry — grow()

Everything that lives and changes is a grow hook, called every frame with the world's camera already set up.

grow
grow(fn: GrowFn)register a per-frame hook
GrowFn
type GrowFn = (t: number, dt: number, w: World) => voidseconds since start · seconds since last frame (capped 50ms) · the world

camera & projection

Valid inside grow hooks and settle — the per-frame basis is computed before hooks run.

cam
cam: { target, dist, pitch, yawBase, yawUser, fov, … }public camera state — a scene may steer it live from a hook
project
project(p: V3): Projected | nullworld to screen — { x, y, s, z }; null behind the near plane
fog
fog(z: number): numberdepth attenuation for a projected z
W / H
W: number · H: numbercanvas CSS size, live through resize

immediate-mode drawing

For grow hooks: draw this frame, projection- and fog-aware, on top of the batched static world.

drawLine
drawLine(a: V3, b: V3, color: string, alpha: number, width?)one projected line
drawPath
drawPath(pts: { p: V3 }[], color, alpha, width?)a projected polyline — segments break cleanly at the near plane
drawGlow
drawGlow(p: V3, hue: number | null, size, alpha)one projected glow — null hue draws the white sprite
drawLabel
drawLabel(p: V3, text, color, alpha, dy?)a mono label, composited over the light
overlay
overlay(cb: (g: CanvasRenderingContext2D, w: World) => void)raw canvas access for screen-space work (voids, vignettes) — never occluded, by design

lifecycle

The loop pauses itself on a hidden tab, and a one-step governor drops resolution and thins the far field if frames slip.

start
start(): voidbegin the requestAnimationFrame loop
settle
settle(t: number): voidone settled frame — the reduced-motion path
capturePoster
capturePoster(opts, build, width, height, t?): string | nullthe window's first frame as a PNG data URL, rendered off-screen with the context released — the placeholder-image pattern: paint the poster at load, build the live chart when watched
dispose
dispose(releaseContext?: boolean): voidcancel the loop and remove every listener. On the 3D engine, pass true to also release the WebGL context immediately — browsers cap live contexts per page, so many-chart pages release on teardown and mount a fresh canvas to build again; leave it false to reuse the same canvas

start growing

See it live.

every chart is a recipe against this API

Every chart on the Generative Charts page is a recipe against this surface — toggle 2D · CPU ↔ 3D · GPU, copy the full code with your data, and make it your own.