agentleFS
bvolpatobvolpato/vizmatic/docs/llms-full.txt

vizmatic

llms.txt3 starsChanged 4 months agoMITView on GitHub

Use Vizmatic to explain things visually from code. It makes two kinds of output: Vizmatic requires Node.js 20 or newer. MP4 and WebM also need FFmpeg. For pnpm 11, allow esbuild's install script with allowBuilds: { esbuild: true } in the root pnpm-workspace.yaml. Start from a template, or write the function yourself. Templates are explainer, equation, architecture, chart, and deck. Rules of the engine: An agent cannot watch a video, so judge a film from the check and from stills.

  • Installs packages

The file

Show its rules and text

We split this llms-full.txt into the rules it gives an agent

27 rules. About 0 are common, 12 are distinctive, and 15 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈12

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 15

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    Use Vizmatic to explain things visually from code.

    line 5
  2. Very specific:

    Start from a template, or write the function yourself.

    line 20
  3. Very specific:

    Use font: 'text-serif' for sentences, labels, and numbers.

    line 59
  4. Very specific:

    Run vizmatic objects and vizmatic looks to see what the installed version offers.

    line 61
  5. Distinctive:

    Use one idea per beat. Hold for a second after each reveal, so a viewer can read it.

    line 81
  6. Distinctive:

    Use chapters. They mark the player's timeline and are the steps of a presentation.

    line 84
  7. Distinctive:

    When a film must teach a topic, lessons.md has the full method.

    line 88
  8. Distinctive:

    Put accuracy first, then completeness, clarity, and brevity.

    line 90
  9. Distinctive:

    Keep each title true for as long as it is visible.

    line 93
  10. Very specific:

    Read a finished film before you write a similar one.

    line 98
  11. Very specific:

    Create frame.tsx. Scene title and subtitle are optional.

    line 102
  12. Distinctive:

    Use a full module when a frame needs helper code or data:

    line 129
  13. Distinctive:

    After creating a visual, report:

    line 153
  14. Distinctive:

    Write the film as a TypeScript file.

    line 170
  15. Very specific:

    Run vizmatic check --json and fix what it reports.

    line 171
  16. Distinctive:

    Create a film from a starter that renders as written and passes the check.

    line 624
  17. Very specific:

    Run npx vizmatic --help to see every option.

    line 705
  18. Very specific:

    When the function returns, the film is compiled.

    line 764
  19. Very specific:

    Put a label at a moment with s.at, so one section does not push others around.

    line 913
  20. Distinctive:

    Use a figure when the reader studies the picture.

    line 938
  21. Distinctive:

    Every component reads its type, spacing, strokes, and corner radius from one set of tokens.

    line 1021
  22. Very specific:

    Use vizmatic-scene when an application stores editable TSX from a user or an agent.

    line 1072
  23. Very specific:

    Use vizmatic/scene-browser in a dedicated module Worker for local editing, scrubbing and playback.

    line 1146
  24. Distinctive:

    Keep only the latest queued frame, stop playback when hidden, and terminate the Worker on an application deadline or when its editor closes.

    line 1188
  25. Very specific:

    Use FilmPlayer on your own canvas when you want your own controls.

    line 1273
  26. Very specific:

    When a CLI frame omits width or height, Vizmatic starts at 960x540 and grows that axis if content overflows.

    line 1491
  27. Very specific:

    Use Vizmatic primitives for common layouts.

    line 1585

≈ “Common” is an estimate: at least 1 in 5 of the 3,592 llms.txt files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
llms-full.txt
# Prompt

# Vizmatic agent prompt

Use Vizmatic to explain things visually from code. It makes two kinds of output:

- A **film** is motion. Write a TypeScript function with `vizmatic/stage`. It plays live in a browser and exports to MP4, WebM, GIF, PNG, and SVG.
- A **figure** is a still. Write TSX with typed components from `vizmatic`. It renders to PNG and SVG in dark and light themes.

Vizmatic requires Node.js 20 or newer. MP4 and WebM also need FFmpeg.

```bash
npm install vizmatic
```

For pnpm 11, allow esbuild's install script with `allowBuilds: { esbuild: true }` in the root `pnpm-workspace.yaml`.

## Films

Start from a template, or write the function yourself. Templates are `explainer`, `equation`, `architecture`, `chart`, and `deck`.

```bash
npx vizmatic new intro --template explainer
```

```ts
import { axes, draw, dynamic, dot, fadeIn, film, line, tex, write } from 'vizmatic/stage'

export default film({ title: 'A limit you can see' }, (s) => {
    s.chapter('The curve')
    const plot = s.add(axes({ x: [-1, 4], y: [-1, 9], grid: true, width: 1080, height: 660 }).at(-340, 60))
    s.play(draw(plot.grid, { lag: 0.02 }), draw(plot.lines), fadeIn(plot.numbers))
    s.play(draw(plot.plot((x) => x * x, { stroke: 'blue', width: 5 })))

    s.chapter('The secant')
    const h = s.value(2)
    const secant = s.add(dynamic(() => {
        const slope = 2 + h.value
        return [
            line(plot.world(0.2, 1 + slope * -0.8), plot.world(3.2, 1 + slope * 2.2), { stroke: 'yellow', width: 3.5 }),
            dot(9, 'yellow').at(plot.world(1 + h.value, (1 + h.value) ** 2)),
        ]
    }))
    s.play(fadeIn(secant))
    s.play(write(s.add(tex('m = 2 + h', { size: 64 }).at(600, -120))))
    s.play(h.to(0.02, { duration: 4 }))
    s.wait(1.5)
})
```

Rules of the engine:

- The stage is 1920 by 1080. (0, 0) is the center and y points down. Plots convert with `plot.world(x, y)`.
- The build function runs once and may run again while assets load. Keep it free of side effects.
- `s.play(...)` writes animation and moves time forward. `s.wait(seconds)` holds. `s.at(time, build)` authors at another time.
- A `dynamic(() => ...)` drawing rebuilds on every frame. Read `value.value` inside it to drive anything.
- `morph(a, b)` leaves `b` on stage and `a` hidden. Later code refers to `b`.
- A `duration` on `s.play` stretches every animation to fill it, and a `duration` inside one of those animations has no effect. For different lengths, leave it off `s.play`, set it on each animation, and use `delay` for an offset: `s.play(fadeOut(oldText, { duration: 0.15 }), delay(fadeIn(newText, { duration: 0.25 }), 0.1))`. Inside a `s.play` that has a duration, use `seq(fadeOut(oldText), fadeIn(newText))`.
- Use `font: 'text-serif'` for sentences, labels, and numbers. Use `font: 'serif'` only for short titles at a large size.
- Colors are look tokens such as `'blue'`, `'yellow'`, `'ink'`, and `'dim'`, so a film renders in every look.
- Run `vizmatic objects` and `vizmatic looks` to see what the installed version offers.

### Check, look, and export

An agent cannot watch a video, so judge a film from the check and from stills.

```bash
npx vizmatic check intro.ts --json
npx vizmatic film intro.ts --frame 3,6.5,12 --width 1280 --out stills
npx vizmatic film intro.ts --out dist/films --look midnight,paper --quality high
```

`vizmatic check` samples the moments where the film comes to rest. It reports `film.off_canvas`, `film.text_edge`, `film.text_overlap`, `film.text_crossed`, and `film.missing_glyph`, each with times, a `box` in canvas pixels, and a `stage` center in authoring coordinates. Fix every error, then open the stills and look at them. `vizmatic preview intro.ts` opens a live player with a timeline for a person to review. The [Studio](https://bvolpato.github.io/vizmatic/studio.html) is a visual film editor in the browser for a person who wants to edit by hand.

The check does not look at motion, so a crossfade where two sentences overlap and a blank gap between two pictures both pass it. Add `--motion` to inspect every frame: `npx vizmatic check intro.ts --motion --json`. It reports a problem that appears only in motion as a warning, 0.2 seconds or more with nothing visible as `film.blank`, and text that leaves before a viewer can read it as `film.text_brief`. Judge each warning, because the check cannot tell a collision from one picture that replaces another. `--content-top <px>` sets where the content starts, so that a heading that stays up does not hide a blank.

`--audio narration.wav` adds sound, padded or trimmed to the film's length, so hold until the voice ends. `s.caption(text, seconds)` writes a WebVTT file beside the video and draws nothing into the picture. MP4 output is H.264 with settings that QuickTime can play. To join a clip to a film without encoding it again, encode the clip with `filmEncoderSettings(quality)` from `vizmatic/stage/export`.

Motion guidance:

- Use one idea per beat. Hold for a second after each reveal, so a viewer can read it.
- Move the thing being explained. Avoid decorative motion.
- Give color a meaning and keep it. A quantity keeps its color from its first appearance to its last.
- Use chapters. They mark the player's timeline and are the steps of a presentation.

### Lessons

When a film must teach a topic, [lessons.md](https://bvolpato.github.io/vizmatic/references/lessons.md) has the full method. In short:

- Put accuracy first, then completeness, clarity, and brevity. There is no target length, so keep every passage that teaches.
- Finish the script before the film. Teach the problem, then the mechanism, then the limits. Keep one worked example throughout.
- Compute every number in the film, and check it with `assert`. The voice and the screen use the same values and the same rounding.
- Keep each title true for as long as it is visible. List every text block with its visible times, and read each one against the picture and the voice.
- Change pictures cleanly. Old text drops under about 25% opacity before new text reaches it, and something stays on screen through the change.
- A person must watch the film at full speed with sound before it is published. You cannot do this, so say that it has not happened.
- Use the computed states from `vizmatic` where they fit: `weightedVectorState`, `categoricalSampling`, `planTeachingRoutedTransfer`, `byteDifferenceState`, and `tokenHistoryState`. Show the limits of each. `check` and `film` accept this import, and `preview` refuses it because it runs in the browser. A project with a checked canvas host can use the same constructions through `createTeachingCanvas(painter)`.

Read a finished film before you write a similar one. `examples/films/` in the package holds them with full source, and the [Films](https://bvolpato.github.io/vizmatic/films.html) page plays them.

## Figures

Create `frame.tsx`. `Scene` title and subtitle are optional.

```tsx
<Scene title="Checkout, read path">
  <Flow
    stages={[
      { title: "Gateway", subtitle: "Envoy, p99 9 ms", tone: "blue" },
      { title: "Cart", subtitle: "Go, 840 rps", tone: "purple" },
      { title: "Orders DB", subtitle: "Postgres, primary + 3", tone: "green" },
    ]}
  />
</Scene>
```

```bash
npx vizmatic check ./frame.tsx --theme dark,light --json
npx vizmatic ./frame.tsx --out ./figures --theme dark,light --format both
```

Fix all errors before the final render. Warnings report low contrast, small text, overlapping labels, uneven panel whitespace, connector congestion, and asset fallbacks.

Figures default to the `dense` density, like a figure in an engineering paper. Fill the space with useful detail: runtimes, rates, schemas, and limits. Do not spread three boxes across a slide. Use `density="presentation"` on a `Scene`, or `density` in a render option, only for a room. The densities are `dense`, `compact`, `comfortable`, `presentation`, or a number.

Choose by meaning: `GraphDiagram` for systems, `SequenceDiagram` for interactions, `DataflowDiagram` for lineage, `DeploymentDiagram` for placement and trust, `TransformerTopology` for models, and `LineChart`, `BarChart`, `DataTable`, or `Heatmap` for data. Omit `width` and `height` to auto-grow from 960 by 540.

PNG and SVG output is alpha-transparent by default. Add `--background theme` or `background={c.bg}` for an opaque canvas. Use `preset = "engineering";` with `--theme light` for flat article figures.

Use a full module when a frame needs helper code or data:

```tsx
import { Flow, Scene, getThemeColors, type ThemeMode } from "vizmatic"

export const width = 1040
export const height = 560

export function create(theme: ThemeMode = "dark") {
  const c = getThemeColors(theme)
  return <Scene c={c} title="Runtime path"><Flow c={c} stages={[{ title: "Input", tone: "blue" }, { title: "Check", tone: "purple" }]} /></Scene>
}
```

## Reference

- [CLI](https://bvolpato.github.io/vizmatic/docs/cli.html) and [Exporting](https://bvolpato.github.io/vizmatic/docs/exporting.html): every flag.
- [Animations](https://bvolpato.github.io/vizmatic/docs/animations.html), [Objects](https://bvolpato.github.io/vizmatic/docs/objects.html), [Camera](https://bvolpato.github.io/vizmatic/docs/camera.html), and [3D](https://bvolpato.github.io/vizmatic/docs/three-d.html): the film API.
- [Figure recipes](https://bvolpato.github.io/vizmatic/docs/figure-recipes.html) and [Figures reference](https://bvolpato.github.io/vizmatic/docs/figures.html): component props and complete frames.
- [Science guide](https://github.com/bvolpato/vizmatic/blob/main/SCIENCE.md): accurate diagrams for math, physics, chemistry, and machine learning.
- [`llms-full.txt`](https://bvolpato.github.io/vizmatic/llms-full.txt): this prompt, all guides, and the component catalog.

## Final response

After creating a visual, report:

- files created or changed
- the exact render command used
- output paths
- which looks or themes rendered
- whether `vizmatic check` passed
- any overflow or layout fixes made
- for a film, which stills you opened, and whether you scanned the motion
- anything you could not verify, including that nobody has watched the film with sound

# Guide: For agents

## A loop that works

An agent cannot watch a video, so the loop relies on checks and stills.

1. Write the film as a TypeScript file.
2. Run `vizmatic check --json` and fix what it reports.
3. Render stills with `--frame` at the moments that matter, and look at them.
4. Export the MP4 when the stills are right.

```bash
npx vizmatic check intro.ts --json
npx vizmatic film intro.ts --frame 2,6,12 --width 1280 --out ./stills
npx vizmatic film intro.ts --quality high --out ./out
```

The check inspects moments of rest. It misses a crossfade where two sentences overlap and a blank gap between two pictures. Add `--motion` to inspect every frame, and judge each warning that it reports. Then extract frames from the MP4 around each change of picture, and read them.

## Films that teach

A lesson needs more than a clean check. [The lesson reference](../references/lessons.md) covers the plan, computed numbers, narration, changes of picture, a frame scan, delivery, and review. An agent cannot judge a lesson at full speed, so a person must watch it with sound before it is published.

[Finished films](../films.html) show the full source for each topic. The package includes them in `examples/films/`.

The package also computes some mechanisms for you, with helpers such as `weightedVectorState` and `tokenHistoryState`. A film can import them from `vizmatic` for `check` and `film`, but `preview` refuses that import because it runs in the browser. [The canvas guide](tsx-animation.html) covers the same constructions for a checked canvas host.

## Install the skill

The portable skill teaches an agent the workflow and the API. See the [agent page](../agents.html) for plugin installs.

```bash
npx skills add bvolpato/vizmatic --skill vizmatic -g -y
```

## Machine-readable docs

| File | Contents |
|---|---|
| [llms.txt](../llms.txt) | A short index with canonical links. |
| [llms-full.txt](../llms-full.txt) | The prompt and the whole component catalog. |
| [components.json](../components.json) | Component metadata, source, and builder links. |
| [PROMPT.md](../PROMPT.md) | The full task prompt and API guidance. |
| [references/films.md](../references/films.md) | Engine rules, patterns, check codes, and common mistakes. |
| [references/lessons.md](../references/lessons.md) | How to plan, narrate, scan, and deliver a film that teaches. |

`vizmatic objects --json` and `vizmatic looks --json` list what is available in the installed version.

## Why the source suits an agent

- A film is one function, with no framework state to hold in mind.
- Layout helpers such as `nextTo`, `alignTo`, and `arrange` replace hand-tuned coordinates.
- Color names come from the look, so an agent does not choose hex values.
- `checkFilm` is also a function, so a test can fail when a label leaves the frame.

```ts
import { checkFilm, film, text } from 'vizmatic/stage'

const draft = film((s) => {
    s.add(text('A title that is much too wide for this frame', { size: 300 }))
    s.wait(1)
})

export async function report(): Promise<string[]> {
    const result = await checkFilm(draft)
    return result.diagnostics.map((item) => `${item.code} at ${item.time.toFixed(1)} s: ${item.message}`)
}
```

# Guide: Animations

Each animation takes optional `duration` and `ease` settings. Without them it uses a length that suits it and the `smooth` ease.

A `duration` on `s.play` stretches every animation in that call to fill it, and it overrides a `duration` set on any of them. To give animations different lengths, leave it off `s.play`.

## Reveal

### draw

`draw` traces strokes in order. Filled shapes outline first and then fill.

```film paired label="A circle and an arrow draw themselves"
import { arrow, circle, draw, film } from 'vizmatic/stage'

export default film({ fps: 30 }, (s) => {
    const ring = circle(300, { stroke: 'blue', width: 10 }).at(-300, 0)
    const pointer = arrow([100, 220], [750, -220], { stroke: 'yellow', width: 12, tip: 60 })
    s.play(draw(ring), draw(pointer), { duration: 1.6 })
    s.wait(1)
})
```

### write

`write` handwrites text and equations. Each glyph is outlined and then filled, with a small lag between glyphs.

```film paired label="An equation handwrites itself"
import { film, tex, write } from 'vizmatic/stage'

export default film({ fps: 30 }, (s) => {
    const eq = s.add(tex('e^{i\\pi} + 1 = 0', { size: 190 }))
    s.play(write(eq, { duration: 2 }))
    s.wait(1)
})
```

### fadeIn and fadeOut

Fades can also travel with `shift` and change size with `scale`. With `lag`, a group fades in child by child.

```film paired label="A word rises into place, holds, and sinks away"
import { fadeIn, fadeOut, film, text } from 'vizmatic/stage'

export default film({ fps: 30 }, (s) => {
    const word = text('arrive', { font: 'text-serif', size: 280 })
    s.play(fadeIn(word, { shift: [0, 90], lag: 0.1, duration: 1.2 }))
    s.wait(0.6)
    s.play(fadeOut(word, { shift: [0, -90] }))
})
```

### grow

`grow` scales up from nothing. Pass `from` to grow out of a point, such as the tail of an arrow.

```film paired label="An arrow grows out of its tail point"
import { arrow, dot, film, grow } from 'vizmatic/stage'

export default film({ fps: 30 }, (s) => {
    s.add(dot(22, 'ink').at(-600, 200))
    const pointer = arrow([-600, 200], [600, -200], { stroke: 'teal', width: 18, tip: 80 })
    s.play(grow(pointer, { from: [-600, 200], duration: 1.2 }))
    s.wait(1)
})
```

### erase

`erase` is the reverse of `draw`.

```film paired label="A wave draws and then erases along its length"
import { draw, erase, film, parametric } from 'vizmatic/stage'

export default film({ fps: 30 }, (s) => {
    const wave = parametric((u) => [-800 + 1600 * u, Math.sin(u * 14) * 220], 0, 1, { stroke: 'pink', width: 10 })
    s.play(draw(wave))
    s.wait(0.4)
    s.play(erase(wave))
})
```

`appear` and `vanish` show or hide a node with no transition.

## Change

### to

`to` animates any change. Make the change inside the callback, and the engine interpolates from the old state to the new one.

```film paired label="A dot moves, changes color, and grows in one motion"
import { dot, film, to } from 'vizmatic/stage'

export default film({ fps: 30 }, (s) => {
    const d = s.add(dot(70, 'blue').at(-550, 150))
    s.play(to(d, (n) => n.at(550, -150).color('yellow').scale(2.4), { duration: 1.5 }))
    s.wait(0.8)
})
```

### moveTo, shift, scaleBy, rotate, recolor, fade

These are short forms of `to` for one kind of change.

```film paired label="A square moves, turns, grows, and changes color in order"
import { film, moveTo, recolor, rect, rotate, scaleBy, seq } from 'vizmatic/stage'

export default film({ fps: 30 }, (s) => {
    const box = s.add(rect(240, 240, { fill: 'teal', stroke: null, radius: 12 }).at(-600, 0))
    s.play(seq(
        moveTo(box, [0, 0]),
        rotate(box, Math.PI / 2),
        scaleBy(box, 1.8),
        recolor(box, 'orange'),
    ))
    s.wait(0.6)
})
```

### along

`along` follows a path or the shape of another node. Set `orient` to turn the node along the way.

```film paired label="A triangle follows a figure-eight path and turns with it"
import { along, film, parametric, regularPolygon } from 'vizmatic/stage'

export default film({ fps: 30 }, (s) => {
    const road = s.add(parametric((t) => [Math.sin(t) * 700, Math.sin(t * 2) * 320], 0, Math.PI * 2, { stroke: 'grid', width: 6 }))
    const rider = s.add(regularPolygon(3, 48, { fill: 'yellow', stroke: null }).rotate(Math.PI / 2))
    s.play(along(rider, road, { orient: true, duration: 4 }))
})
```

### morph

`morph` turns one shape into another. Afterwards the target is on stage and the source is hidden, so later code always refers to the node you can see.

```film paired label="A square morphs into a circle and then into a letter"
import { circle, film, morph, rect, text } from 'vizmatic/stage'

export default film({ fps: 30 }, (s) => {
    const a = s.add(rect(520, 520, { fill: 'blue', stroke: null }))
    const b = circle(290, { fill: 'teal', stroke: null })
    const c = text('v', { font: 'text-serif', size: 900, color: 'yellow' })
    s.play(morph(a, b))
    s.wait(0.3)
    s.play(morph(b, c))
    s.wait(0.6)
})
```

### morphMatching

`morphMatching` morphs equations and labels by meaning. Glyphs on both sides travel to their new places, and the rest fade where they stand.

```film paired label="An equation rearranges as its terms travel"
import { film, morphMatching, tex, write } from 'vizmatic/stage'

export default film({ fps: 30 }, (s) => {
    const a = s.add(tex('a^2 + b^2 = c^2', { size: 150 }))
    s.play(write(a))
    const b = tex('a^2 = c^2 - b^2', { size: 150 })
    s.play(morphMatching(a, b, { duration: 1.6 }))
    s.wait(0.8)
})
```

### warp and applyMatrix

`warp` deforms shapes through any function of stage position. `applyMatrix` is the linear case.

```film paired label="A square grid shears under a matrix"
import { applyMatrix, film, plane } from 'vizmatic/stage'

export default film({ fps: 30 }, (s) => {
    const grid = s.add(plane({ unit: 200 }))
    s.wait(0.3)
    s.play(applyMatrix(grid, [[1, 1], [0, 1]], { duration: 2 }))
    s.wait(0.8)
})
```

```film paired label="A square grid bends under a smooth function of position"
import { film, plane, warp } from 'vizmatic/stage'

export default film({ fps: 30 }, (s) => {
    const grid = s.add(plane({ unit: 180 }))
    s.play(warp(grid, (x, y) => [x + Math.sin(y / 170) * 120, y + Math.sin(x / 170) * 120], { duration: 2.4 }))
    s.wait(0.8)
})
```

## Emphasis

These leave a node as they found it.

### indicate

```film paired label="One term of an equation swells and turns yellow, then returns"
import { film, indicate, tex } from 'vizmatic/stage'

export default film({ fps: 30 }, (s) => {
    const eq = s.add(tex('E = m c^2', { size: 190 }))
    s.wait(0.3)
    s.play(indicate(eq.part('c^2'), { scale: 1.3 }))
    s.wait(0.6)
})
```

### shake

```film paired label="A warning word shakes"
import { film, shake, text } from 'vizmatic/stage'

export default film({ fps: 30 }, (s) => {
    const word = s.add(text('careful', { font: 'text-serif', size: 300, color: 'red' }))
    s.wait(0.3)
    s.play(shake(word, { angle: 0.12 }))
    s.wait(0.6)
})
```

### circumscribe

```film paired label="A lit line traces around a word and runs off"
import { circumscribe, film, text } from 'vizmatic/stage'

export default film({ fps: 30 }, (s) => {
    const word = s.add(text('this one', { font: 'text-serif', size: 260 }))
    s.wait(0.2)
    s.play(circumscribe(word, { pad: 40, width: 7 }))
    s.wait(0.4)
})
```

### flash and ripple

```film paired label="A burst of rays and an expanding ring at a point"
import { dot, film, flash, ripple } from 'vizmatic/stage'

export default film({ fps: 30 }, (s) => {
    s.add(dot(30, 'yellow'))
    s.play(flash([0, 0], { radius: 320, rays: 14 }))
    s.play(ripple([0, 0], { radius: 460, width: 8 }))
    s.wait(0.3)
})
```

### pulse

`pulse` sends a short lit segment along a line: a packet on an edge, or a signal on a wire.

```film paired label="A lit pulse travels down a curved line"
import { film, parametric, pulse } from 'vizmatic/stage'

export default film({ fps: 30 }, (s) => {
    const wire = s.add(parametric((u) => [-800 + 1600 * u, Math.sin(u * 7) * 200], 0, 1, { stroke: 'grid', width: 10 }))
    s.play(pulse(wire, { width: 16, glow: 20, duration: 1.6 }))
    s.wait(0.2)
})
```

## Composition

### par, seq, and lagged

`par` runs animations together. `seq` runs them in order. `lagged` starts each one after a fraction of the previous one has played.

```film paired label="A row of dots fades in as a wave from left to right"
import { dot, fadeIn, film, lagged } from 'vizmatic/stage'

export default film({ fps: 30 }, (s) => {
    const dots = Array.from({ length: 13 }, (_, i) => dot(30, 'teal').at(-720 + i * 120, Math.sin(i * 0.8) * 180))
    s.play(lagged(dots.map((d) => fadeIn(d, { scale: 0.1 })), { lag: 0.25 }), { duration: 2 })
    s.wait(0.8)
})
```

### delay, retime, and call

`delay` waits before an animation begins. `retime` changes the length or ease of an animation without changing what it does. `call` runs a function at a moment on the timeline.

`delay` gives a clean change from one sentence to the next. The old text fades before the new text can be read, so the two never overlap. Inside a `s.play` that has a `duration`, `seq(fadeOut(oldText), fadeIn(newText))` does the same.

```ts
import { delay, fadeIn, fadeOut, film, text } from 'vizmatic/stage'

export default film((s) => {
    const oldText = s.add(text('The old sentence', { size: 72 }))
    s.wait(1)
    const newText = text('The new sentence', { size: 72 })
    s.play(fadeOut(oldText, { duration: 0.15 }), delay(fadeIn(newText, { duration: 0.25 }), 0.1))
    s.wait(1)
})
```

### Value.to

Animate a number. Anything that reads it follows.

```film paired label="A readout counts from zero to one hundred"
import { decimal, film } from 'vizmatic/stage'

export default film({ fps: 30 }, (s) => {
    const n = s.value(0)
    s.add(decimal(() => n.value, { digits: 1, size: 220, color: 'yellow' }))
    s.play(n.to(100, { duration: 2.4 }))
    s.wait(0.6)
})
```

## Easing

Pass an `ease` to any animation. Names such as `smooth`, `smoother`, `easeOutCubic`, `rushInto`, `rushFrom`, and `bounce` are functions you import, and `overshoot()`, `spring()`, `wiggle()`, `cubicBezier()`, and `during()` build new ones.

```film paired label="Three dots travel with different easing"
import { bounce, dot, film, linear, moveTo, spring } from 'vizmatic/stage'

export default film({ fps: 30 }, (s) => {
    const row = (y: number, color: string) => s.add(dot(40, color).at(-650, y))
    const a = row(-220, 'blue'), b = row(0, 'yellow'), c = row(220, 'pink')
    s.play(
        moveTo(a, [650, -220], { ease: linear }),
        moveTo(b, [650, 0], { ease: spring() }),
        moveTo(c, [650, 220], { ease: bounce }),
        { duration: 2 },
    )
    s.wait(0.6)
})
```

# Guide: API reference

<div data-api>@@API@@</div>

# Guide: Camera

## Move and zoom

`s.camera` is a node. Its position is the point at the center of the frame. Its `sx` property is the zoom.

```film paired label="The camera zooms into one dot, then pulls back"
import { dot, film, Node, seq, to } from 'vizmatic/stage'

export default film({ fps: 30 }, (s) => {
    for (let i = 0; i < 40; i++) {
        s.add(dot(14, i % 7 ? 'dim' : 'yellow').at(-760 + (i % 8) * 216, -360 + Math.floor(i / 8) * 180))
    }
    const look = (x: number, y: number, zoom: number) =>
        to(s.camera, (c: Node) => c.at(x, y).set('sx', zoom), { duration: 1.4 })
    s.play(seq(look(-112, -180, 4), look(0, 0, 1)))
})
```

## Keep titles in place

A pinned node ignores the camera. Use `.pin()` for titles, captions, and readouts that stay on the screen.

```film paired label="A grid slides under a pinned title"
import { film, Node, plane, text, to } from 'vizmatic/stage'

export default film({ fps: 30 }, (s) => {
    s.add(plane({ unit: 160 }))
    s.add(text('This title is pinned.', { font: 'text-serif', size: 120 }).at(0, -380).pin())
    s.play(to(s.camera, (c: Node) => c.at(700, 300).set('sx', 1.6), { duration: 2 }))
    s.wait(0.5)
})
```

## Turn the view

Set `rotation` on the camera to turn the view.

```film paired label="The view rotates a quarter turn around the center"
import { film, Node, plane, to } from 'vizmatic/stage'

export default film({ fps: 30 }, (s) => {
    s.add(plane({ unit: 200 }))
    s.play(to(s.camera, (c: Node) => c.set('rotation', Math.PI / 2), { duration: 2 }))
    s.wait(0.5)
})
```

Stroke widths scale with the zoom, as if the camera moved closer. For a line that keeps its screen width at every zoom, use a pinned node or set the width from the zoom in a `dynamic` drawing.

# Guide: CLI

## Films

### vizmatic new

Create a film from a starter that renders as written and passes the check.

```bash
npx vizmatic new intro --template explainer
```

The templates are `explainer`, `equation`, `architecture`, `chart`, and `deck`.

### vizmatic preview

Open a live preview with a timeline. The page reloads when you save.

```bash
npx vizmatic preview intro.ts --open
```

### vizmatic check

Inspect a film without rendering video. The check samples the timeline and reports marks off the canvas, overlapping text, and lines that cross text. Each finding has a time and a position. Use `--json` to read the report in a script.

A film whose lines run past the frame on purpose, such as a warped grid, sets `bleed: true` in its options. The check then ignores lines and shapes past the edge, and it still reports text that runs off the canvas or sits against an edge.

```bash
npx vizmatic check intro.ts --look midnight,paper --json
```

The check looks where the film comes to rest, so it misses a problem that lasts only while something moves. `--motion` also inspects every frame. It reports a problem that appears only in motion as a warning, because it cannot tell a collision from one picture that replaces another. It reports 0.2 seconds or more with nothing visible as `film.blank`. It reports text that leaves before a viewer can read it as `film.text_brief`: a reader takes in 20 characters in one second, and a text needs half a second at least. `--reading-speed <n>` changes the speed, and 0 turns the rule off. `--stepped` is for a deck, which waits at each chapter: text that is up there is not reported. In a film, `readingSeconds(text)` from `vizmatic/stage` gives the time that a plain text needs. `--content-top <px>` sets the canvas y where the content starts, so that a heading that stays up does not count as content. `--text-margin <px>` sets how near text may come to an edge of the frame before the check reports `film.text_edge`. The default is 2% of the shorter side, and 0 turns the rule off. While the camera is zoomed, the check looks only at pinned text, because a zoom crops the rest on purpose.

```bash
npx vizmatic check intro.ts --motion --content-top 200
```

### vizmatic film

Export a film. See [Exporting](exporting.md) for the options.

```bash
npx vizmatic film intro.ts --out dist/films --look midnight,paper --quality high
```

| Flag | Meaning |
|---|---|
| `--out <dir>` | The output folder. |
| `--look a,b` | Render in these looks. |
| `--format mp4,webm,gif,png,svg` | The output formats. |
| `--quality draft\|standard\|high\|master` | Encoder quality and motion blur. |
| `--fps <n>` | Frame rate. |
| `--width <n>` | Output width in pixels. |
| `--motion-blur <n>` | Sub-frames per frame. |
| `--shutter <0-1>` | The fraction of a frame the shutter stays open. |
| `--range a:b` | Render part of the timeline. |
| `--frame t,t` | Write stills at these times. |
| `--every <seconds>` | Write stills at this interval, across the film or its `--range`. |
| `--sheet` | Join the stills into contact sheets, each tile with its time. With no times, a sheet shows 12 moments spread over the film. |
| `--columns <n>` | Tiles in a row of a contact sheet. |
| `--poster` | Also write a poster frame. |
| `--workers <n>` | Rasterizer threads. |
| `--audio <file>` | Add a sound track. |
| `--background <color\|none>` | Replace the background. |
| `--ffmpeg <path>` | Use this FFmpeg. |
| `--json` | Print a report as JSON. |

### vizmatic looks and vizmatic objects

List the looks and the object library. Use `--json` in scripts and agents.

```bash
npx vizmatic looks --json
npx vizmatic objects --json
```

## Figures

Render TSX figures to PNG and SVG.

```bash
npx vizmatic ./frame.tsx --out ./figures --theme dark,light --format both
npx vizmatic check ./frame.tsx --theme dark,light --json
npx vizmatic gif ./animated-frame.tsx --out ./figures --theme dark,light --fps 20
```

Run `npx vizmatic --help` to see every option.

# Guide: Core concepts

## The stage

The stage is a coordinate space. The origin is the center of the frame, and y points down. The default stage is 1920 by 1080 units. You always author in these units, and the export width only changes how many pixels each unit gets.

```film paired label="A dot moves to each corner of the stage in turn"
import { dot, film, moveTo, seq } from 'vizmatic/stage'

export default film({ fps: 30 }, (s) => {
    const d = s.add(dot(40, 'yellow'))
    const corner = (x: number, y: number) => moveTo(d, [x, y], { duration: 0.6 })
    s.play(seq(
        corner(800, -400), corner(800, 400),
        corner(-800, 400), corner(-800, -400),
        corner(0, 0),
    ))
})
```

The stage also gives you `s.left`, `s.right`, `s.top`, and `s.bottom`, which are the edges in stage units.

## Nodes

Everything visible is a `Node`. A node with a path draws that path. A node with children groups them. Setter methods chain, so setup reads as one line.

```ts
import { circle, group, text } from 'vizmatic/stage'

const ring = circle(120, { stroke: 'blue', width: 4 }).glow(8)
const label = text('radius', { size: 36, color: 'dim' }).nextTo(ring, 'down', 24)
export const figure = group(ring, label).at(-300, 0)
```

Text and equations are also nodes. Each glyph is a child, so you can animate one letter or one term of an equation.

## The timeline

`s.play(...)` does not wait. It records keys on the timeline and moves the cursor forward, and your function finishes almost at once. Animations passed to one `s.play` run together. Use `seq` and `lagged` to arrange them in time.

```film paired label="Three squares fade in one after another while a label writes"
import { fadeIn, film, lagged, rect, text, write } from 'vizmatic/stage'

export default film({ fps: 30 }, (s) => {
    const boxes = [-400, 0, 400].map((x) =>
        rect(260, 260, { fill: 'teal', stroke: null, radius: 16 }).at(x, -60))
    const label = text('one play, two animations', { size: 72 }).at(0, 240)

    s.play(lagged(boxes.map((box) => fadeIn(box, { shift: [0, 40] }))), write(label))
    s.wait(1)
})
```

Because the function finishes before the first frame, code after a `play` sees the end state. This is why `eq.nextTo(graph)` is correct even when `graph` is still animating on screen.

## Why frames are pure

When the function returns, the film is compiled. A frame is then only a function of time: `film.frame(t)`. Nothing replays and nothing accumulates.

This gives you some useful properties:

- Seeking is immediate in both directions. On the [home page](../index.html), your scroll bar sets the time.
- Export can run frames on many threads, in any order, and encode them in sequence.
- Output is deterministic. The same source gives the same frames.

## Values and dynamic drawings

A `Value` is a number on the timeline. A `dynamic` node rebuilds its drawing on every frame from whatever it reads. Together they let geometry follow a number, as a secant follows its increment.

```film paired label="A sine wave whose frequency rises as a readout counts up"
import { axes, decimal, dynamic, film, Node } from 'vizmatic/stage'

export default film({ fps: 30 }, (s) => {
    const k = s.value(1)
    const graph = s.add(axes({ x: [0, 6.3], y: [-1.5, 1.5], width: 1300, height: 560, labels: false }).at(0, 60))
    s.add(dynamic(() => new Node(graph.plotPath((x) => Math.sin(k.value * x)))
        .stroke('yellow', 6).at(graph.get('x'), graph.get('y'))))
    s.add(decimal(() => k.value, { prefix: 'k = ', size: 72 }).at(0, -380))

    s.play(k.to(5, { duration: 3 }))
    s.play(k.to(1, { duration: 2 }))
})
```

A drawing inside `dynamic` should read values and return nodes. It should not change anything else, because it can run for any time in any order.

## Updaters

`s.update(node, fn)` runs a function on each frame and can set properties on a node that is already on stage. Use it when one node should follow another, without rebuilding geometry.

```ts
import { dot, film, moveTo, text } from 'vizmatic/stage'

export default film((s) => {
    const a = s.add(dot(20, 'yellow').at(-500, 0))
    const label = s.add(text('here', { size: 48 }))
    s.update(label, (node) => node.nextTo(a, 'up', 24))
    s.play(moveTo(a, [500, 0], { duration: 2 }))
})
```

## Assets load themselves

Fonts and the TeX engine load the first time a film asks for them. The compiler catches the request, waits, and then runs your function again. This is why a build function should have no side effects other than authoring on the stage.

# Guide: Cookbook

## A point that follows a curve

Drive a `dynamic` drawing from a `Value`. The dot and its guides stay true to the curve at any time.

```film paired label="A dot travels along a curve with a readout"
import { axes, draw, dot, dynamic, film, line } from 'vizmatic/stage'

export default film({ fps: 30 }, (s) => {
    const graph = s.add(axes({ x: [0, 6.3], y: [-1.4, 1.4], width: 1400, height: 700, labels: false }))
    const curve = graph.plot(Math.sin, { stroke: 'blue', width: 7 })
    s.play(draw(graph.lines), draw(curve), { duration: 1 })
    const x = s.value(0)
    s.add(dynamic(() => {
        const p = graph.world(x.value, Math.sin(x.value))
        return [
            line(graph.world(x.value, 0), p, { stroke: 'dim', width: 3, dash: [10, 12] }),
            dot(20, 'yellow').glow(20).at(p),
        ]
    }))
    s.play(x.to(6.28, { duration: 3 }))
})
```

## Trace a moving point

`traced` keeps the path of any point over a span of time.

```film paired label="A spirograph traces itself"
import { dot, dynamic, film, traced, type Vec } from 'vizmatic/stage'

export default film({ fps: 30 }, (s) => {
    const t = s.value(0)
    const point = (): Vec => {
        const a = t.value
        return [Math.cos(a) * 300 + Math.cos(a * 5.1) * 150, Math.sin(a) * 300 + Math.sin(a * 5.1) * 150]
    }
    s.add(traced(point, 0, 5, { color: 'teal', width: 4 }))
    s.add(dynamic(() => dot(16, 'yellow').at(point())))
    s.play(t.to(Math.PI * 6, { duration: 5, ease: (u) => u }))
})
```

## A signal through a system

Lines, labels, and pulses make a small architecture film. Connect the boxes first, and then send packets along the edges.

```film paired label="A request pulses through three services"
import { arrow, draw, fadeIn, film, lagged, pulse, rect, seq, text, group } from 'vizmatic/stage'

export default film({ fps: 30 }, (s) => {
    const box = (label: string, x: number) => group(
        rect(380, 200, { stroke: 'blue', width: 4, radius: 12 }),
        text(label, { size: 64 }).at(0, 0),
    ).at(x, 0)
    const boxes = [box('edge', -620), box('api', 0), box('db', 620)]
    const links = [arrow([-420, 0], [-200, 0], { stroke: 'dim', width: 6 }), arrow([200, 0], [420, 0], { stroke: 'dim', width: 6 })]
    s.play(lagged(boxes.map((b) => fadeIn(b, { shift: [0, 24] }))), { duration: 1 })
    s.play(...links.map((link) => draw(link)))
    const shaft = (i: number) => links[i]!.children[0]!
    s.play(seq(pulse(shaft(0), { duration: 0.7 }), pulse(shaft(1), { duration: 0.7 })))
    s.play(seq(pulse(shaft(1), { duration: 0.7 }), pulse(shaft(0), { duration: 0.7 })))
})
```

## Reveal a chart

Bars and lines are nodes, so a chart can arrive in order and call out one value.

```film paired label="Bars grow and one is highlighted"
import { barChart, film, grow, indicate, lagged } from 'vizmatic/stage'

export default film({ fps: 30 }, (s) => {
    const chart = s.add(barChart([
        { label: 'Mon', value: 3 }, { label: 'Tue', value: 5 }, { label: 'Wed', value: 9 }, { label: 'Thu', value: 4 },
    ], { width: 1200, height: 720 }))
    s.play(lagged(chart.bars.map((bar) => grow(bar, { from: bar.bottom })), { lag: 0.25 }), { duration: 1.4 })
    s.play(indicate(chart.bars[2]!, { color: 'yellow', scale: 1.05 }))
    s.wait(0.5)
})
```

## Build a logo sting

A glyph outline is a path, so a mark can draw itself and settle.

```film paired label="The letter v draws itself and a dot lands beside it"
import { dot, draw, fadeIn, film, overshoot, text } from 'vizmatic/stage'

export default film({ fps: 30 }, (s) => {
    const mark = s.add(text('v', { font: 'text-serif', size: 620 }))
    s.play(draw(mark, { duration: 1.4 }))
    const point = s.add(dot(36, 'yellow').glow(24).at(190, -160))
    s.play(fadeIn(point, { scale: 0.05, ease: overshoot() }))
    s.wait(0.6)
})
```

## Time things to a voiceover

Put a label at a moment with `s.at`, so one section does not push others around.

```ts
import { fadeIn, film, text } from 'vizmatic/stage'

export default film((s) => {
    const cue = (time: number, words: string, y: number) => {
        s.at(time, () => {
            s.play(fadeIn(s.add(text(words, { size: 72 }).at(0, y))))
        })
    }
    cue(0.5, 'First sentence.', -100)
    cue(3.2, 'Second sentence.', 40)
    s.wait(6)
})
```

`--audio narration.wav` pads or trims the sound to the film's length, so a film that ends early cuts off the voice. Make the film at least as long as the narration.

# Guide: Diagrams and density

## Two engines, one package

Films use the stage engine, which is vector animation from TypeScript functions. Figures use the component library, which lays out TSX elements and renders PNG, SVG, and GIF.

Use a figure when the reader studies the picture. Use a film when the change over time is the point.

```bash
npx vizmatic ./architecture.tsx --out ./figures --theme dark,light --format both
```

The [component explorer](../components.html) shows every component with live controls, and the [diagram builder](../playground.html) arranges them on a canvas.

## Titles are optional

A scene takes a title and a subtitle, and both are optional. Leave them out when the surrounding page already names the figure, and add a title when the visual needs them.

## A rich node

Graph nodes can carry more than a title. A node can have key and value rows, metrics with small trend lines, tags, an owner, and a footer. A reader then gets the facts from the figure without a second document.

```tsx
import { getThemeColors, GraphDiagram, Scene, type GraphDiagramEdge, type GraphDiagramNode, type ThemeMode } from 'vizmatic'

const nodes: GraphDiagramNode[] = [
    {
        id: 'api', label: 'Checkout API', detail: 'Go, 3 regions', icon: 'server', tone: 'blue', status: 'ok',
        rows: [{ key: 'replicas', value: '48' }, { key: 'deploy', value: 'blue/green' }],
        metrics: [{ label: 'p95', value: '184 ms' }, { label: 'errors', value: '0.02%' }],
    },
    { id: 'db', label: 'Orders', detail: 'Postgres 16', icon: 'database', tone: 'green' },
]
const edges: GraphDiagramEdge[] = [{ from: 'api', to: 'db', label: 'SQL, 2.1k/s' }]

export const width = 960
export const height = 420

export function create(theme: ThemeMode = 'dark') {
    const c = getThemeColors(theme)
    return (
        <Scene c={c} title="Checkout">
            <GraphDiagram c={c} nodes={nodes} edges={edges} width={900} height={320} />
        </Scene>
    )
}
```

## Database schemas

`EntityRelationshipDiagram` draws entity tables with attribute types, primary and foreign keys, uniqueness, nullability, and crow-foot endpoints. Each endpoint states how many records of that entity can match one record at the opposite end: `one`, `zero-or-one`, `one-or-many`, or `zero-or-many`.

```tsx
import { EntityRelationshipDiagram, getThemeColors, type ThemeMode } from 'vizmatic'

export const width = 960
export const height = 420

export function create(theme: ThemeMode = 'dark') {
    return <EntityRelationshipDiagram c={getThemeColors(theme)} width={960} height={420}
        title="Customer orders"
        entities={[
            { id: 'customers', attributes: [
                { name: 'id', type: 'uuid', primaryKey: true },
                { name: 'email', type: 'text', unique: true },
            ] },
            { id: 'orders', attributes: [
                { name: 'id', type: 'uuid', primaryKey: true },
                { name: 'customer_id', type: 'uuid', foreignKey: true },
            ] },
        ]}
        relationships={[{
            id: 'customer-orders', label: 'places',
            from: { entity: 'customers', attribute: 'id', cardinality: 'one' },
            to: { entity: 'orders', attribute: 'customer_id', cardinality: 'zero-or-many' },
        }]} />
}
```

This relationship gives each order exactly one customer; a customer may have no orders or many orders. For guest orders, mark `customer_id` nullable and use `zero-or-one` at the customer endpoint. Mark each primary-key attribute for a composite key, and pass an ordered attribute array at each relationship endpoint to show a composite relationship. Set `identifying` when the parent key forms part of the child primary key; the relationship then uses a solid line.

The component checks duplicate names, endpoint references, composite arity, nullable primary keys, and canvas dimensions. Tables expand for long names instead of reducing text size. Exported SVGs retain an accessible schema summary, entity constraints, and relationship cardinalities. The [checkout example](../examples.html#example-entity-relationship) includes guest orders and a composite line-item key.

## Accessible chart exports

Charts keep their titles and data summaries in exported SVGs. Use `title` to name the chart and `description` to explain its takeaway or context. The generated summary also retains category values, series, coordinates, or intervals. A labeled sparkline can provide the same metadata; an unlabeled sparkline stays decorative beside its surrounding metric.

## Density

Every component reads its type, spacing, strokes, and corner radius from one set of tokens. One source can render as a dense engineering figure or as a slide.

| Density | Use |
|---|---|
| `dense` | The default. Small type at 10 to 12 px, hairlines, and tight gutters. |
| `compact` | A little more air, for docs and dashboards. |
| `comfortable` | The sizes the components were first designed at. |
| `presentation` | Large type and strokes, for a room. |
| a number | A direct scale. `0.9` is slightly dense, and `1.25` is large. |

Set it on a scene.

```tsx
import { CalloutCard, getThemeColors, Scene, type ThemeMode } from 'vizmatic'

export function create(theme: ThemeMode = 'dark') {
    const c = getThemeColors(theme)
    return (
        <Scene c={c} title="Same content, larger" density="presentation">
            <CalloutCard c={c} title="Density is one prop" detail="Nothing else in the source changes." />
        </Scene>
    )
}
```

Or set it for a whole render.

```ts
import { renderToPng } from 'vizmatic'
import type { ReactElement } from 'react'

export async function renderBoth(figure: ReactElement): Promise<void> {
    await renderToPng(figure, { width: 1400, height: 800, outputPath: 'figure.png', density: 'dense' })
    await renderToPng(figure, { width: 1400, height: 800, outputPath: 'slide.png', density: 'presentation' })
}
```

## Check before you publish

`vizmatic check` reports overflow, contrast, small text, overlapping labels, and connector congestion. It works in both themes and can write JSON.

```bash
npx vizmatic check ./architecture.tsx --theme dark,light --json
```

## Keep the source

A figure is a TSX file. Keep it beside the document it illustrates, review it in a pull request, and render it in CI. Dark and light come from the same file.

# Guide: Editable scenes

Use `vizmatic-scene` when an application stores editable TSX from a user or an
agent. The regular `vizmatic` CLI loads trusted Node modules; it is not the entry
point for running editable source inside an application.

```bash
vizmatic-scene --input examples/pressure-mechanism.scene.json --output pressure.mp4
```

The JSON request contains `source`, `width`, `height`, `duration`, optional `fps`
(24 by default), `theme`, `cues`, `seed`, and optional preview `time`. All times
are **seconds**. Output is PNG for an exact preview time or a silent H.264 MP4.
FFmpeg must be on PATH for MP4. Narration, captions and final assembly remain the
calling application's responsibility.

```typescript
import { renderSceneFrame, renderSceneVideo } from 'vizmatic'

const scene = {
    width: 1920, height: 1080, duration: 12, fps: 24, theme: 'dark' as const,
    cues: { reveal: 3.2 }, seed: 0,
    source: `const amount = ease(cue('reveal', 2));
<svg width={width} height={height}>
    <rect width={width} height={height} fill={c.bg}/>
    <circle cx={lerp(300, 1600, amount)} cy={540} r={70} fill={c.info}/>
    <text x={150} y={200} fontSize={64} fill={c.textPrimary}>Follow the mechanism</text>
</svg>`,
}
await renderSceneFrame({ ...scene, time: 4, outputPath: './preview.png' })
await renderSceneVideo({ ...scene, outputPath: './scene.mp4' })
```

For editor previews, add `outputScale: 0.5` to either renderer and
`previewFps: 12` to `renderSceneVideo`. The renderer still evaluates and lays out
the original logical `width`, `height`, `duration`, `fps`, `frame`, and cues; it
only rasterizes fewer pixels and samples fewer video frames. `outputScale` must
be from 0.25 to 1 and produce even integer dimensions. `previewFps` must be an
integer from 1 to the scene's logical fps. These options also work in the
`vizmatic-scene` JSON request (`previewFps` is MP4-only).

Preview metadata reports effective `width`, `height`, `fps`, and `frameCount`,
plus `logicalWidth`, `logicalHeight`, `logicalFps`, `logicalFrameCount`,
`outputScale`, and, for video, `previewFps`. Its input hash binds the effective
output settings. Omitting the options preserves full-quality rendering and its
existing input hash.

Source uses local JavaScript/TypeScript declarations and helper components followed
by one JSX root. Imports, exports and module wrappers are not supported. Vizmatic
JSX primitives are provided automatically. SVG geometry and `<text>`/`<tspan>` use
the bundled fonts through Resvg, including inside HTML layouts. `Scene`, `Board`,
`CanvasItem`, charts and other trusted Vizmatic components use the same rendering
path in previews and video frames. Function-valued props are unsupported; calculate
formatted values and geometry in the source before returning JSX.

| Source value | Meaning |
| --- | --- |
| `width`, `height` | Logical canvas dimensions before optional preview scaling |
| `time` / `t`, `duration` | Current position and narration duration in seconds |
| `progress`, `frame`, `fps` | Normalized progress, zero-based frame and cadence |
| `theme`, `c` | Theme name and frozen Vizmatic theme colors |
| `cues` | Named narration cue positions in seconds |
| `seed`, `Math.random()` | Explicit uint32 seed and deterministic random sequence |
| `clamp(x, min=0, max=1)`, `lerp(a,b,p)` | Clamp and linear interpolation |
| `ease(p)`, `easeInOut(p)`, `easeOut(p)` | Clamped smoothstep and cubic ease-out |
| `cue(name, span=1)` | Progress from a named cue over a span in seconds |

A missing named cue fails preparation rather than inventing a timing. Applications
should resolve production cues against actual narration and label estimated preview
cues as estimates. Source can be evaluated without Node rendering through
`evaluateSceneSource` or `prepareScene` from `vizmatic/scene-runtime`, suitable for
a browser Worker. The same evaluator produces a validated JSON scene graph in both
environments.

### Realtime browser previews

Use `vizmatic/scene-browser` in a dedicated module Worker for local editing,
scrubbing and playback. It uses the same bounded QuickJS evaluator, fonts, trusted
components and Resvg rasterizer as the Node scene renderer. Both native SVG and
Vizmatic layouts are supported. Compile once per source revision and render each
new time locally; a full MP4 or HTTP request is not required for playback.

```typescript
// Inside a dedicated Worker. The application fetches these assets from its own origin.
import { createBrowserSceneRenderer } from 'vizmatic/scene-browser'

const renderer = await createBrowserSceneRenderer({
    quickjsWasm, resvgWasm,
    layoutWasm: () => loadLayoutWasm(),
    fonts: { interRegular, interSemiBold, interBold, jetBrainsMono, notoSans, notoSansMath },
    emoji: () => loadOfflineEmoji(), // Optional, loaded only for an emoji glyph.
})
const prepared = await renderer.prepare(scene, { outputScale: 0.5 })
const frame = await prepared.render(4)
postMessage(frame, [frame.pixels.buffer])
```

The three WASM inputs and six font inputs are `ArrayBuffer`s. `layoutWasm` also
accepts a loader, so native SVG previews never load Satori's chunk or layout WASM.
Their hashes are checked before initialization, and initialization is reused. The
optional emoji loader returns the parsed pinned emoji JSON; it is checked and
cached on first use. Satori's standalone build embeds its HarfBuzz runtime.

At application build time, `getSceneBrowserAssetPaths()` from
`vizmatic/browser-assets` resolves `quickjsWasm`, `layoutWasm`, `resvgWasm`,
`fonts`, and `emojiBrotli` to installed local files. Copy them to your static assets;
decompress the emoji Brotli file at build time if the browser does not support
Brotli streams. The browser entry is prebundled ESM and requires no Node shims or
special file loaders. Keep it out of the initial application bundle and load its
Worker when a preview becomes visible. Keep ESM code splitting enabled in your
application's Worker build to preserve the deferred Satori download.

Frames contain owned, transferable RGBA `pixels`, effective `width` and `height`,
`time`, source and input hashes, runtime version, and `timings` in milliseconds.
Draw them with `ImageData` on a canvas. A single unchanged graph retains one backing
raster while each caller receives an independent pixel buffer. Logical scene
dimensions, fps, cues and frame globals remain unchanged by `outputScale`.

Keep only the latest queued frame, stop playback when hidden, and terminate the
Worker on an application deadline or when its editor closes. QuickJS limits author
execution; the Worker deadline also bounds trusted layout and rasterization. Keep
narration alignment and final export validation in the application's server path.
`pnpm check:scene-browser` checks real Chromium Worker rendering against Node pixels,
isolation, error recovery and playback without network requests.

Editable JavaScript executes inside QuickJS WebAssembly with no host objects,
callbacks, filesystem, network, browser storage or module loader. Each frame gets
a fresh context, a 64 MiB guest memory limit, a 256 KiB stack and a 250 ms evaluation
deadline. Only bounded JSON reaches trusted components. External image references,
resource URLs, event handlers, raw HTML and executable props are rejected. The
renderer uses fingerprinted package fonts and emoji assets, with no environment
overrides, system fonts or network fallback. Local SVG fragment references work.

Limits are 128 KiB of source, 2 MiB of serialized graph, 2,000 JSX elements, 64
levels of nesting, at most 1,920 pixels per edge and 2,073,600 pixels per canvas,
120 seconds per scene, 1–60 fps and 3,600 frames. Nested SVGs also share a per-frame
allocation budget. Run the CLI in a subprocess with a bounded environment, captured
stderr, memory limit and total deadline; the QuickJS deadline governs author code,
while the caller's process deadline also covers trusted layout and FFmpeg.

The CLI writes one JSON metadata line on success. `runtimeVersion`,
`sourceSha256`, `inputSha256`, `assetSha256`, `fontAssetsSha256`, dimensions,
duration, fps, timeline `frameCount`, output byte count and output path identify the
result. `assetSha256` hashes the actual output file. Preview metadata adds `time`;
MP4 adds `encodedDuration`. Cache inputs include the runtime, exact source, cues,
seed, theme, dimensions, duration, cadence, pinned assets, output kind and preview
time. Like the existing video API, MP4 duration rounds to the nearest frame.
Existing output files survive a failed render.

# Guide: Embedding the player

## The element

`<vz-film>` plays a compiled film. Importing `vizmatic/stage/browser` registers the element. Bundle your film with it, and then add the element to your page.

```html
<script type="module" src="./stage.js"></script>
<vz-film src="./my-film.js" autoplay loop label="A curve becomes its tangent"></vz-film>
```

The element compiles the film when it scrolls within reach, pauses when it leaves the screen, and honors `prefers-reduced-motion` by showing the final frame.

Your film and the engine must share one module, because that module holds the timeline clock. A bundler with code splitting does this for you.

## Attributes

| Attribute | Meaning |
|---|---|
| `src` | The URL of a module whose default export is a film. |
| `autoplay` | Play when visible. |
| `loop` | Start again at the end. |
| `stepped` | Stop at each chapter. See [Presentations](presentations.md). |
| `nocontrols` | Hide the controls. |
| `start` | Start at this time in seconds. |
| `look` | Compile in this look, such as `paper`. |
| `background` | Replace the look's background, so the film sits directly on your page. |
| `label` | The text alternative for assistive technology. Always set this. |

## Events

| Event | Detail |
|---|---|
| `ready` | `{ film, player }`, once the film is compiled. |
| `timeupdate` | `{ time }`, on every painted frame. |
| `error` | `{ error }`, if the film fails to load. The element also shows the message. |

## Drive it from your page

`element.controller` is a `FilmPlayer`. Its `seek` method is cheap enough to call on every scroll event. This is how the home page works.

```ts
export function scrubWithScroll(section: HTMLElement, element: HTMLElement & { controller: { seek(time: number): void; film: { duration: number } } | null }): void {
    window.addEventListener('scroll', () => {
        const box = section.getBoundingClientRect()
        const amount = Math.min(1, Math.max(0, -box.top / (box.height - innerHeight)))
        const player = element.controller
        if (player) player.seek(amount * player.film.duration)
    }, { passive: true })
}
```

## Without the element

Use `FilmPlayer` on your own canvas when you want your own controls.

```ts
import { compileFilm, FilmPlayer, film, text, write } from 'vizmatic/stage/browser'

const intro = film((s) => {
    s.play(write(s.add(text('Hello', { font: 'text-serif', size: 200 }))))
})

export async function mount(canvas: HTMLCanvasElement): Promise<FilmPlayer> {
    return new FilmPlayer(canvas, await compileFilm(intro), { autoplay: true, loop: true })
}
```

## Take input

Because a frame is computed when it is needed, a film can read from your page. Put your state in plain variables, read them in a `dynamic` node, and call `player.seek(player.time)` when they change. The [matrix laboratory](../index.html#lab) on the home page works this way.

## Fonts

In the browser, the engine fetches TrueType fonts from an `assets/fonts` folder beside its module. Call `setAssetBase(url)` if they live elsewhere.

# Guide: Exporting

## From the command line

```bash
npx vizmatic film my-film.ts --out ./out
npx vizmatic film my-film.ts --format mp4,webm,gif --quality high
npx vizmatic film my-film.ts --look midnight,paper --width 3840
```

MP4 export needs FFmpeg with `libx264`. WebM also needs `libvpx-vp9`. The CLI writes a manifest with sizes and timings beside the files.

## Quality

| Quality | Use | H.264 CRF | Motion blur |
|---|---|---|---|
| `draft` | Checking timing. | 26 | off |
| `standard` | The default. | 18 | off |
| `high` | Publishing. | 15 | 4 samples |
| `master` | Archiving and re-encoding. | 11 | 8 samples |

Motion blur evaluates every sampled time and averages real sub-frames across the shutter. `--motion-blur` and `--shutter` set these directly.

MP4 files stay at H.264 level 4.2 at 1080p60, so they play in QuickTime, Quick Look, and on Apple devices. To join another clip to a film without encoding it again, encode the clip with `filmEncoderSettings(quality)` from `vizmatic/stage/export`. QuickTime reads one set of decoder settings from the start of a file, and it stops at a clip that uses different ones.

## Resolution

A film is authored in stage units, so `--width` changes only the pixel count. MP4 and WebM need even dimensions, so the export rounds an odd width or height up to the next even number.

## Stills

`--frame` writes PNG files at the given times. `--poster` also writes the fullest moment at rest, or the time you set with `film({ poster: 12 })`.

```bash
npx vizmatic film my-film.ts --frame 2.5,7 --width 1280
```

`--every` samples the film at an interval, in place of a list of times. `--sheet` joins the stills into one PNG, a contact sheet, with the time under each tile. With `--sheet`, `--width` is the width of one tile and defaults to 480. A sheet has up to 6 rows, and a longer list of times continues on the next sheet.

```bash
npx vizmatic film my-film.ts --every 5 --sheet --columns 4
npx vizmatic film my-film.ts --range 30:60 --every 1 --sheet
```

In code, `renderFilmSheet(film, times, path, { width, columns })` from `vizmatic/stage/export` writes one sheet.

## SVG

A frame is a list of vector paths, so it exports as standalone SVG with no fonts needed.

```bash
npx vizmatic film my-film.ts --format svg --frame 7
```

## Part of a film

`--range` renders part of the timeline, which helps while you adjust one section.

```bash
npx vizmatic film my-film.ts --range 12:18 --quality draft
```

## Sound

`--audio` adds a track. The audio is padded or trimmed to the film's length. An MP4 gets stereo AAC at 48 kHz and 384 kbit/s, which is what YouTube asks for in an upload. FFmpeg must include AAC.

```bash
npx vizmatic film my-film.ts --audio narration.wav
```

Captions from `s.caption` export as a WebVTT file beside the video.

## From code

```ts
import { compileFilm, film, text, write } from 'vizmatic/stage'
import { renderFilmFrame, renderFilmVideo } from 'vizmatic/stage/export'

const intro = film((s) => {
    s.play(write(s.add(text('Hello', { font: 'text-serif', size: 200 }))))
})

export async function render(): Promise<void> {
    const compiled = await compileFilm(intro, { look: 'paper' })
    await renderFilmVideo(compiled, 'out/intro.mp4', { quality: 'high' })
    await renderFilmFrame(compiled, compiled.duration, 'out/intro.png', { width: 1280 })
}
```

## Bloom

Bloom is part of a look, and export applies it after rasterizing. Pass `bloom` on the film to change it. Set it to `0` for line art that needs exact colors.

## Speed

Each frame compiles to SVG on the main thread and rasterizes on a worker. The exporter chooses the thread count from your CPU count and frame size. Use `--workers` to change it. See the [benchmarks](../benchmarks.html) for measured numbers from the component renderer.

Each worker can reuse its last completed RGBA result when every SVG sample, dimension, bloom setting, and background value is identical. This applies to generated frames with prepared, embedded image assets. Raw SVG jobs and unresolved image references always rasterize again. Updaters and every shutter sample still run before this comparison.

The worker retains one private result, capped at 64 MiB of pixels and an 8 MiB serialized-key allowance. Returned buffers are separate copies. Render planning includes this retained memory when choosing workers.

# Guide: Figure recipes

## Full frame example

Create `frames/agent-pipeline.tsx`:

```jsx
width = 1040;
height = 560;

<Scene
  title="Agent visual pipeline"
  subtitle="prompt -> TSX scene -> render -> files"
  gap={26}
>
  <Flow
    connectorTone="purple"
    stages={[
      {
        eyebrow: "input",
        title: "Prompt",
        subtitle: "intent and constraints",
        tone: "blue",
        lines: ["domain language", "audience", "theme tokens"],
        width: 190,
      },
      {
        eyebrow: "contract",
        title: "Scene spec",
        subtitle: "typed structure",
        tone: "purple",
        lines: ["cards", "flows", "charts", "tables"],
        width: 190,
      },
      {
        eyebrow: "render",
        title: "Node renderer",
        subtitle: "headless output",
        tone: "cyan",
        lines: ["layout", "theme", "checks"],
        width: 190,
      },
      {
        eyebrow: "output",
        title: "PNG / SVG / GIF",
        subtitle: "rendered files",
        tone: "green",
        lines: ["dark + light", "alpha", "manifest"],
        width: 190,
      },
    ]}
  />
  <Row width="100%" gap={14}>
    <CalloutCard
      title="Use scene primitives"
      detail="Common diagrams do not need hand-written SVG paths or x/y coordinates."
      tone="purple"
      width={470}
    />
    <CalloutCard
      title="Use theme tokens"
      detail="Tokens set color, typography, radius, spacing, and contrast."
      tone="green"
      width={470}
    />
  </Row>
</Scene>
```

Render it:

```bash
vizmatic frames --out public/vizmatic --theme dark,light --watermark "Your Product" --watermark-image ./logo.svg --watermark-position top-right
```

Generated `manifest.json` includes each output file plus its theme, path, width, and height.

The static CLI defaults to PNG. Add `--format svg` for vector output or `--format both` to render PNG and SVG together in every requested theme.

PNG and SVG renders have transparent backgrounds by default. Use the theme background when the image needs an opaque canvas:

```bash
vizmatic frames --out public/vizmatic --theme dark,light --background theme
```

Or set it in the frame:

```jsx
<Scene background={c.bg} title="Opaque article figure">
  <Flow stages={[{ title: "Theme fill", tone: "purple" }]} />
</Scene>
```

CSS alpha colors are preserved in PNG/SVG backgrounds. Use `<Scene background="rgba(15, 23, 42, 0.5)">` or `--background 'rgba(15, 23, 42, 0.5)'` for a semi-transparent canvas. Leave the background unset for fully transparent output. Contrast checks composite alpha text and nested backgrounds over known solid surfaces; a transparent canvas has no known host-page backdrop.

When a CLI frame omits `width` or `height`, Vizmatic starts at `960x540` and grows that axis if content overflows. Set `autoSize = false` when dimensions must remain fixed and clipping should fail the render.

Bare CLI frames do not need imports, `defineIllustration`, or `c` props. Use `c` for explicit tokens such as `background={c.bg}`. `Scene` title and subtitle are optional. Use a full module for other dependencies, reusable helpers, animation exports, or direct renderer APIs.

Frame modules can export a watermark element instead of using CLI flags:

```jsx
import { Watermark } from "vizmatic"

export const watermark = (
  <Watermark position="bottom-right" opacity={0.82}>
    <div style={{
      display: "flex",
      alignItems: "center",
      gap: 6,
      color: "#7c3aed",
      fontFamily: "Inter",
      fontWeight: 800,
      fontSize: 12,
    }}>
      <img src="data:image/svg+xml;base64,..." width={14} height={14} />
      Your Product
    </div>
  </Watermark>
)
```

Render a sampled state timeline with `createAnimation(theme)`:

```bash
vizmatic gif frames/animated-pipeline.tsx --out public/vizmatic --theme dark,light --fps 20 --watermark "Your Product" --watermark-image ./logo.svg --watermark-position top-right --scale 1
```

## Animated frames

An animated module exports `createAnimation(theme)` for GIF or MP4 output and keeps `create(theme)` for a static or reduced-motion fallback. Define state once, then describe holds, eased transitions, instant keyframes, and parallel property tracks. Vizmatic samples absolute timeline time and renders every frame.

For MP4, export `createAnimation(theme)` and run `vizmatic video frames/lesson.tsx --out public/videos --theme dark --fps 30`. This requires Node.js 20 or newer and FFmpeg with the `libx264` encoder; add `--audio narration.wav` for AAC audio. The CLI pads or trims audio to the animation duration. Exported canvas dimensions after `--scale` must be positive even integers. MP4 uses an opaque theme background by default and supports 1–120 fps independently of GIF cadence. The browser builder exports GIF; MP4 runs in Node.js. Keep `.vtt` or `.srt` captions as separate files and synchronize them to the finished timeline. Use `examples/videos/request-lifecycle.tsx` as a short, generic starting point and `examples/videos/signal-to-story.tsx` for a longer film with parallel tracks, path tracing, camera motion, and an ending held on screen. Animate the data or mechanism the lesson explains; avoid decorative logo or icon reveals. For package consumers, install Vizmatic and React in the project and FFmpeg separately. In the repository, `pnpm render:video` regenerates the showcase media.

```jsx
import React from "react"
import { Scene, defineAnimation, getThemeColors, hold, keyframe, tween, type ThemeMode } from "vizmatic"

export const width = 1040
export const height = 560

type State = { operation: number; progress: number }

function buildFrame(theme: ThemeMode, state: State) {
  const c = getThemeColors(theme)
  return (
    <Scene c={c} title={state.operation === 0 ? "Broadcast" : "AllReduce"}>
      <div style={{ display: "flex", width: "100%", height: 260, position: "relative" }}>
        <div style={{
          display: "flex",
          position: "absolute",
          left: 40 + state.progress * 720,
          top: 90,
          padding: "10px 16px",
          borderRadius: 8,
          background: c.accent,
          color: c.textOnColor,
        }}>chunk</div>
      </div>
    </Scene>
  )
}

export function createAnimation(theme: ThemeMode) {
  return defineAnimation<State>({
    initial: { operation: 0, progress: 0 },
    timeline: [
      hold(500, "Broadcast input"),
      tween({ progress: 1 }, { duration: 900, easing: "ease-in-out", label: "Broadcast" }),
      hold(500, "Broadcast output"),
      keyframe({ operation: 1, progress: 0 }),
      tween({ progress: 1 }, { duration: 900, easing: "ease-in-out", label: "AllReduce" }),
    ],
    fps: 20,
    render: (state) => buildFrame(theme, state),
  })
}

export function create(theme: ThemeMode = "dark") {
  return buildFrame(theme, { operation: 1, progress: 1 })
}

export default create("dark")
```

Top-level timeline steps run sequentially. Properties inside one `tween` move together. Use `parallel({ x: [...], opacity: [...] })` when property tracks need different delays or durations. Default interpolation supports finite numbers; use a custom `interpolate(from, to, progress)` for colors, points, or other values. Infinite loops should finish on the same visual state where they start. Fade moving content out and reset it with a zero-duration `keyframe` while hidden instead of reversing a one-way operation. `createScenes(theme)` remains supported for deliberate scene cuts and pixel `fade`/`appear` transitions.

## Choose primitives

Use Vizmatic primitives for common layouts. Use raw SVG only for geometry the component set cannot express.

Shared values:

- `ThemeMode`: `"dark" | "light"`.
- `ToneName`: `"blue" | "purple" | "green" | "warm" | "cyan" | "pink" | "red" | "critical" | "neutral" | "sunset" | "ocean" | "dark"`.
- `ColorName`: `"primary" | "secondary" | "positive" | "warning" | "critical" | "info" | "accent" | "neutral"`.
- `FlexAlign`: `"start" | "center" | "end" | "stretch"`.
- `FlexJustify`: `"start" | "center" | "end" | "space-between" | "space-around"`.
- Bare CLI frames omit `c` props. The local `c` object remains available for explicit values such as `background={c.bg}`.
- Full modules pass the `c: ThemeColors` value from `defineIllustration((c) => ...)` to visual components.
- PNG/SVG renders are alpha-transparent by default. Use `--background theme`, `background: "theme"`, or `<Scene background={c.bg}>` for opaque theme fill.
- Omitted CLI dimensions auto-grow on overflow. Explicit `width` / `height` remain fixed and fail on clipping.

## Component API

Frame helpers:

- `defineIllustration(build, defaultTheme?)`: `build: (c: ThemeColors) => ReactElement`, `defaultTheme?: ThemeMode = "dark"`. Returns `{ create(theme), default }`.
- `Canvas`: `children?`, `c`, `padding?: number = 40`, `justify?: "center" | "flex-start" | "space-between" | "space-around" = "center"`, `background?: string`. Default root background is alpha-transparent during PNG/SVG rendering.
- `Board`: `children?`, `c`, `width?: number = 960`, `height?: number = 540`, `background?: string`. Fixed relative canvas with a transparent default background; clips content outside its bounds. Match frame dimensions to the board. Dimensions must be finite, greater than zero, and at most 8192.
- `CanvasItem`: `children?`, `x: number`, `y: number`, `width?: number`, `height?: number`. Absolute position in logical board pixels. Coordinates must be finite. Explicit dimensions must be greater than zero and at most 8192; they supply the child's width/height props. With both dimensions set, chart primitives scale proportionally to fit their complete plotting layer, headings, legends, and footer inside the item. Use literal coordinates and dimensions for preview move/resize controls.
- `Scene`: `c`, `children`, `title?`, `subtitle?`, `padding?: number = 40`, `gap?: number = 24`, `justify?: "center" | "flex-start" | "space-between" | "space-around" = "flex-start"`, `align?: FlexAlign = "stretch"`, `contentWidth?: number | string = "100%"`, `background?`, `contentStyle?: React.CSSProperties`. Title/subtitle are optional and the title bar is omitted when no title is provided. Use `background={c.bg}` for an opaque theme frame.
- `TitleBar`: `title`, `subtitle?`, `c`.

Layout primitives:

- `Row` / `Column`: `children`, `gap?: number = 12`, `align?: FlexAlign = "center"`, `justify?: FlexJustify = "center"`, `wrap?: boolean = false`, `width?: number | string`, `height?: number | string`, `style?: React.CSSProperties`.
- `Stack`: `children`, `direction?: "vertical" | "horizontal" = "vertical"`, `gap?: number = 12`, `align?: "start" | "center" | "end" | "stretch" = "center"`, `wrap?: boolean = false`.
- `Panel`: `title`, `c`, `children?`, `subtitle?`, `tone?: ToneName = "blue"`, `footer?`, `width?`, `minWidth?`, `height?`, `minHeight?`, `padding?: number | string = 14`, `radius?: number = 8`, `gap?: number = 10`, `align?: FlexAlign = "stretch"`, `justify?: FlexJustify = "start"`, `shadow?: boolean = true`, `background?`, `borderColor?`, `titleFontSize?`, `accentWidth?: number = 34`, `accentHeight?: number = 4`, `bodyStyle?: React.CSSProperties`.
- `Card`: `children?`, `c`, `title?`, `subtitle?`, `tone?`, `footer?`, `width?`, `minWidth?`, `height?`, `minHeight?`, `padding?: number | string = 20`, `radius?: number = 8`, `gap?`, `align?: FlexAlign = "stretch"`, `justify?: FlexJustify = "start"`, `shadow?: boolean = false`, `background?`, `borderColor?`, `bodyStyle?: React.CSSProperties`.
- `WindowFrame`: `c`, `children?`, `title?`, `variant?: "window" | "browser" | "terminal" = "window"`, `tone?: ToneName = "blue"`, `dots?: boolean = true`, `width?`, `minWidth?`, `height?`, `minHeight?`, `padding?: number | string = 16`, `radius?: number = 10`, `shadow?: boolean = true`, `background?`, `bodyStyle?: React.CSSProperties`.

Cards, labels, and compact UI:

- `TextLabel`: `text`, `c`, `variant?: keyof typography = "body"`, `color?`, `fontSize?`, `fontWeight?`, `width?`, `align?: "left" | "center" | "right" = "left"`, `math?: boolean = false`, `mono?: boolean = false`.
- `MathText`: `text`. Returns formatted unicode-ish math text for simple `_` and `^` notation.
- `formatMathText(text)`: `text: string`.
- `Icon`: `name`, `c`, `tone?: ToneName = "blue"`, `color?`, `size?: number = 24`, `strokeWidth?: number = 2`, `label?`, `muted?: boolean = false`. `IconName` includes presentation icons plus architecture icons: `browser`, `bucket`, `cache`, `cloud`, `cluster`, `container`, `database`, `firewall`, `gateway`, `load-balancer`, `mobile`, `monitor`, `network`, `queue`, `server`, `storage`, `stream`, and `user`.
- `ToneStrip`: `tone`, `width?: number = 34`, `height?: number = 4`.
- `StepCard`: `title`, `c`, `subtitle?`, `eyebrow?`, `tone?: ToneName = "blue"`, `width?: number | string = 210`, `minWidth?`, `minHeight?: number | string = 74`, `padding?: number | string = "12px 14px"`, `radius?: number = 8`, `shadow?: boolean = true`, `align?: "left" | "center" = "center"`, `math?: boolean = false`.
- `MetricCard`: `label`, `value`, `c`, `tone?: ToneName = "blue"`, `detail?`, `width?`, `minWidth?`, `minHeight?: number | string = 74`, `padding?: number | string = "10px 12px"`, `radius?: number = 8`, `shadow?: boolean = true`, `align?: "left" | "center" = "center"`, `math?: boolean = false`, `valueMono?: boolean = true`, `valueFontSize?: number = 22`, `valueColor?`.
- `CalloutCard`: `c`, `children?`, `title?`, `detail?`, `tone?: ToneName = "blue"`, `width?`, `minHeight?`, `padding?: number | string = 12`, `titleFontSize?: number`, `detailFontSize?: number`, `filled?: boolean = true`, `align?: "left" | "center" = "center"`.
- `ValuePill`: `label`, `value`, `c`, `tone?: ToneName = "blue"`, `detail?`, `width?: number | string = 112`, `math?: boolean = false`.
- `BadgePill`: `text`, `c`, `tone?: ToneName = "blue"`, `width?`, `minWidth?: number | string = 34`, `height?: number | string = 20`, `padding?: number | string = "0 8px"`, `radius?: number = 6`, `fontSize?: number = 11`, `mono?: boolean = true`, `filled?: boolean = false`, `math?: boolean = false`.
- `Badge`: `label`, `c`, `color?: ColorName = "primary"`.
- `GradientChip`: `title`, `c`, `subtitle?`, `tone?: ToneName = "blue"`, `gradient?`, `width?`, `minWidth?`, `height?`, `minHeight?: number | string = 78`, `padding?: number | string = "0 18px"`, `radius?: number = 8`, `shadow?: boolean = true`, `align?: "left" | "center" = "left"`, `subtitleMono?: boolean = true`, `math?: boolean = false`.
- `EquationCard`: `title`, `formula`, `c`, `tone?: ToneName = "purple"`, `result?`, `detail?`, `width?: number | string = 190`, `height?`, `math?: boolean = false`, `align?: "left" | "center" = "left"`.
- `DetailList`: `items`, `c`, `tone?`, `gap?: number = 7`, `padding?: number | string = "7px 8px"`, `fontSize?: number = 11`, `mono?: boolean = false`, `math?: boolean = false`.

Lists, tables, status, and comparison:

- `ProgressRow`: `label`, `value: number`, `valueLabel?`, `tone?: ToneName = "blue"`, `muted?: boolean = false`, `c`, `labelWidth?: number = 42`, `valueWidth?: number = 38`, `barHeight?: number = 8`, `fontSize?: number = 11`. Keep `value` in `[0, 1]`.
- `ProgressList`: `rows: ProgressRowSpec[]`, `c`, `gap?: number = 8`, `labelWidth?`, `valueWidth?`, `barHeight?`, `fontSize?`. `ProgressRowSpec`: `label`, `value`, `valueLabel?`, `tone?`, `muted?`.
- `StatusRow`: `label`, `detail?`, `status?: "check" | "cross" | "warn" | "info" | "pending" | "dot" = "check"`, `tone?`, `c`, `boxed?: boolean = true`, `fontSize?: number = 11`, `width?`, `math?: boolean = false`.
- `StatusList`: `rows: StatusRowSpec[]`, `c`, `gap?: number = 7`, `boxed?: boolean = true`, `fontSize?`, `width?`, `math?: boolean = false`. `StatusRowSpec`: `label`, `detail?`, `status?`, `tone?`.
- `Timeline`: `events: TimelineEventSpec[]`, `c`, `title?`, `subtitle?`, `direction?: "vertical" | "horizontal" = "vertical"`, `width?`, `eventWidth?`, `gap?: number = 12`, `markerSize?: number = 14`, `math?: boolean = false`. `TimelineEventSpec`: `title`, `detail?`, `time?`, `tone?`, `status?`, `width?`.
- `KeyValueList`: `rows: KeyValueRow[]`, `c`, `title?`, `width?`, `minWidth?`, `keyWidth?`, `gap?: number = 0`, `fontSize?: number = 11`, `divider?: boolean = true`, `keyMono?: boolean = false`, `math?: boolean = false`. `KeyValueRow`: `key`, `value`, `tone?`, `valueMono?`.
- `Comparison`: `sides: ComparisonSideSpec[]`, `c`, `divider?: ReactNode | boolean = false`, `gap?: number = 14`, `sideWidth?`, `minHeight?`, `align?: FlexAlign = "stretch"`, `math?: boolean = false`. `ComparisonSideSpec`: `title`, `subtitle?`, `eyebrow?`, `tone?`, `lines?`, `children?`, `footer?`, `width?`.
- `DataTable`: `rows: ReactNode[][]`, `c`, `cellWidth?: number = 54`, `firstColWidth?: number = cellWidth`, `cellHeight?: number = 28`, `gap?: number = 5`, `headerRows?: number = 1`, `headerCols?: number = 1`, `fontSize?: number = 11`, `math?: boolean = false`.
- `Grid`: `rows: Array<Array<ReactNode | GridCell>>`, `c`, `cellWidth?: number = 34`, `cellHeight?: number = 30`, `gap?: number = 5`, `radius?: number = 7`, `fontSize?: number = 11`, `math?: boolean = false`, `headerRows?: number = 0`, `headerCols?: number = 0`. `GridCell`: `label?`, `tone?`, `color?`, `backgroundColor?`, `borderColor?`, `opacity?`.
- `CodeBlock`: `lines: Array<ReactNode | CodeLineSpec>`, `c`, `title?`, `tone?`, `width?`, `minWidth?`, `fontSize?: number = 12`, `showLineNumbers?: boolean = false`, `padding?: number | string = 14`, `radius?: number = 10`, `background?`, `shadow?: boolean = false`, `math?: boolean = false`. `CodeLineSpec`: `text`, `tone?`, `dim?`, `prefix?`.

Tiles:

- `Tile`: combines `TileSpec` with display props. `TileSpec`: `title`, `subtitle?`, `eyebrow?`, `icon?`, `tone?`, `lines?`, `children?`, `width?`, `minHeight?`. Display props: `c`, `align?: "left" | "center" = "center"`, `padding?: number | string = "14px 16px"`, `radius?: number = 10`, `shadow?: boolean = true`, `math?: boolean = false`.
- `TileGrid`: `tiles: TileSpec[]`, `c`, `columns?`, `gap?: number = 12`, `tileWidth?`, `minHeight?`, `align?: "left" | "center" = "center"`, `math?: boolean = false`.

Flows and process diagrams:

- `Flow`: `stages: FlowStageSpec[]`, `c`, `direction?: "horizontal" | "vertical" = "horizontal"`, `gap?: number = 10`, `connectorLength?: number = 28`, `connectorTone?: ToneName = "neutral"`, `align?: FlexAlign = "center"`, `math?: boolean = false`. `FlowStageSpec`: `title`, `subtitle?`, `eyebrow?`, `tone?`, `lines?`, `children?`, `width?`, `minWidth?`, `minHeight?`, `padding?`.
- `FlowArrow`: `direction?: "down" | "right" | "up" | "left" = "right"`, `label?`, `length?: number = 40`, `c`, `tone?: ToneName = "blue"`, `color?`.
- `Connector`: same props as `FlowArrow`.
- `Pipeline`: `stages: PipelineStage[]`, `c`, `title?`. `PipelineStage`: `label`, `sublabel?`, `icon?`, `color?: ColorName`.

Graphs and networks:

- `LayeredNetwork`: `c`, `layers: LayeredNetworkLayer[]`, `activePath?: number[] = []`, `annotations?: string[] = []`, `formula?`, `legend?: string = "highlighted path"`, `width?: number = 900`, `height?: number = 400`, `nodeSize?: number = 56`, `showFormula?: boolean = true`. `LayeredNetworkLayer`: `title`, `nodes: string[]`, `tone?`.
- `GraphDiagram` routing options: `routing?: "dagre" | "straight" | "orthogonal" = "dagre"`, `routingClearance?: number = 12`. Dagre preserves existing routes; straight connects facing node sides; orthogonal avoids node boxes and group headers with right-angle paths. Node layout remains automatic or manual independently of routing. `GraphDiagramNode` additionally accepts `ports?: GraphDiagramPort[]`; a port is `{ id: string, side: "top" | "right" | "bottom" | "left", offset?: number = 0.5 }`. Offset is finite and in `[0, 1]`, running left to right or top to bottom. `GraphDiagramEdge` additionally accepts `fromPort?`, `toPort?`, `fromSide?`, and `toSide?`. An endpoint cannot combine a port reference with a side. Explicit anchors opt an edge into orthogonal geometry even in Dagre mode. Missing references, duplicate port IDs, and invalid offsets fail validation.

- Orthogonal routing separates parallel relationships, supports self-loops with distinct endpoint anchors, and uses `edgeGap` for lane spacing. It runs offline with limits of 64 node/header obstacles, 256 edges, 18,000 grid points per search, 54,000 search states per search, and 160,000 search states per diagram. A blocked path, unavailable separate lane, same-anchor self-loop, or exhausted limit omits that connector and emits a checker error with code `layout.connector_unroutable`. Increase canvas size or spacing, choose different sides/ports, or reduce clearance; do not publish missing connectors. See `examples/orthogonal-routing.tsx`.

- `GraphDiagram`: `nodes: GraphDiagramNode[]`, `edges: GraphDiagramEdge[]`, `groups?: GraphDiagramGroup[]`, `c`, `width?: number = 520`, `height?: number = 420`, `nodeWidth?: number = 150`, `nodeHeight?: number = 66`, `labelFontSize?: number = 14`, `detailFontSize?: number = 11`, `arrowSize?: number = 5`, `padding?: number = 28`, `layout?: "auto" | "manual"`, `direction?: "LR" | "RL" | "TB" | "BT" = "LR"`, `nodeGap?: number = 34`, `rankGap?: number = 64`, `edgeGap?: number = 14`, `sizing?: "content" | "fixed" = "content"`, `iconSize?: number = 20`, `id?`, `ariaLabel?`, `title?`, `description?`. `GraphDiagramNode`: `id`, `label`, `detail?`, `href?`, `title?`, `x?`, `y?`, `tone?`, `muted?`, `width?`, `height?`, `group?`, `icon?: IconName | DiagramIconDefinition | ReactElement`, `iconSize?`. Icon size is clamped inside its node; custom React elements own their theme colors and accessible label. `GraphDiagramGroup`: `id`, `label`, `detail?`, `parent?`, `tone?`, `muted?`. Groups nest through `parent` and require automatic layout. Omit coordinates on every node for automatic layout; array order is the stable tie-breaker and dimensions expand to fit unless `sizing="fixed"`. Set both coordinates on every node for manual normalized positioning when no groups are present. Mixed coordinate modes fail. `GraphDiagramEdge`: `id?`, `from`, `to`, `tone?`, `muted?`, `dashed?`, `label?`, `href?`, `title?`, `kind?: "sync" | "async" | "event" | "data" | "dependency" | "association"`, `style?: "solid" | "dashed" | "dotted"`, `arrow?: "forward" | "backward" | "both" | "none"`. Association edges default to no arrow. `title`, `description`, and `id` add structured SVG metadata and deterministic graph, node, and edge IDs. Nodes and edges can link to HTTP(S), `mailto:`, `tel:`, fragment, or relative URLs; unsupported schemes fail. Use these properties for `renderToSvg` and playground SVG downloads.
- `SequenceDiagram`: `participants`, ordered `items`, `c`, and optional `width`, `height`, spacing, title, subtitle, and `ariaLabel`. Participants have `id`, `label`, optional `detail`, `tone`, `kind`, and `icon`. Items are messages (`from`, `to`, `label`, `kind?: "sync" | "async" | "return"`), notes, activations, or recursive fragments with `kind: "alt" | "loop" | "parallel"` and labeled branches.
- `DataflowDiagram`: `nodes`, `edges`, optional nested `boundaries`, `c`, GraphDiagram sizing/layout props including `id`, `title`, and `description`, and `ariaLabel`. Nodes require `id`, `label`, and `kind: "source" | "transform" | "store" | "sink"`; optional schema metadata renders in node detail, with optional `href` and `title` for SVG links and tooltips. Edges use `mode?: "batch" | "stream"`, optional schema/label metadata, and optional `id`, `href`, and `title`.
- `DeploymentDiagram`: `nodes`, `connections`, optional nested `boundaries`, `c`, GraphDiagram sizing/layout props including `id`, `title`, and `description`, and `ariaLabel`. Boundaries distinguish region, zone, VPC, subnet, namespace, host, cluster, and trust scopes. Nodes declare infrastructure kind, boundary, and ports, with optional `href` and `title`. Connections declare ingress, egress, internal, or cross-boundary direction plus protocol and port references; `id`, `href`, and `title` add stable SVG identity and links.
- `DeploymentPort`: `id`, `port?`, `protocol?`, `label?`, `direction?: "ingress" | "egress" | "bidirectional"`, `side?: GraphPortSide`, `offset?: number`. With explicit `routing="straight"` or `routing="orthogonal"`, connection `fromPort` / `toPort` references require declared node ports and anchor geometrically. Side defaults follow graph direction and ingress/egress use; explicit side/offset overrides them. Default Dagre deployment rendering keeps the existing label-only behavior.
- `TransformerTopology`: `blocks`, optional `routes`, `c`, `expandRepeats?`, GraphDiagram sizing/layout props including `id`, `title`, and `description`, and `ariaLabel`. Blocks use embedding, attention, norm, MLP, residual, router, output, or custom kinds with tensor shapes and repeat counts, plus optional `href` and `title`. Routes use activation, residual, KV-cache, expert, or collective semantics, plus optional `id`, `href`, and `title`. `formatTensorShape(shape)` formats reusable shape metadata.
- `defineDiagramIcon(id, render)`: defines a reusable custom graph icon. `render` receives `c`, `color`, `size`, and optional `label`.
- `defineIconRegistry(icons)`: preserves typed names for a set of custom or provider icons without global mutable state.
- `TreeDiagram`: `root: TreeNodeSpec`, `c`, `title?`, `subtitle?`, `width?`, `height?`, `nodeWidth?: number = 156`, `nodeHeight?: number = 64`, `levelGap?: number = 58`, `siblingGap?: number = 24`, `padding?: number = 28`, `math?: boolean = false`. `TreeNodeSpec`: `label`, `id?`, `detail?`, `tone?`, `muted?`, `children?`.

Matrices and heatmaps:

- `Matrix`: `data: number[][]`, `c`, `rowLabels?`, `colLabels?`, `labels?`, `title?`, `cellSize?: number = 48`, `cellWidth?`, `cellHeight?`, `rowLabelWidth?`, `format?: "decimal" | "percent" | "integer" = "decimal"`, `colorize?: boolean = true`, `colorScale?: "heat" | "strength" = "heat"`.
- `Heatmap`: `data: number[][]`, `xLabels`, `yLabels`, `c`, `labels?`, `title?`, `cellSize?: number = 52`, `cellWidth?`, `cellHeight?`, `rowLabelWidth?`, `colorScale?: "heat" | "strength"`.
- `TiledMatrix`: `rows`, `cols`, `c`, `regions?: TiledMatrixRegion[] = []`, `title?`, `subtitle?`, `cellSize?: number = 24`, `gap?: number = 3`, `tone?: ToneName = "blue"`, `muted?: boolean = false`, `crossedOut?: boolean = false`. `TiledMatrixRegion`: `rowStart`, `rowEnd`, `colStart`, `colEnd`, `tone?`.

Charts and plots:

- `ChartFrame`: `children`, `c`, `title?`, `subtitle?`, `legend?: ChartLegendItem[]`, `footer?`, `width?: number | string = "100%"`, `height?`, `padding?: number = 16`. `ChartLegendItem`: `label`, `color: ColorName | string`.
- `PieChart`: `segments: DonutChartSegment[]`, `c`, `title?`, `subtitle?`, `width?: number = 360`, `height?: number = 240`, `size?`, `format?: ChartValueFormat = "decimal"`, `showLegend?: boolean = true`, `footer?`. Each segment has `label`, `value`, optional `color`, and optional `valueLabel`. Values must be finite and non-negative; zero slices are omitted. The full-circle pie shows part-to-whole proportions, with values and shares in the legend. Use `examples/pie-chart.tsx` for a complete frame.
- `DonutChart`: `segments: DonutChartSegment[]`, `c`, `title?`, `subtitle?`, `width?: number = 360`, `height?: number = 230`, `size?`, `thickness?`, `format?: ChartValueFormat = "decimal"`, `centerLabel?`, `centerValue?`, `showLegend?: boolean = true`, `footer?`. `DonutChartSegment`: `label`, `value`, `color?`, `valueLabel?`.
- `BarChart`: `data: BarChartDatum[]`, `c`, `title?`, `subtitle?`, `width?: number = 320`, `height?: number = 220`, `min?`, `max?`, `format?: ChartValueFormat = "decimal"`, `showGrid?: boolean = true`, `showValues?: boolean = true`, `yAxisLabel?`, `footer?`. `BarChartDatum`: `label`, `value`, `color?`, `valueLabel?`.
- `ParetoBarChart`: `data: BarChartDatum[]`, `c`, `title?`, `subtitle?`, `width?: number = 520`, `height?: number = 300`, `format?: ChartValueFormat = "integer"`, `showGrid?: boolean = true`, `showValues?: boolean = true`, `footer?`. A classic categorical Pareto chart with descending bars and a cumulative percentage line, separate value/share axes, and stable tie order. Values must be finite and non-negative. Sorting preserves the input array. A positive total ends at 100%; an all-zero total stays at 0%. Use this for the causes behind a total, and `ParetoChart` for a non-dominated scatter frontier. See `examples/pareto-bar-chart.tsx`.
- `LineChart`: `series: LineChartSeries[]`, `c`, `title?`, `subtitle?`, `width?: number = 420`, `height?: number = 240`, `labels?`, `min?`, `max?`, `format?: ChartValueFormat = "decimal"`, `showGrid?: boolean = true`, `showPoints?: boolean = true`, `yAxisLabel?`, `footer?`. `LineChartSeries`: `name`, `points`, `color?`, `area?`.
- `ScatterPlot`: `points: ScatterPoint[]`, `c`, `title?`, `subtitle?`, `width?: number = 380`, `height?: number = 260`, `xMin?`, `xMax?`, `yMin?`, `yMax?`, `xAxisLabel?`, `yAxisLabel?`, `formatX?: ChartValueFormat = "decimal"`, `formatY?: ChartValueFormat = "decimal"`, `showGrid?: boolean = true`, `footer?`. `ScatterPoint`: `x`, `y`, `label?`, `color?`, `size?`.
- `ParetoChart`: `points: ScatterPoint[]`, `c`, `title?`, `subtitle?`, `width?: number = 640`, `height?: number = 360`, `xMin?`, `xMax?`, `yMin?`, `yMax?`, `xAxisLabel?`, `yAxisLabel?`, `formatX?: ChartValueFormat = "decimal"`, `formatY?: ChartValueFormat = "decimal"`, `showGrid?: boolean = true`, `xScale?: "linear" | "log" = "linear"`, `xObjective?: "minimize" | "maximize" = "minimize"`, `yObjective?: "minimize" | "maximize" = "maximize"`, `frontierColor?`, `showGoal?: boolean = true`, `goalLabel?`, `footer?`. It detects non-dominated points automatically; logarithmic x scales omit non-positive points.
- `QuadrantChart`: `points: ScatterPoint[]`, `regions`, `c`, `title?`, `subtitle?`, `width?: number = 700`, `height?: number = 360`, `xMin?`, `xMax?`, `yMin?`, `yMax?`, `xThreshold?`, `yThreshold?`, `xAxisLabel?`, `yAxisLabel?`, `formatX?: ChartValueFormat = "decimal"`, `formatY?: ChartValueFormat = "decimal"`, `showTicks?: boolean = false`, `showGrid?: boolean = false`, `footer?`. Without explicit domains, points use normalized 0-1 coordinates and thresholds default to centered splits. `regions`: `{ topLeft, topRight, bottomLeft, bottomRight }`, each `QuadrantRegion`: `label`, `detail?`, `color?`, `emphasis?`.
- `IntervalPlot`: `data: IntervalDatum[]`, `c`, `title?`, `subtitle?`, `width?: number = 640`, `height?: number = 220`, `min?`, `max?`, `format?: ChartValueFormat = "decimal"`, `axisLabel?`, `footer?`. `IntervalDatum`: `label`, `low`, `mid`, `high`, `color?`, `lowLabel?`, `midLabel?`, `highLabel?`.
- `StackedBar`: `segments: StackedBarSegment[]`, `c`, `width?: number = 480`, `height?: number = 68`, `title?`, `subtitle?`, `showLegend?: boolean = true`. `StackedBarSegment`: `label`, `value`, `color?`, `valueLabel?`.
- `MiniBarChart`: `data: MiniBarDatum[]`, `c`, `max?`, `minBarHeight?: number = 8`, `height?: number = 92`, `barWidth?: number = 22`, `gap?: number = 10`, `radius?: number = 7`, `fontSize?: number = 11`, `showValues?: boolean = false`. `MiniBarDatum`: `label`, `value`, `tone?`, `color?`, `valueLabel?`, `opacity?`.
- `AxisPlot`: `c`, `width`, `height`, `xMin?: number = -1`, `xMax?: number = 1`, `yMin?: number = -1`, `yMax?: number = 1`, `padding?: number = 18`, `showFrame?: boolean = true`, `showAxes?: boolean = true`, `showGrid?: boolean = false`, `gridCount?: number = 5`, `frameRx?: number = 8`, `frameFill?`, `frameStroke?`, `axisColor?`, `points?: AxisPlotPoint[] = []`, `vectors?: AxisPlotVector[] = []`, `paths?: AxisPlotPath[] = []`, `xAxisLabel?`, `yAxisLabel?`, `children?`.
- `AxisPlotPoint`: `x`, `y`, `fill?`, `tone?`, `r?`, `stroke?`, `strokeWidth?`, `opacity?`.
- `AxisPlotVector`: `x1`, `y1`, `x2`, `y2`, `color?`, `tone?`, `strokeWidth?`, `opacity?`, `arrow?`, `arrowSize?`, `showStartDot?`, `showEndDot?`, `dotRadius?`.
- `AxisPlotPath`: `points: Array<{ x, y }>`, `color?`, `tone?`, `strokeWidth?`, `opacity?`, `interpolation?: "linear" | "step-after"`, `dashed?`.
- `ChartValueFormat`: `"decimal" | "percent" | "integer" | "compact" | ((value: number) => string)`.

SVG and geometry helpers:

- `SvgFrame`: `key?`, `x?: number = 0`, `y?: number = 0`, `width`, `height`, `c`, `rx?: number = 8`, `fill?`, `stroke?`, `strokeWidth?: number = 1`, `opacity?`.
- `SvgPoint`: `key?`, `cx`, `cy`, `r?: number = 5`, `fill`, `stroke?`, `strokeWidth?`, `opacity?`.
- `SvgMathText`: positioned HTML overlay with `text`, `x`, `y`, `fill`, `fontSize?: number = 13`, `fontFamily?: string = "JetBrains Mono"`, `fontWeight?: number = 800`, `textAnchor?: "start" | "middle" | "end" = "middle"`, `dominantBaseline?: string = "middle"`, `opacity?: number = 1`. Place it beside, not inside, raw SVG geometry in a `position: "relative"` container.
- `SvgFrame` pairs well with `VectorArrow`, `VectorSegment`, `SvgPoint`, `DotPoint`, `DashedLine`, and `Legend` inside custom SVG.
- `Arrow`: `direction?: "down" | "right" | "up" | "left" = "down"`, `label?`, `length?: number = 40`, `c`, `color?`.
- `VectorArrow`: `key?`, `x1`, `y1`, `x2`, `y2`, `color`, `strokeWidth?: number = 2`, `arrowSize?: number = 2.2`, `opacity?: number = 1`.
- `VectorSegment`: `key?`, `x1`, `y1`, `x2`, `y2`, `color`, `strokeWidth?: number = 4`, `opacity?: number = 1`, `showStartDot?: boolean = false`, `showEndDot?: boolean = true`, `dotRadius?: number = 4.5`.
- `Box`: `label`, `c`, `color?: ColorName = "primary"`, `width?`, `height?`, `fontSize?: number = 14`, `gradient?: boolean = false`, `sublabel?`, `icon?`, `radius?: number = 8`, `outlined?: boolean = false`.
- `ArrowMarkerDef`: `id`, `color`, `size?: number = 5`, `direction?: "start" | "end" = "end"`.
- `DotPoint`: `x`, `y`, `label`, `color`, `c`, `size?: number = 12`, `labelOffset?: { x?: number; y?: number }`.
- `DashedLine`: `x1`, `y1`, `x2`, `y2`, `color`, `dotSpacing?: number = 8`, `dotSize?: number = 2`.
- `Legend`: `items: LegendItem[]`, `c`, `title?`. `LegendItem`: `label`, `color`, `style?: "solid" | "dashed"`.

Theme and render APIs:

- `checkFrame(frame, options?)`: Check `{ scene: ReactNode }` or `{ create(theme): ReactNode }` inputs. Frame metadata accepts `source?`, `width?`, `height?`, and `autoSize?`. Options accept `themes?: ThemeMode[]`, `background?: "transparent" | "theme" | CSS color`, and `failOn?: "error" | "warning" | "info" | "never"`; defaults are both themes, transparent background, and errors only. `ok` means diagnostics pass `failOn`; `rendered` means every requested theme completed rendering. Reports include per-theme `dimensions` and `diagnostics`, plus `summary` severity counts.
- `CHECK_DIAGNOSTIC_CODES`, `CheckDiagnostic`, `CheckDiagnosticCode`, `CheckDiagnosticLocation`, `CheckAutoSize`, `CheckAutoSizeAxes`, `CheckFrameInput`, `CheckFrameReport`, `CheckOptions`: Stable code list and TypeScript contracts for programmatic checks. Locations contain a React `path` or rendered `x`, `y`, `width`, and `height`; available element identifiers appear as `elementId` and `relatedElementIds` on diagnostics.
- `getThemeColors(mode, preset?)`: `mode: ThemeMode`; `preset?: "default" | "engineering"`. Use `engineering` for flat technical-blog figures with a light gray palette, left-aligned black titles, pastel nodes, thin neutral connectors, compact corners, no shadows, Inter labels, and JetBrains Mono annotations. In a bare frame, set `preset = "engineering";` before the JSX. Add `background={c.bg}` to `Scene` for an opaque light gray canvas; omit it for alpha transparency.
- `getToneColor(tone, c)`, `getToneFill(tone, c)`, `getToneGradient(tone)`, `getColor(name, c?)`, `getGradient(name)`, `heatColor(value)`.
- `getReadableColor(name, c)`: returns the semantic color variant with greatest contrast against `c.bgCard`.
- `getReadableToneColor(tone, c, background?)`: maps a tone to a readable semantic color and falls back to readable foreground text when contrast is below 4.5:1. `background` defaults to `c.bgCard`.
- `getReadableTextColor(background, c)`: parses solid or alpha CSS colors and chooses a theme foreground meeting 4.5:1 contrast when possible.
- `createPlotArea(width, height, margin)`: returns `PlotArea` geometry with inner width and height clamped to at least one pixel.
- `renderToPng(element, options, createFn?, theme?)`: `element`, `options: RenderOptions`, `createFn?: (theme: "dark" | "light") => ReactNode`, `theme?: "dark" | "light"`.
- `renderToPngWithOutput(...)`: same arguments as `renderToPng`; returns logical `width` / `height`, physical `pixelWidth` / `pixelHeight`, and painted `contentBounds`.
- `CanvasOverflowError`: typed clipping failure from strict static or animated renders. Exposes `width`, `height`, `overflow`, and optional animation frame/scene/time/label metadata.
- `Watermark`: JSX marker component for frame-module exports. Props are `WatermarkOptions` plus `children?: ReactNode`; children become the complete watermark body.
- `WatermarkInput`: `boolean | string | WatermarkOptions | ReactElement<WatermarkElementProps>`. `true` uses Vizmatic defaults. A string sets the watermark text. A React element can be `<Watermark>...</Watermark>` or any custom element.
- `WatermarkImageOptions`: `src`, `width?: number`, `height?: number`, `alt?: string`. Programmatic `src` should be a URL or data URI. CLI `--watermark-image` accepts URL, data URI, or local path.
- `WatermarkOptions`: `text?: string | false`, `image?: string | WatermarkImageOptions`, `icon?: ReactNode | string | false`, `element?: ReactNode`, `position?: "top-left" | "top-right" | "bottom-left" | "bottom-right" = "top-right"`, `opacity?: number`, `color?: string`.
- `normalizeWatermark(input)`: converts boolean, string, object, or React-element `WatermarkInput` to `WatermarkOptions | undefined`.
- `wrapWithBrand(element, width, height, theme?, label?)`: compatibility wrapper around `wrapWithWatermark`; theme defaults to `"dark"` and label defaults to `"Vizmatic"`.
- `getFonts()`: lazily loads and caches bundled Satori font data for advanced integrations.
- `loadAdditionalAsset(code, segment)`: Satori missing-glyph callback; resolves emoji segments from the compressed offline Twemoji bundle or public fallback and returns an empty font list for other codes.
- `RenderOptions`: `width`, `height`, `outputPath`, `background?: "transparent" | "theme" | CSS color = "transparent"`, `theme?: "dark" | "light" = "dark"` for direct-render watermark defaults, `watermark?: WatermarkInput`, `brand?: boolean | string` as a compatibility alias, `crop?: boolean | "height" | "both" = true`, `scale?: number = 2`. Default autocrop retains 24 source pixels around detected content. Use `crop: "height"` when host layouts require stable width but should still trim extra vertical whitespace.
- `renderToBuffer(element, width, height, options?)`: `options` supports `background?`, `theme?`, `watermark?`, `brand?`, `scale?`.
- `renderToSvg(element, width, height, options?)`: `options` supports `background?`, `theme?`, `watermark?`, `brand?`.
- `defineAnimation({ initial, timeline, render, fps? })`: creates deterministic typed state animation. `fps` defaults to `20` and accepts integers from `1` to `50`.
- `hold(duration, label?)`: keep current state for milliseconds.
- `tween(to, { duration, easing?, interpolate?, label? })`: transition listed properties together. Easing is `"linear" | "ease-in" | "ease-out" | "ease-in-out"` or pure progress function.
- `keyframe(value, label?)`: apply instant partial state update.
- `parallel(tracks, label?)`: run property tracks concurrently; each track accepts `hold`, `tween`, and `keyframe` steps.
- `sampleAnimation(animation, time)`: evaluate state at exact millisecond.
- `sampleAnimationFrame(animation, time, { index, count, delay, fps })`: sample state and timeline context at an exact video timestamp.
- `sampleAnimationFrames(animation, { fps? })`: return deterministic state/frame samples with GIF-compatible delays.
- `analyzeAnimationCadence(animation, { fps? })`: return frame count, encoded duration, target interval, and delay range without rendering.
- `renderAnimationGif(animation, options)`: stream timeline samples into GIF.
- `renderAnimationVideo(animation, { width, height, outputPath, fps?, scale?, theme?, background?, watermark?, audioPath?, crf?, preset?, threads?, signal? })`: render typed TSX frames to H.264 MP4. Defaults to 30 fps and an opaque theme background; streams frames and checks overflow at each timestamp. FFmpeg is installed separately.
- `renderRgbaVideo(renderFrame, { width, height, duration, outputPath, fps?, audioPath?, crf?, preset?, signal? })`: encode caller-painted RGBA frames without buffering the whole film.
- `tracePolyline(points, progress)` and `pointAlongPath(points, progress)`: reveal and follow a path by arc length; `arcSwap(from, to, progress, options?)` exchanges positions along mirrored arcs; `smoothstep(progress)` clamps and eases progress.
- `renderAnimationGifWithOutput(animation, options)`: same arguments and returns dimensions, input/encoded frame counts, delta-frame count, duration, and byte size.
- `renderAnimatedGif(scenes, options)`: legacy `AnimatedScene[]` renderer for scene cuts and pixel transitions. It also accepts a defined animation.
- `renderAnimatedGifWithOutput(scenes, options)`: same arguments as `renderAnimatedGif`; returns dimensions and encoding metrics.
- `AnimatedScene`: `element`, `duration` in ms, `transition?: "none" | "fade" | "appear"`, `transitionDuration?`, `label?`.
- `AnimationOptions`: `width`, `height`, `outputPath`, `loop?: number = 0`, `scale?: number = 1`, `fps?: number`, `deltaFrames?: boolean = true`, `background?: "theme" | "transparent" | CSS color = "theme"`, `watermark?: WatermarkInput`, `brand?: boolean | string` as a compatibility alias, `theme?: "dark" | "light" = "dark"`. Every actual frame is checked for overflow. Opaque GIFs encode changed rectangles; transparent GIFs stay full-frame for correct one-bit compositing. Use PNG/SVG when smooth alpha edges matter.

## Prop-aware examples

### Window + code + status

```jsx
width = 1040;
height = 560;

<Scene title="Release gate" subtitle="tool output and checklist">
  <Row gap={18} align="stretch">
    <WindowFrame title="ci.log" variant="terminal" tone="green" width={520}>
      <CodeBlock
        fontSize={12}
        showLineNumbers
        lines={[
          { text: "pnpm typecheck", tone: "green", prefix: "$" },
          { text: "pnpm test", tone: "green", prefix: "$" },
          { text: "pnpm render:examples", tone: "cyan", prefix: "$" },
        ]}
      />
    </WindowFrame>
    <StatusList
      width={360}
      rows={[
        { label: "Types", detail: "clean", status: "check", tone: "green" },
        { label: "Visuals", detail: "dark/light generated", status: "check", tone: "cyan" },
        { label: "NPM", detail: "auth required", status: "warn", tone: "warm" },
      ]}
    />
  </Row>
</Scene>
```

### Matrix + chart

```jsx
width = 1040;
height = 560;

<Scene title="Attention audit" subtitle="weights, scores, and distribution">
  <Row gap={18} align="stretch">
    <Heatmap
      title="Attention"
      xLabels={["q1", "q2", "q3"]}
      yLabels={["k1", "k2", "k3"]}
      data={[
        [0.9, 0.2, 0.1],
        [0.4, 0.8, 0.2],
        [0.1, 0.3, 0.7],
      ]}
    />
    <Matrix
      title="Scores"
      format="decimal"
      rowLabels={["A", "B", "C"]}
      colLabels={["x", "y", "z"]}
      data={[
        [0.4, 0.7, 0.2],
        [0.5, 0.2, 0.8],
      ]}
    />
    <StackedBar
      width={300}
      title="Token budget"
      segments={[
        { label: "prompt", value: 42, color: "primary" },
        { label: "context", value: 38, color: "info" },
        { label: "answer", value: 20, color: "positive" },
      ]}
    />
  </Row>
</Scene>
```

## Rendering rules

- Use semantic tones instead of hard-coded colors: `blue`, `purple`, `green`, `warm`, `cyan`, `pink`, `critical`, `ocean`, `neutral`.
- For Datadog Engineering-style article diagrams, set `preset = "engineering";` and render the light theme. Keep each figure focused on one transition or tradeoff.
- Use explicit sizes for final assets. Common sizes are `1040x560` for article figures, `1280x720` for slide frames, and `900x520` for compact diagrams.
- During exploration, dimensions can be omitted. Vizmatic starts at `960x540` and grows to fit content.
- Prefer alpha-transparent PNG/SVG backgrounds for blog embeds and docs cards. Use theme backgrounds only when the host surface is unknown or needs full-frame fill.
- Use `width`, `minWidth`, `height`, `minHeight`, `gap`, and `padding` to keep layout stable.
- Keep text short. Use `TextLabel`, `Panel`, `StepCard`, `MetricCard`, `DataTable`, and `Grid` for wrapping-safe labels.
- Omit `x` and `y` from `GraphDiagram` nodes for automatic layered layout and nested groups. Add both only when an ungrouped graph needs editorial manual control.
- Render reusable assets in both themes with `--theme dark,light`.
- Run `vizmatic check <frame> --theme dark,light --json` before final render and fix every error.
- Use GIF only when motion explains state change. Prefer `createAnimation(theme)` with `hold` and `tween`, keep timelines short, match terminal and initial visuals for infinite loops, and keep static `create(theme)` fallback. Sampling follows exact cadence boundaries and includes terminal state. Use `analyzeAnimationCadence` when timing must be audited. Use `createScenes(theme)` only for deliberate cuts or crossfades.
- If render fails with `Canvas overflow detected`, increase the canvas, reduce content, or remove strict sizing. Do not ignore the error.
- Confirm generated files exist and open at least one image before finishing.

## Common recipes

### RAG graph

```jsx
width = 1040;
height = 560;

<Scene title="RAG control graph" subtitle="retrieval with a citation check" align="center">
  <GraphDiagram
    width={820}
    height={390}
    nodeWidth={132}
    rankGap={44}
    nodes={[
      { id: "query", label: "Query", detail: "intent", tone: "blue" },
      { id: "retrieve", label: "Retrieve", detail: "top-k docs", tone: "cyan" },
      { id: "rerank", label: "Rerank", detail: "quality gate", tone: "warm" },
      { id: "answer", label: "Answer", detail: "grounded draft", tone: "green" },
      { id: "verify", label: "Verify", detail: "citations", tone: "critical" },
    ]}
    edges={[
      { from: "query", to: "retrieve", label: "search", tone: "blue" },
      { from: "retrieve", to: "rerank", label: "rank", tone: "cyan" },
      { from: "rerank", to: "answer", label: "context", tone: "green" },
      { from: "answer", to: "verify", label: "claims", tone: "critical" },
    ]}
  />
</Scene>
```

### Evaluation dashboard

```jsx
width = 1040;
height = 620;

<Scene title="Evaluation snapshot" subtitle="charts inherit theme, labels, and contrast">
  <Row gap={18} align="stretch">
    <BarChart
      width={440}
      height={260}
      title="Pass rate by task"
      format="percent"
      data={[
        { label: "tools", value: 0.82, color: "positive" },
        { label: "math", value: 0.71, color: "secondary" },
        { label: "code", value: 0.77, color: "primary" },
        { label: "long", value: 0.58, color: "warning" },
      ]}
    />
    <LineChart
      width={440}
      height={260}
      title="Quality over releases"
      format="percent"
      labels={["v1", "v2", "v3", "v4", "v5"]}
      series={[
        { name: "quality", points: [0.55, 0.61, 0.69, 0.74, 0.81], color: "positive", area: true },
        { name: "latency", points: [0.72, 0.69, 0.66, 0.62, 0.59], color: "warning" },
      ]}
    />
  </Row>
</Scene>
```

## Scientific explanations

Use `NumberLine` and `GeometryDiagram` for intervals and explicit geometric
relations, `FreeBodyDiagram` for vectors on a common force scale, and
`WaveDiagram` for signed sinusoidal superposition. Use `MoleculeDiagram` for
explicit 2D atom positions and bonds, `ReactionEquation` for coefficients and
states, and `ReactionEnergyDiagram` for explicitly supplied energy levels.
For numerical arrays, use `Matrix` with a readable `fontSize` and explicit
`colorDomain` when values are outside 0–1. Highlight the selected row, column,
or cell; derive multiplication and transpose results with `multiplyMatrices`
and `transposeMatrix`. For image-to-tensor lessons, use `PixelGrid`, `rgbChannel`,
`cropRgbImage`, and `flattenRgbImage`. An RGB image has three channel matrices.
Keep full channel planes separate from an enlarged numeric crop, preserve
row/column indices and byte order, and identify the actual image source.

`AttentionMatrix` displays normalized probabilities; `scaledDotProductAttention`
computes one head from explicit Q, K, and V tensors in regular TypeScript.
These components complement `AxisPlot`, equations, matrices, graphs, and
`TransformerTopology`. See [SCIENCE.md](https://github.com/bvolpato/vizmatic/blob/main/SCIENCE.md) for contracts and worked
examples. Bare editor scenes use the component names without imports.

Keep units and coordinate scales stable, animate the changing mechanism, and
drive its diagram and readout from the same numerical state. Chemical balance
requires supplied composition maps; molecular drawings do not infer chemical
validity. Label illustrative data, and do not present attention weights as an
explanation of model reasoning.

# Guide: Figures reference

A figure is a TSX file. The `vizmatic` command renders it in each theme. Use [Diagrams and density](diagrams.md) for the design guidance, and this page for the details.

## Write a frame

Create `frame.tsx`:

```jsx
<Scene title="Agent visual pipeline">
  <Flow
    stages={[
      { title: "Prompt", subtitle: "intent", tone: "blue" },
      { title: "Scene spec", subtitle: "typed structure", tone: "purple" },
      { title: "PNG / SVG / GIF", subtitle: "files", tone: "green" },
    ]}
  />
</Scene>
```

`Scene` title and subtitle are optional. Omit both when surrounding copy already names the visual.

Render it directly:

```bash
vizmatic ./frame.tsx --out ./dist/frames --theme dark,light
```

Check both themes before publishing. `--json` reports overflow, contrast, small text, overlapping labels, uneven panel whitespace, connector congestion, asset failures, and suggested dimensions:

```bash
vizmatic check ./frame.tsx --theme dark,light --json
```

PNG and SVG renders have transparent backgrounds by default. Add `--background theme` for an opaque dark or light canvas:

```bash
vizmatic ./frame.tsx --out ./dist/frames --theme dark,light --background theme
```

CSS alpha colors stay semi-transparent in PNG and SVG output. Set the canvas background in the frame or pass a quoted CSS color to `--background`:

```jsx
<Scene background="rgba(15, 23, 42, 0.5)">
  <CalloutCard title="Alpha background" detail="The host page remains visible through the canvas." />
</Scene>
```

```bash
vizmatic ./frame.tsx --background 'rgba(15, 23, 42, 0.5)'
```

When a frame omits `width` or `height`, Vizmatic starts at `960x540` and grows that axis if content overflows. Set `autoSize = false` when dimensions must remain fixed and clipping should fail the render.

Bare CLI frames do not need imports. Use a regular module when a frame needs helper code, data loading, other dependencies, animation, or direct renderer APIs.

## Rendering options

Add a text or image watermark when output needs branding:

```bash
vizmatic ./frame.tsx --out ./dist/frames --theme dark,light --watermark "Acme" --watermark-image ./logo.svg
```

Each output directory includes `manifest.json`. `outputs` lists generated files, and `outputDetails` records theme, path, and pixel dimensions.

Static CLI renders default to PNG. Use `--format svg` for vector output or `--format both` to export PNG and SVG together. Each manifest output detail includes its `format`; both formats support themes, backgrounds, watermarks, cropping, and automatic canvas growth.

```bash
vizmatic ./frame.tsx --out ./visuals --theme dark,light --format both
```

Render an animation:

```bash
vizmatic gif ./animated-frame.tsx --out ./dist/frames --theme dark,light --fps 20 --watermark "Acme" --watermark-image ./logo.svg
```

Or call the renderer directly:

```typescript
import { renderToPng } from "vizmatic"
import { create, width, height } from "./frame"

await renderToPng(create("dark"), {
  width,
  height,
  outputPath: "dist/agent-pipeline.png",
  background: "theme", // omit for alpha-transparent output
  watermark: { text: "Acme", image: "data:image/svg+xml;base64,...", position: "top-right" },
})
```

Frames can export their own watermark:

```jsx
import { Watermark } from "vizmatic"

export const watermark = (
  <Watermark position="bottom-right" opacity={0.82}>
    <div style={{ display: "flex", alignItems: "center", gap: 6, color: "#7c3aed", fontWeight: 800 }}>
      <img src="data:image/svg+xml;base64,..." width={14} height={14} />
      Acme
    </div>
  </Watermark>
)
```

## Runtime assets

Vizmatic uses `react` for JSX frames, `satori` for layout, `@resvg/resvg-js` for PNG output, `gifenc` for GIFs, and `tsx` to load frame files.

Published packages include Inter, JetBrains Mono, Noto Sans, Noto Sans Math, and a compressed Twemoji bundle under `assets/`, so normal CLI rendering does not fetch them at runtime. Set `VIZMATIC_FONT_DIR` or `VIZMATIC_ASSET_DIR` to use other local assets. Set `VIZMATIC_DISABLE_NETWORK=1` or `VIZMATIC_OFFLINE=1` to disable public fallbacks.

The complete Twemoji set stays available offline but ships as one Brotli bundle instead of 3,720 individual SVG files. `pnpm deps:check` keeps the package below 3.2 MB compressed, 7.3 MB unpacked, and 100 files, including the separately loaded browser scene renderer.

## API

### Themes

```typescript
import { getThemeColors, getToneFill } from "vizmatic"

const dark = getThemeColors("dark")
const light = getThemeColors("light")
const engineering = getThemeColors("light", "engineering")
const engineeringNodeFill = getToneFill("purple", engineering)
```

Primitives receive `c`, the resolved theme object, for colors and typography.

Bare frames select the same style with `preset = "engineering";`. Full modules pass the preset as the second argument to `getThemeColors`.

### Component catalog

Use these primitives before writing raw SVG or absolute-positioned layouts.

#### Frame and layout

| Component | Use |
|---|---|
| `defineIllustration` | Wraps a theme-aware frame builder and exports `create(theme)` plus a default element. |
| `Canvas` | Low-level full-frame root with alpha-transparent default background, optional background fill, padding, and vertical alignment. |
| `Board` | Fixed canvas for freely positioned components, with explicit dimensions and an optional background. |
| `CanvasItem` | Positions and sizes a component at logical-pixel `x` / `y` coordinates. Charts fit their complete plotting layers, labels, and frame inside the item. |
| `Scene` | Standard frame wrapper with optional title/subtitle, content column, and theme-aware typography. |
| `TitleBar` | Shared title/subtitle block used by `Scene`. |
| `Row` / `Column` | Flex layout primitives with gap, alignment, wrapping, width, and height props. |
| `Stack` | Layered vertical stack for repeated cards, tokens, or processing layers. |

Use `Board` with `CanvasItem` for layouts that people can move and resize in the browser builder. Coordinates and sizes remain in the TSX source. Set the frame dimensions to match the board; the board clips content outside its bounds. With both item dimensions set, chart primitives scale proportionally to fit the complete chart, including wrapped headings, legends, and footers.

```jsx
width = 960;
height = 540;

<Board width={960} height={540}>
  <CanvasItem x={40} y={40} width={280} height={160}>
    <MetricCard label="Requests" value="1,240" tone="green" />
  </CanvasItem>
  <CanvasItem x={360} y={40} width={540} height={340}>
    <PieChart title="Requests by outcome" format="integer" segments={[
      { label: "Successful", value: 82, color: "positive" },
      { label: "Retried", value: 18, color: "warning" },
    ]} />
  </CanvasItem>
</Board>
```

#### Surfaces, labels, and cards

| Component | Use |
|---|---|
| `Panel` | Titled card surface with tone strip, subtitle, footer, shadow, and body controls. |
| `Card` | Flexible surface for custom content without a forced title. |
| `StepCard` | Compact stage card for flows, choices, and process steps. |
| `MetricCard` | KPI/value card with label, value, detail, tone, and monospace value support. |
| `CalloutCard` | Highlight block for takeaways, warnings, decisions, and summaries. |
| `WindowFrame` | Browser/terminal-style framed panel for code, UI, or tool output. |
| `Box` | Gradient or outlined labeled rectangle with optional icon and sublabel. |
| `Tile` / `TileGrid` | Uniform repeated tiles with tone, title, detail, and metric layouts. |
| `Badge` / `BadgePill` | Small labels for status, categories, and annotations. |
| `ValuePill` | Compact value badge for numbers or short state labels. |
| `GradientChip` | Small colored chip with gradient fill for legends and tone keys. |
| `ToneStrip` | Small semantic accent strip for visual grouping. |
| `Icon` | Curated SVG icon set for cards, callouts, compact rows, and presentation frames. |
| `TextLabel` | Wrapping-safe text with variant, color, math formatting, width, and alignment. |
| `MathText` / `formatMathText` | Converts simple `x_i` / `x^2` style strings into readable unicode math text. |
| `SvgMathText` | Positioned HTML overlay for math labels aligned to custom plot coordinates. Place it beside, not inside, raw SVG geometry in a relative container. |

#### Arrows, connectors, and SVG helpers

| Component | Use |
|---|---|
| `Arrow` | Simple directional arrow element. |
| `FlowArrow` / `Connector` | Theme-aware connector between flow stages. |
| `VectorArrow` | SVG vector arrow for coordinate-style diagrams. |
| `VectorSegment` | Labeled segment/vector primitive for geometry and embedding visuals. |
| `SvgFrame` | SVG container with theme-aware background and border. |
| `SvgPoint` / `DotPoint` | Point markers with optional labels. |
| `ArrowMarkerDef` | SVG arrowhead marker definition for custom paths and graph edges. Use `direction="start"` to define a backward-facing path-start tip. |
| `DashedLine` | Dotted/dashed SVG line helper. |
| `Legend` | Reusable legend block with colored items and optional title. |

#### Lists, comparisons, and status blocks

| Component | Use |
|---|---|
| `DetailList` | Compact repeated detail rows inside panels and flow stages. |
| `ProgressRow` / `ProgressList` | Progress bars with labels, values, tones, and optional math text. |
| `StatusRow` / `StatusList` | Check, cross, warning, info, pending, and dot rows. |
| `Timeline` | Vertical or horizontal milestone list with tone markers, time labels, and compact event cards. |
| `KeyValueList` | Structured label/value rows for configs and metadata. |
| `CodeBlock` | Themed code panel with highlighted lines and optional annotations. |
| `EquationCard` | Formula card with title, equation, and supporting detail. |
| `Comparison` | Side-by-side comparison panel with two titled sides and detail rows. |

#### Flows, pipelines, and graph diagrams

| Component | Use |
|---|---|
| `Flow` | Horizontal or vertical staged process diagram with optional detail rows. |
| `Pipeline` | Process pipeline with stage labels and a shared title. |
| `LayeredNetwork` | Neural-network/DAG diagram with layers, nodes, active path, annotations, and formula. |
| `GraphDiagram` | Auto-laid architecture graph with nested boundaries, technical icons, typed relationships, and custom icon definitions. |
| `TreeDiagram` | Auto-laid parent/child hierarchy for ownership, routing, taxonomy, and decision trees. |
| `SequenceDiagram` | Participants, lifelines, activations, notes, sync/async/return messages, and control fragments. |
| `DataflowDiagram` | Schema-aware source, transform, store, and sink lineage with batch or streaming edges. |
| `DeploymentDiagram` | Nested infrastructure boundaries with workload kinds, ports, protocols, and trust direction. |
| `TransformerTopology` | Transformer blocks, tensor shapes, repeated layers, residual paths, caches, experts, and collectives. |

`GraphDiagram` uses deterministic layered layout when node coordinates are omitted. Groups become nested system boundaries, while built-in technical icons keep common architecture nodes concise:

```jsx
<GraphDiagram
  ariaLabel="Checkout architecture"
  groups={[
    { id: "prod", label: "Production", tone: "blue" },
    { id: "data", label: "Data", parent: "prod", tone: "green" },
  ]}
  nodes={[
    { id: "client", label: "Client", icon: "browser", tone: "cyan" },
    { id: "api", label: "API", icon: "server", group: "prod", tone: "purple" },
    { id: "db", label: "Postgres", icon: "database", group: "data", tone: "green" },
  ]}
  edges={[
    { from: "client", to: "api", label: "HTTPS", kind: "sync" },
    { from: "api", to: "db", label: "SQL", kind: "data", arrow: "both" },
  ]}
/>
```

Groups support unlimited nesting in automatic layout, reserve a boundary-header gutter, and clip long headers inside the boundary. Set `icon` to a built-in `IconName`, a React element, or a reusable icon from `defineDiagramIcon`; `defineIconRegistry` groups provider packs without a runtime registry. Icon size stays inside its node. Custom React elements own their theme colors and accessible label. Relationship `kind` selects solid, dashed, or dotted defaults, `style` overrides them, and `arrow` accepts `forward`, `backward`, `both`, or `none`. Associations default to no arrow.

Set `id`, `title`, and `description` for accessible SVG exports. Nodes and edges accept safe `href` links and tooltip `title` text. These semantics are included by `renderToSvg` and the playground SVG download; see the [SVG accessibility guide](https://github.com/bvolpato/vizmatic/blob/main/docs/accessibility.md) for supported links and stable ID behavior.

Set `x` and `y` on every node for normalized manual positioning when no groups are present. Mixed coordinate modes fail with a clear error. Automatic layout treats node and edge array order as its stable tie-breaker and expands `width` or `height` when needed. Set `sizing="fixed"` to keep exact graph dimensions and let overflow checks report a canvas that is too small. Use `direction`, `nodeGap`, `rankGap`, and `edgeGap` to tune the result.

Connector routing is separate from node layout. The default `routing="dagre"` preserves existing geometry. Choose `routing="straight"` for direct connectors or `routing="orthogonal"` for right-angle routes that avoid node boxes and group headers. `routingClearance` defaults to 12 logical pixels; `edgeGap` separates parallel traffic. Orthogonal routing supports automatic and manual node positions, self-loops with distinct anchors, and separated parallel routes.

Declare node `ports` as `{ id, side, offset? }`, then reference them with edge `fromPort` / `toPort`. Sides are `top`, `right`, `bottom`, or `left`; offsets run from 0 to 1 along the side and default to 0.5. An edge can instead specify `fromSide` / `toSide`. Use either a port reference or a side for each endpoint. Explicit anchors also opt an edge into orthogonal geometry when the routing mode is Dagre. With explicit straight or orthogonal routing, `DeploymentDiagram` anchors declared port references and accepts port `side` / `offset` overrides.

Routing runs synchronously offline with limits of 64 node/header obstacles and 256 edges, plus bounded grid and search work. A blocked route, repeated route with no separate lane, or exhausted limit omits the affected connector and makes the checker report `layout.connector_unroutable` as an error. Increase spacing or canvas size, change anchors, or reduce clearance before publishing. Missing port references fail validation. See the [routed graph example](https://github.com/bvolpato/vizmatic/blob/main/examples/orthogonal-routing.tsx).

The [import guide](https://github.com/bvolpato/vizmatic/blob/main/docs/import.md) documents the supported Mermaid and D2 subsets and their structured source diagnostics.

#### Matrices, tables, and grids

| Component | Use |
|---|---|
| `Matrix` | Numeric matrix visualization for linear algebra and attention examples. |
| `Heatmap` | Color-scaled matrix/attention heatmap. |
| `TiledMatrix` | Matrix with region labels and grouped tiles. |
| `DataTable` | Compact text table with header rows/columns and monospace cells. |
| `Grid` | General-purpose cell grid with per-cell label, tone, color, border, and opacity. |

#### Charts and plots

| Component | Use |
|---|---|
| `ChartFrame` | Shared chart wrapper with title, subtitle, footer, and theme-aware shell. |
| `AxisPlot` | Low-level axis plot for custom lines, paths, and plotted values. |
| `PieChart` | Full-circle part-to-whole chart with segment values, percentages, and a legend. |
| `DonutChart` | Part-to-whole chart with center label/value, segment labels, and shared chart styling. |
| `MiniBarChart` | Small bar chart for cards and dashboards. |
| `StackedBar` | Segmented horizontal bar with labels and percentages. |
| `BarChart` | Full bar chart with ticks, labels, values, grids, and formats. |
| `LineChart` | Multi-series line chart with optional area fill, points, labels, and grid. |
| `ScatterPlot` | Labeled scatter plot with axes, ticks, point sizing, and collision-aware labels. |
| `ParetoBarChart` | Categories sorted by descending count with a cumulative percentage line and separate count/share axes. |
| `ParetoChart` | Automatic non-dominated frontier for competing objectives, including log-scaled cost axes. |
| `QuadrantChart` | 2x2 decision matrix with numeric axes, configurable thresholds, labeled points, and optional region emphasis. |
| `IntervalPlot` | Timeline/range plot for spans, phases, latencies, and schedules. |

Use `PieChart` for category shares, `BarChart` for category values, `ScatterPlot` for paired numeric observations, and `ParetoBarChart` to identify the categories behind most of a total. Pie and Pareto bar values must be finite and non-negative; zero-valued pie slices are omitted. Pareto bars sort descending without changing the input array and preserve input order for equal values. The cumulative line reaches 100% when the total is positive. See the [pie](https://github.com/bvolpato/vizmatic/blob/main/examples/pie-chart.tsx) and [Pareto bar](https://github.com/bvolpato/vizmatic/blob/main/examples/pareto-bar-chart.tsx) examples.

`ParetoChart` shows a non-dominated scatter frontier for competing objectives. It defaults to minimizing x and maximizing y, which fits cost-versus-score comparisons. Set `xObjective` or `yObjective` for other tradeoffs. Non-positive x values are omitted on a logarithmic x-axis. `QuadrantChart` accepts raw numeric domains, threshold values, tick formats, and optional grid lines; omit them to keep normalized 0-1 coordinates and centered splits. Regions accept `emphasis: true` to give one decision region a stronger fill and border.

#### Rendering and verification

| API | Use |
|---|---|
| `checkFrame` | Check a React scene or themed frame and return typed diagnostics with configurable severity-based pass/fail. |
| `renderToPng` | Render React scene to PNG through Satori and resvg, with optional watermark, crop, scale, and overflow check. |
| `renderToPngWithOutput` | Render PNG and return logical dimensions, physical pixel dimensions, and painted `contentBounds`. |
| `CanvasOverflowError` | Typed clipping failure with canvas dimensions and per-edge overflow details. |
| `renderAnimatedGif` | Render ordered `AnimatedScene[]` states to GIF with `fade`, `appear`, or no transition. |
| `renderAnimatedGifWithOutput` | Render GIF and return dimensions, timing, encoded-frame, delta-frame, and byte metrics. |
| `defineAnimation` | Define a typed state timeline rendered at deterministic sample times. |
| `hold`, `tween`, `keyframe`, `parallel` | Compose sequential pauses, eased property transitions, instant state changes, and staggered concurrent tracks. |
| `sampleAnimation` | Evaluate timeline state at an exact millisecond without rendering. |
| `analyzeAnimationCadence` | Inspect sampled frame count, target interval, encoded duration, and delay range. |
| `renderAnimationGif` | Stream sampled timeline frames into a GIF without retaining every RGBA frame. |
| `renderToBuffer` | Render PNG to memory for tests and pipelines. |
| `renderToSvg` | Render SVG markup directly. |
| `Watermark` | JSX marker component for expressive frame-module watermarks. |
| `wrapWithWatermark` | Add optional watermark text, image/icon, custom element, and position to a frame. |
| `normalizeWatermark` | Normalize boolean, string, object, or React-element watermark input to `WatermarkOptions`. |
| `wrapWithBrand` | Compatibility wrapper for adding a text watermark through `wrapWithWatermark`. |
| `getFonts` | Load and cache bundled Satori font data for advanced renderer integrations. |
| `loadAdditionalAsset` | Resolve Satori missing-glyph requests, including bundled or cached Twemoji assets. |
| `detectBackgroundColor` | Find dominant transparent/background color for cropping. |
| `detectContentBounds` | Compute non-background bounds for autocrop. |
| `cropPixels` | Crop raw pixel buffers. |
| `detectOverflow` | Fail frames that clip content at canvas edges. |

Theme and chart helpers are public for custom primitives: `getReadableColor(name, c)` selects the highest-contrast semantic variant, `getReadableToneColor(tone, c, background?)` adapts tone text to a background, and `getReadableTextColor(background, c)` chooses a foreground meeting 4.5:1 contrast when possible. `createPlotArea(width, height, margin)` returns clamped chart geometry for custom axes and marks.

### Programmatic checking

Use `checkFrame` when a build, editor, or agent already has a React scene in memory. Pass `scene` for one fixed element or `create(theme)` for theme-specific elements:

```jsx
import { CalloutCard, Scene, checkFrame, getThemeColors } from "vizmatic"

const report = await checkFrame({
  source: "agent-pipeline",
  width: 640,
  height: 360,
  create(theme) {
    const c = getThemeColors(theme)
    return (
      <Scene c={c}>
        <CalloutCard c={c} title="Ready" detail="Checked in-process" tone="green" />
      </Scene>
    )
  },
}, {
  themes: ["dark", "light"],
  background: "theme",
  failOn: "warning",
})

if (!report.ok) throw new Error(`Visual check failed: ${report.summary.errors} errors`)
```

`failOn` accepts `"error"` (the default), `"warning"`, `"info"`, or `"never"`. `report.ok` means the diagnostics pass that policy; `report.rendered` means every requested theme completed rendering. Each theme report includes resolved and output dimensions, stable diagnostic codes, and severity counts. Diagnostics expose `elementId`, related IDs, and a `location` path or rendered bounds when available. Import `CHECK_DIAGNOSTIC_CODES`, `CheckDiagnostic`, `CheckFrameInput`, and `CheckFrameReport` for typed integrations. Use `vizmatic check ... --json` when the checker should discover and load frame files itself.

The [`render.svg_context` diagnostic](#svg-context-diagnostics) detects SVG shapes placed in known HTML contexts, including `SvgFrame` directly inside `Board` or `CanvasItem`. Use a native `<svg>` or `AxisPlot` to provide the required SVG context.

### Rendering

```typescript
import { renderAnimatedGif, renderAnimationGif, renderToPng, renderToBuffer, renderToSvg } from "vizmatic"
```

`renderToPng` renders at 2x scale, checks for clipping, and crops extra whitespace while keeping 24 source pixels around detected content. Output stays transparent unless `background` is set.

Use `background: "theme"` to paint `c.bg`, or pass a CSS color. Use `crop: "height"` to keep canvas width while trimming vertical whitespace. `watermark.image` accepts a URL or data URI; CLI flag `--watermark-image` also accepts a local path. A frame module can export `watermark = <Watermark>...</Watermark>`. `brand` remains a compatibility alias.

```typescript
await renderToPng(element, {
  width: 1040,
  height: 560,
  outputPath: "dist/frame.png",
  background: "theme",
  theme: "dark",
  watermark: { text: "Your Product", image: "https://example.com/logo.svg", position: "top-right" },
  crop: "height",
  scale: 2,
})
```

Frame modules can export watermark config directly:

```jsx
import { Watermark } from "vizmatic"

export const watermark = (
  <Watermark position="bottom-right">
    <div style={{ color: "#7c3aed", fontFamily: "Inter", fontWeight: 800 }}>
      Acme Research
    </div>
  </Watermark>
)
```

Prefer `defineAnimation` when values should move, resize, or fade continuously. Timeline array runs sequentially; every property in one `tween` moves in parallel. `parallel` adds independent property tracks for staggered or overlapping motion. Renderer samples exact cadence boundaries plus terminal state, checks every rendered frame for overflow, coalesces unchanged frames, and delta-encodes changed rectangles on opaque GIFs. Infinite loops should end on same visual state where they begin. Fade moving content out and reset it with a `keyframe` while hidden instead of reversing a one-way operation. Keep `create(theme)` as static fallback for PNG, docs previews, and reduced-motion users.

```jsx
import { defineAnimation, getThemeColors, hold, keyframe, Scene, tween, type ThemeMode } from "vizmatic"

function buildFrame(theme: ThemeMode, state: { operation: number; progress: number }) {
  const c = getThemeColors(theme)
  return <Scene c={c} title={`Step ${state.operation + 1}: ${Math.round(state.progress * 100)}%`}><div /></Scene>
}

export function createAnimation(theme: ThemeMode) {
  return defineAnimation({
    initial: { operation: 0, progress: 0 },
    timeline: [
      hold(500, "Broadcast input"),
      tween({ progress: 1 }, { duration: 900, easing: "ease-in-out", label: "Transfer" }),
      hold(500, "Broadcast output"),
      keyframe({ operation: 1, progress: 0 }),
      tween({ progress: 1 }, { duration: 900, easing: "ease-in-out", label: "AllReduce" }),
    ],
    fps: 20,
    render: (state) => buildFrame(theme, state),
  })
}
```

```typescript
await renderAnimationGif(createAnimation("dark"), {
  width: 1040,
  height: 560,
  outputPath: "dist/collective.gif",
  watermark: { text: "Your Product", image: "data:image/png;base64,...", position: "bottom-right" },
  theme: "dark",
  scale: 1,
})
```

`createScenes(theme)` and `renderAnimatedGif(AnimatedScene[])` remain supported for intentional scene cuts and pixel crossfades. `fps` controls their transition sampling as well as typed timelines. GIF output uses one shared 256-color palette and centisecond timing. Opaque animations use changed rectangles by default; set `deltaFrames: false` only for legacy decoder debugging. Transparent animations stay full-frame to preserve one-bit compositing. Use PNG/SVG for smooth alpha edges.

### SVG context diagnostics

`render.svg_context` reports an error when an SVG-only element appears in a known HTML context. `Board` and `CanvasItem` create HTML containers. Place `SvgFrame` inside a native `<svg>` or `AxisPlot` so its rectangle renders. Satori can otherwise complete rendering while omitting the shape. The diagnostic identifies the component or tag, React element path, and element ID when available.

Native and nested `<svg>` elements establish SVG context. `AxisPlot` places its children inside its generated SVG. Children of `<foreignObject>` use HTML context, so SVG shapes there need another `<svg>` wrapper. The checker follows resolved wrappers and known primitive contracts. It does not infer the child namespace of opaque function wrappers. Inspect their rendered output to confirm that shapes appear.

## The browser builder

The Components page and the Playground share a live component explorer. Choose a component to open its focused example, or use Add to scene to compose several components. Select an element to edit its literal props, add optional properties, duplicate or remove it, and move it within its group. Controls follow inline text and TSX edits. Computed props and expression-generated structure stay editable in the TSX. Preview framing and zoom preserve the full canvas in downloaded PNG and SVG files.

Choose the Freeform canvas template for a positioned builder. Move and resize `CanvasItem` components through the preview handles, use grid snapping, or adjust their literal coordinates in the inspector. Arrow keys nudge a focused handle; Shift makes a larger move and Alt bypasses snapping. The layout remains ordinary `Board` / `CanvasItem` TSX.

Structured data editors let you add, remove, and change pie segments, bar categories, scatter points, and table cells. Nested styles, arrays, and objects also have JSON editors. Invalid shapes and missing graph node references report an error before changing the source. Computed data remains editable in TSX.

Open Canvas and palette to change the frame dimensions, style preset, and any theme color. Choose a transparent, theme, or custom CSS background. These settings are saved in the TSX and participate in undo, project files, share links, and exports. Bare CLI frames support the same metadata:

```jsx
width = 960;
height = 540;
preset = "default";
background = "#101827";
themeColors = {"primary":"#22c55e","primaryLight":"hsl(140, 70%, 65%)"};

<Board width={960} height={540}>
  <CanvasItem x={40} y={40} width={300} height={120}>
    <CalloutCard title="Your palette" detail="Colors travel with the frame." />
  </CanvasItem>
</Board>
```

Copy block copies the selected element as reusable bare TSX, including its setup and canvas settings. Inside a Board it keeps the enclosing CanvasItem and its geometry, so the copied block renders on its own. Ctrl/Command+C copies a block when its canvas handle is focused. Copy source copies the complete composition.

Edits are saved locally in this browser and restore after reload. Component, shared-source, and gallery links open their own examples. Editing clears the original example or source bookmark so reload can recover your draft. Undo and redo cover source typing, controls, inline text, and scene operations, with up to 100 source snapshots for the current session. Use the buttons or Ctrl/Command+Z and Ctrl/Command+Shift+Z. Drafts do not sync between devices; use Save project to download a named `.vizmatic.json` file containing source, output theme, and canvas background, then Open project to restore it. Share and Copy source also let you move your TSX between tools.

# Guide: Getting started

## Install

Vizmatic needs Node.js 20 or newer. MP4 export also needs [FFmpeg](https://ffmpeg.org/download.html) with the `libx264` encoder.

```bash
npm install vizmatic
```

## Write a film

A film is a function. Create `my-film.ts` with this code. The player below runs the same code, live in this page.

```film open label="Axes draw on, a parabola traces across them, and its equation writes itself"
import { axes, draw, fadeIn, film, tex, write } from 'vizmatic/stage'

export default film({ title: 'My first film' }, (s) => {
    const graph = s.add(axes({ x: [-3, 3], y: [-1, 9], grid: true }).at(-240, 40))
    s.play(draw(graph.grid, { lag: 0.02 }), draw(graph.lines), fadeIn(graph.numbers))

    const parabola = graph.plot((x) => x * x, { stroke: 'blue', width: 5 })
    s.play(draw(parabola))

    const equation = s.add(tex('y = x^2', { size: 72 }).at(600, -200))
    s.play(write(equation))
    s.wait(1)
})
```

The `s` argument is the stage. Each `s.play(...)` writes animation onto a timeline and moves forward. Each `s.wait(...)` holds the picture.

## Preview it

Run a live preview. The page reloads when you save the file.

```bash
npx vizmatic preview my-film.ts
```

## Export it

Export writes an MP4 with worker threads. The result is frame-exact, so identical input gives identical frames.

```bash
npx vizmatic film my-film.ts --out ./out --quality high
```

Use `--look paper` for a light version of the same film. Colors such as `'blue'` are names from the current look, so a film changes look without any edits.

## Where to go next

- [Core concepts](concepts.md) explains the stage, the timeline, and why frames are pure.
- [Animations](animations.md) shows every animation with a small live demo.
- [Objects](objects.md) lists the ready-made axes, planes, charts, graphs, and matrices.
- The [Studio](../studio.html) lets you build a film in the browser.

# Guide: Looks and color

## Names, not values

Write `'yellow'` or `'ink'` in your film. Each look gives those names its own values, so one film renders correctly in every look.

```film paired label="An equation and a curve in the current look"
import { axes, draw, film, tex, write } from 'vizmatic/stage'

export default film({ fps: 30 }, (s) => {
    const graph = s.add(axes({ x: [-3, 3], y: [-1, 5], width: 900, height: 700, grid: true, labels: false }).at(-380, 0))
    const curve = graph.plot((x) => 0.5 * x * x, { stroke: 'teal', width: 6 })
    const eq = s.add(tex('y = \\tfrac{1}{2} x^2', { size: 110, color: 'yellow' }).at(520, 0))
    s.play(draw(graph.lines), draw(curve), write(eq))
    s.wait(1)
})
```

This page follows the site theme. Switch the theme at the top of the page and the film above recompiles in the other look.

## The looks

| Look | Character |
|---|---|
| `midnight` | Deep neutral black with luminous pigments. The default. |
| `paper` | Warm paper and printed ink, for articles and print. |
| `blueprint` | Drafting-table navy with cool cyan linework. |
| `chalk` | Near-black green with warm chalk, for lessons. |
| `neon` | Saturated light on pure black, for trailers. |

Choose a look in the film, or override it when you render.

```ts
import { film, text, write } from 'vizmatic/stage'

export default film({ look: 'blueprint', title: 'Plans' }, (s) => {
    s.play(write(s.add(text('Plans', { font: 'text-serif', size: 200 }))))
})
```

```bash
npx vizmatic film my-film.ts --look midnight,paper
```

## The tokens

Each look defines `bg`, `surface`, `ink`, `dim`, `faint`, `grid`, `gridStrong`, `blue`, `teal`, `green`, `yellow`, `gold`, `orange`, `red`, `pink`, and `purple`. A CSS color also works anywhere a token does, and it stays fixed across looks.

## Gradients

`colorAlong` colors a stroke along its length. `linearGradient` and `radialGradient` fill shapes. `ramp(n, a, b)` gives `n` evenly mixed colors for a list of items. Colors mix in the OKLab color space, so blends stay bright and even.

```film paired label="A curve whose color travels along its length"
import { colorAlong, draw, film, parametric } from 'vizmatic/stage'

export default film({ fps: 30 }, (s) => {
    const curve = parametric((u) => [-800 + 1600 * u, Math.sin(u * 12) * 320 * (1 - u * 0.5)], 0, 1, {
        stroke: colorAlong('blue', 'teal', 'yellow', 'orange', 'pink'), width: 12,
    })
    s.play(draw(curve), { duration: 2 })
    s.wait(0.8)
})
```

## Light

`.glow(radius)` adds soft light around one shape. A look's `bloom` lets every bright mark spill a little light across the frame. Set `bloom` on the film to change it, and use `0` to turn it off. Looks on a light ground have no bloom.

```film paired label="A ring with a strong glow beside one with none"
import { circle, draw, film } from 'vizmatic/stage'

export default film({ fps: 30, bloom: 0.3 }, (s) => {
    const lit = circle(260, { stroke: 'yellow', width: 10 }).glow(40).at(-380, 0)
    const flat = circle(260, { stroke: 'yellow', width: 10 }).at(380, 0)
    s.play(draw(lit), draw(flat))
    s.wait(1)
})
```

## Make your own

A look is a plain object. Pass it as `look` on the film.

```ts
import { film, midnight, text, write, type Look } from 'vizmatic/stage'

const studio: Look = {
    ...midnight,
    name: 'studio',
    background: '#101418',
    colors: { ...midnight.colors, bg: '#101418', yellow: '#f6c945' },
}

export default film({ look: studio }, (s) => {
    s.play(write(s.add(text('Our colors', { font: 'text-serif', size: 200, color: 'yellow' }))))
})
```

# Guide: Objects

## Axes and plots

`axes(options)` draws coordinate axes. `.world(x, y)` converts math coordinates to stage points, and `.plot(f)` draws a function.

```film paired label="A curve on axes with a dot and dashed guides"
import { axes, dashedGuides, draw, fadeIn, film } from 'vizmatic/stage'

export default film({ fps: 30 }, (s) => {
    const graph = s.add(axes({ x: [0, 4], y: [0, 16], width: 1300, height: 760, grid: true }))
    const curve = graph.plot((x) => x * x, { stroke: 'blue', width: 7 })
    const dotAt = graph.dotAt(3, 9, 'yellow', 16)
    const guides = dashedGuides(graph, 3, 9)
    graph.add(guides)
    s.play(draw(graph.grid, { lag: 0.02 }), draw(graph.lines), fadeIn(graph.numbers), { duration: 1 })
    s.play(draw(curve))
    s.play(fadeIn(dotAt), fadeIn(guides))
    s.wait(0.8)
})
```

`riemann`, `areaUnder`, `tangentLine`, and `secantLine` work with an `Axes`. `numberLine` draws one axis alone.

```film paired label="Rectangles under a curve grow more numerous and approach the area"
import { axes, draw, fadeIn, film, morph, riemann } from 'vizmatic/stage'

export default film({ fps: 30 }, (s) => {
    const graph = s.add(axes({ x: [0, 4], y: [0, 16], width: 1300, height: 760 }))
    const f = (x: number) => x * x
    const curve = graph.plot(f, { stroke: 'yellow', width: 6 })
    s.play(draw(graph.lines), fadeIn(graph.numbers), draw(curve), { duration: 1 })
    let bars = riemann(graph, f, { from: 0, to: 4, n: 6 })
    graph.add(bars)
    s.play(fadeIn(bars))
    for (const n of [12, 40]) {
        const next = riemann(graph, f, { from: 0, to: 4, n })
        graph.add(next)
        s.play(morph(bars, next))
        bars = next
    }
    s.wait(0.6)
})
```

## Planes and fields

`plane` draws the classic grid. Its lines are dense paths, so they bend smoothly under `applyMatrix` and `warp`. `complexPlane` and `applyComplex` map the grid through a complex function.

```film paired label="A grid is mapped through the complex square function"
import { applyComplex, complex, complexPlane, film } from 'vizmatic/stage'

export default film({ fps: 30 }, (s) => {
    const grid = s.add(complexPlane({ unit: 170, x: [-5, 5], y: [-4, 4] }))
    s.wait(0.2)
    s.play(applyComplex(grid, (re, im) => {
        const [x, y] = complex.pow([re, im], 2)
        return [x / 2.2, y / 2.2]
    }, { duration: 2.6 }))
    s.wait(0.6)
})
```

`vectorField` draws arrows from a function, and `streamLines` draws flowing paths through the same field.

```film paired label="Streamlines flow around a center"
import { fadeIn, film, streamLines, vectorField } from 'vizmatic/stage'

export default film({ fps: 30 }, (s) => {
    const field = (x: number, y: number): readonly [number, number] => [-y, x]
    const arrows = s.add(vectorField(field, { unit: 150, step: 1, x: [-6, 6], y: [-3, 3] }))
    const flow = s.add(streamLines(field, { unit: 150, x: [-6, 6], y: [-3, 3] }))
    s.play(fadeIn(arrows), fadeIn(flow))
    s.wait(3)
})
```

## Fourier

`fourier(points)` computes rotating circles from a closed outline. `epicycles(terms, options)` draws them, and `fourierPoint` gives the pen position at any fraction of a turn.

The [Fourier film](../films.html) uses these three together.

## Marks

| Object | Use |
|---|---|
| `matrix(rows)` | A bracketed matrix of numbers or TeX. |
| `braceLabel(target, label)` | A curly brace beside a node, with a label. |
| `surroundingBox(target)` | A rounded box around a node. |
| `labeledDot(label)` | A dot with a label, as a graph vertex. |
| `angleMark(vertex, from, to)` | An arc between two rays. |
| `rightAngle(vertex, a, b)` | A square corner mark. |
| `traced(point, from, to)` | The path a moving point left behind. |

```film paired label="A matrix writes itself and a brace labels it"
import { braceLabel, fadeIn, film, matrix, write } from 'vizmatic/stage'

export default film({ fps: 30 }, (s) => {
    const m = s.add(matrix([[2, 1], [0, 3]], { size: 130 }))
    s.play(write(m))
    const note = s.add(braceLabel(m, '\\text{a linear map}', { side: 'bottom', size: 72 }))
    s.play(fadeIn(note, { shift: [0, -24] }))
    s.wait(0.8)
})
```

## Charts

`barChart`, `histogram`, `scatter`, `lineChart`, `pie`, `donut`, and `heatmap` take data and expose their parts for animation.

```film paired label="Bars grow up from an axis"
import { barChart, film, grow, lagged } from 'vizmatic/stage'

export default film({ fps: 30 }, (s) => {
    const chart = s.add(barChart([
        { label: 'A', value: 4 }, { label: 'B', value: 7 }, { label: 'C', value: 5 }, { label: 'D', value: 9 },
    ], { width: 1200, height: 700 }))
    s.play(lagged(chart.bars.map((bar) => grow(bar, { from: bar.bottom })), { lag: 0.2 }), { duration: 1.6 })
    s.wait(0.8)
})
```

`dotChart(data)` draws one row per item: a label, a dot at its value on a shared axis, and a line for its interval. Use it for scores with their uncertainty. It takes less room than bars, and its axis does not have to start at zero. The rows keep the order of the data, so sort the data to rank it. `hollow: true` draws a ring, to tell a second group apart.

```film paired label="Five scores with their intervals arrive from the best to the worst"
import { dotChart, film } from 'vizmatic/stage'

export default film({ fps: 30 }, (s) => {
    const chart = dotChart([
        { label: 'blocks', value: 0.89, low: 0.87, high: 0.91 },
        { label: 'tall CSV', value: 0.88, low: 0.86, high: 0.9 },
        { label: 'statistics alone', value: 0.57, low: 0.54, high: 0.6, color: 'orange' },
        { label: 'chart image', value: 0.57, low: 0.53, high: 0.6, color: 'pink' },
        { label: 'no data', value: 0.08, low: 0.06, high: 0.1, color: 'dim' },
    ], { width: 1100, labelSize: 40, domain: [0, 1], ticks: [0, 0.25, 0.5, 0.75, 1], xLabel: 'mean score, higher is better. Lines: 95% intervals' }).at(150, -40)
    s.play(chart.enter(), { duration: 2.4 })
    s.wait(0.8)
})
```

As with the other charts, `width` and `height` are the size of the plot, and the plot is centered on the chart. The labels are outside it, at the left, and the ticks are under it. Without a `domain`, the axis covers a round range around the data. `ticks` is a count or a list of values. `chart.rows` holds one node per item, with its label, its interval and its dot, and `chart.p(value, row)` gives a place on the chart for a bracket or a note. A value outside the domain, a value outside its interval, an interval with one end, and a tick outside the domain are errors.

`scale: 'log'` gives each power of ten the same room on the axis. Use it when the values differ by more than about a hundred times. On a linear axis, all of them but the largest sit at zero. On a log axis, a distance shows a ratio and not a difference, so say on screen that the axis is logarithmic. The `xLabel` is the place for that.

```film paired label="Five times, from 100 ns to 10 ms, on a log axis"
import { dotChart, film } from 'vizmatic/stage'

// About how long one operation takes, in nanoseconds.
const time = (ns: number) => (ns >= 1e6 ? `${ns / 1e6} ms` : ns >= 1e3 ? `${ns / 1e3} µs` : `${ns} ns`)

export default film({ fps: 30 }, (s) => {
    const chart = dotChart([
        { label: 'system call', value: 100 },
        { label: 'page fault', value: 1_000 },
        { label: 'read from a fast drive', value: 20_000, color: 'teal' },
        { label: 'round trip in a data center', value: 500_000, color: 'purple' },
        { label: 'read from a spinning disk', value: 10_000_000, color: 'orange' },
    ], { scale: 'log', width: 1000, labelSize: 40, format: time, xLabel: 'time for one operation, logarithmic' }).at(250, -40)
    s.play(chart.enter(), { duration: 2.4 })
    s.wait(0.8)
})
```

On a log axis, the default domain is the powers of ten around the data: values from 80 to 12,000,000 give an axis from 10 to 100,000,000. The default ticks are the powers of ten in the domain. When there are more than eight, every second or third power gets a tick, and the two ends keep theirs. A number for `ticks` asks for about that many. A list of `ticks` and a `domain` are used as they are. The default format is short: 1, 10, 100, 1k, 10k, 1M, 1G, and 0.1, 0.01, 0.001 below one. Pass `format` for a unit, as in the example. `chart.p(value, row)` gives the place of a value on the log axis, and `chart.domain` holds the two ends as values.

When the domain has three powers of ten or fewer, the grid also has fainter lines at 2 to 9 times each power. The axis must be wide enough to leave 12 px between the two closest lines. A log axis has no zero. A value, an interval end, a domain end, or a tick that is 0 or less is an error.

`unitChart(groups)` draws one mark for each unit, so the viewer sees a count and does not only read it. Use it for a number of cases, runs, hosts, or files, up to about 2,000. A group has a `count`, a `color`, and a `label`.

```film paired label="330 runs arrive, take the color of their result, and gather into three blocks"
import { film, unitChart } from 'vizmatic/stage'

export default film({ fps: 30 }, (s) => {
    const chart = unitChart([
        { count: 262, label: '262: no effect' },
        { count: 39, color: 'green', label: '39 better' },
        { count: 29, color: 'red', label: '29 worse' },
    ], { rows: 11, mixed: true, plain: true, cell: 22, labelSize: 34 })
    s.play(chart.enter(), { duration: 1.4 })
    s.play(chart.paint(), { duration: 0.8 })
    s.wait(0.4)
    s.play(chart.rearrange({ split: true }), { duration: 1.6 })
    s.wait(0.8)
})
```

The chart has three arrangements. By default the groups follow each other in one grid, so each group is one part of the whole. `mixed: true` puts the marks of all groups at mixed places in that grid, to show a few among many. The same `seed` gives the same places. `split: true` draws each group as its own block, with its label over it. The labels show only while the groups are split. For the other two arrangements, add a `legend()`. `rows` sets the marks in one column, and the chart fills column by column. `columns` sets the marks in one row, and the chart fills row by row. `cell` is the side of one mark, and `gap` the room between two marks.

`chart.enter()` brings the marks in line by line. `plain: true` starts every mark and label in one color, and `chart.paint()` gives each group its color. `chart.rearrange(arrangement)` moves the marks to another arrangement. It keeps the rows or columns and the seed that the new arrangement does not name. `split` and `mixed` turn each other off, and `rows` replaces `columns`. Each group moves over the whole time of the move, so a small group does not wait for a large one.

`.at()` places the middle of the grid, or of the row of blocks. A block is as wide as its marks or its label, so a row of blocks is wider than the grid: check both arrangements. A group with a count of 0 and a label keeps the room of its label, such as "0 failed". `chart.marks` holds every mark, `chart.groups` the marks of each group, and `chart.p(index)` the place of a mark. `shape: 'square'` draws squares. A chart of more than 20,000 marks is an error: let one mark stand for more than one unit, and say so in a label. Above about 2,000 marks, a move reads as a wipe.

## Graphs and networks

`graph(nodes, edges)` draws a node-link diagram, and `tree(root)` lays out a tree. Edges work with `pulse`, so a signal can run along them.

```film paired label="A signal pulses through a small tree"
import { fadeIn, film, pulse, seq, tree } from 'vizmatic/stage'

export default film({ fps: 30 }, (s) => {
    const t = s.add(tree({ label: 'a', children: [
        { label: 'b', children: [{ label: 'd' }, { label: 'e' }] },
        { label: 'c', children: [{ label: 'f' }] },
    ] }, { levelGap: 210, siblingGap: 170, radius: 46, labelSize: 44 }))
    s.play(fadeIn(t))
    s.play(seq(...t.edges.map((edge) => pulse(edge, { duration: 0.4 }))))
    s.wait(0.4)
})
```

## Pictograms

`pictogram(name)` draws a small picture of a concept: a time series, a forecast, logs, traces, a graph, a database, a model. All pictures share one 24-unit grid and one line style, so pictures in a row read as one family. `color` sets the lines, and `accent` sets the part that carries the meaning. A picture with one part, such as `series` or `check`, is all in the accent. `.at(x, y)` places the middle of the grid, so pictures of one `size` line up.

```film paired label="A row of pictograms for telemetry, structure, and systems"
import { draw, fadeIn, film, lagged, pictogram, text } from 'vizmatic/stage'

export default film({ fps: 30 }, (s) => {
    const names = ['series', 'forecast', 'logs', 'traces', 'graph', 'dag', 'database', 'model'] as const
    const pictures = names.map((name, i) => pictogram(name, { size: 150, accent: 'teal' }).at((i - 3.5) * 220, -40))
    const labels = names.map((name, i) => text(name, { font: 'mono', size: 32, color: 'dim' }).at((i - 3.5) * 220, 110))
    s.play(lagged(pictures.map((picture) => draw(picture)), { lag: 0.12 }), { duration: 2 })
    s.play(fadeIn(labels))
    s.wait(0.8)
})
```

`PICTOGRAMS` lists every name:

- Telemetry: `series`, `chart`, `forecast`, `threshold`, `anomaly`, `logs`, `traces`, `bars`, `table`, `matrix`, `event`, `alert`.
- Structure: `graph`, `dag`, `tree`, `layers`, `pipeline`, `branch`.
- Systems: `service`, `database`, `queue`, `cloud`, `chip`.
- Machines: `vm`, `process`, `kernel`, `memory`, `disk`, `nic`, `network`, `snapshot`, `clone`, `jail`, `wall`, `key`, `stopwatch`, `bolt`.
- Models: `model`, `tokens`, `vector`, `agent`, `spark`.
- Work: `dashboard`, `terminal`, `container`, `experiment`, `target`, `loop`, `eye`, `message`, `shield`.
- General: `document`, `code`, `gear`, `clock`, `user`, `search`, `lock`, `deploy`, `check`, `cross`, `warning`, `question`.

Use a pictogram when a card or a legend needs a picture of a kind of thing. When the picture must show real data or a real topology, use `sparkline`, `graph`, or `tree`, and size it with `fitTo`.

## Cards

`card(options)` draws a box with a `title` at the top, a `figure` in the middle, and a `caption` at the bottom. All three are optional. The title and the caption sit at fixed distances from the border. The figure is centered in the space between them, and a figure that is too large is scaled down to fit. So the cards of a row share one layout whatever they hold: give them one `width`, one `height`, and one `captionLines`, which is the number of lines of the longest caption.

```film paired label="Three cards with one layout hold three different figures"
import { card, fadeIn, film, group, lagged, pictogram, sparkline } from 'vizmatic/stage'

export default film({ fps: 30 }, (s) => {
    const values = Array.from({ length: 48 }, (_, i) => Math.sin(i / 5) + 0.4 * Math.sin(i / 1.7))
    const row = { width: 500, height: 460, captionLines: 2, font: 'text-serif' } as const
    const cards = [
        card({ title: 'Metrics', figure: sparkline(values, { width: 380, height: 150, color: 'teal', weight: 5 }), caption: 'a number every ten seconds', color: 'teal', ...row }),
        card({ title: 'Logs', figure: pictogram('logs', { size: 170, color: 'dim', accent: 'yellow' }), caption: 'one line per event', color: 'yellow', ...row }),
        card({ title: 'Traces', figure: pictogram('traces', { size: 170, color: 'dim', accent: 'purple' }), caption: 'the path of one request through the services', color: 'purple', ...row }),
    ]
    group(...cards).arrange('right', 40).moveTo([0, 0])
    s.play(lagged(cards.map((made) => fadeIn(made, { shift: [0, 24] })), { lag: 0.25 }), { duration: 1.6 })
    s.wait(0.8)
})
```

A caption that is too wide wraps, and a title that is too wide is set in a smaller size, down to 0.6 of `titleSize`. Without a `height`, a card is as tall as its content. Content that cannot fit is an error: a `height` with no room for the figure, a title that is too long, or a caption word that is wider than the card. So a figure never runs into the text. Give the figure to one card only, and before it is on the stage.

`card.frame`, `card.title`, `card.figure`, and `card.caption` are the parts. `card.figureBox` is the space that the figure may use, in the coordinates of the card, where (0, 0) is its middle. `titleSize`, `captionSize`, `padding`, `font`, `color`, `captionColor`, `fill`, `radius`, and `borderWidth` set the look.

## Zones

`zone(content, options)` draws a border around one node or several, with a `title` on the border: a host, a network, a sandbox, a boundary of trust. The zone measures its content when it is built and takes the content as its children. So the zone and what it holds move, fade, and scale as one node, and a zone can hold other zones. `dash: true` draws a dashed border, for a boundary that is a rule and not a wall.

```film paired label="A process inside a jail inside a host, built from the inside out"
import { draw, fadeIn, film, group, rect, text, zone } from 'vizmatic/stage'

export default film({ fps: 30 }, (s) => {
    const box = (name: string, color: string) => group(
        rect(300, 110, { radius: 14, stroke: color, fill: 'surface', width: 3 }),
        text(name, { size: 38, font: 'text-serif', color }),
    )
    const devices = box('devices', 'purple').at(440, 0)
    const guest = zone([box('guest kernel', 'teal').at(0, -75), box('your code', 'ink').at(0, 75)], { title: 'microVM', color: 'teal', titleSize: 32 })
    const process = zone([guest, devices], { title: 'VMM process', color: 'purple', titleSize: 32, padding: 48 })
    const jail = zone(process, { title: 'jail', color: 'yellow', dash: true, titleSize: 32, padding: 52 })
    const host = s.add(zone(jail, { title: 'host', titleSize: 32, padding: 56, titleAlign: 'center' }).at(0, 0))
    // Everything is on the stage now. Hide what arrives later, then reveal it from the inside out.
    const layers = [guest, process, jail, host]
    for (const node of [devices, ...layers.flatMap((layer) => [layer.frame, layer.title!])]) node.hide()
    s.play(fadeIn(guest.content, { lag: 0.2 }), { duration: 0.8 })
    s.play(draw(guest.frame), fadeIn(guest.title!), { duration: 0.7 })
    s.play(fadeIn(devices), draw(process.frame), fadeIn(process.title!), { duration: 0.7 })
    s.play(draw(jail.frame), fadeIn(jail.title!), { duration: 0.7 })
    s.play(draw(host.frame), fadeIn(host.title!), { duration: 0.7 })
    s.wait(0.8)
})
```

Build the content where you want it, then the zone, then add the zone to the stage. The zone stays where its content was built, and `.at(x, y)` places the middle of its border, as for a card. A node of the content that is already on the stage or in a group is an error. After the zone is built, do not add a node of its content to the stage or to another group: that takes the node out of the zone.

The zone is wide enough for its title. `minWidth` and `minHeight` give the zones of a row one size. `padding` is the space between the content and the border. The border has a gap for the title, so no line crosses the title. A `fill` is faint by default, because its top edge runs behind the title. `zone.frame`, `zone.title`, `zone.backdrop` (the fill), and `zone.content` are the parts. `zone.box` is the middle line of the border, in the coordinates of the zone.

## Tags

`tag(word, options)` draws a word in a pill, with a `note` beside it. Use it to say what kind of claim is on the screen and where it comes from: measured, plan, judgment, with the source and its date. `.at(x, y)` places the left end of the pill, so tags with words of different lengths start at one x.

`tagTrack(states, { until })` is a tag that changes with time, at one place. Each state has the time when it starts. One state is on screen at a time: a state fades out before the next one fades in. States that are out of order, or too near to each other for the two fades, are an error.

```film paired label="A tag in the corner changes from a plan to a result when the chart arrives"
import { barChart, fadeIn, fadeOut, film, tagTrack, text } from 'vizmatic/stage'

export default film({ fps: 30 }, (s) => {
    s.add(tagTrack([
        { at: 0, word: 'PLAN', color: 'blue', note: 'the design of March' },
        { at: 2.4, word: 'MEASURED', color: 'green', note: 'the run of June, on 926 questions' },
    ], { until: 5.4, size: 36 }).at(-900, -470))
    const plan = text('Does the new format cost fewer tokens?', { font: 'text-serif', size: 64 })
    s.play(fadeIn(plan), { duration: 0.8 })
    s.wait(1)
    s.play(fadeOut(plan), { duration: 0.5 })
    const chart = barChart([{ label: 'old', value: 21 }, { label: 'new', value: 4 }], { width: 900, height: 560 }).at(0, 60)
    s.play(fadeIn(chart, { shift: [0, 24] }), { duration: 0.8 })
    s.wait(1.8)
})
```

Place a track as any node. It does not know the size of the frame: `.at(-912, -490)` is the top left of a 1920 by 1080 frame. A track is pinned to the screen, so a move or a zoom of the camera does not move it. `tag.pill`, `tag.word`, and `tag.note` are the parts of a tag, and `tag.color(c)` recolors the border and the word.

## State grids

`stateGrid(options)` draws a grid of cells, and each cell is in a named state: the pages of a memory, the blocks of a disk, the bits of a bitmap, the hosts of a fleet. The state sets the look of a cell. `grid.change(which, state)` moves cells to another state, one after another in index order, so the viewer sees a sweep.

```film paired label="Pages arrive, a sweep reads most of them into memory, and some of those get dirty"
import { fadeIn, film, stateGrid } from 'vizmatic/stage'

export default film({ fps: 30 }, (s) => {
    const pages = stateGrid({ columns: 24, rows: 10, cell: 56, initial: 'absent' }).at(0, -40)
    const key = pages.legend([['absent', 'not in memory'], ['present', 'in memory'], ['dirty', 'changed since the last write']], { size: 36, font: 'text-serif' })
    key.at(pages.left[0], 370)
    s.play(pages.enter(), fadeIn(key), { duration: 1.2 })
    s.play(pages.change((index) => index < 168, 'present'), { duration: 1.6 })
    s.play(pages.change((index, state) => state === 'present' && index % 5 === 2, 'dirty'), { duration: 1.2 })
    s.wait(0.8)
})
```

`columns` and `rows` set the shape, `cell` the side of one cell, and `gap` the room between two cells. `.at()` places the middle of the grid. With a `cell` and a `gap` that are whole numbers, the edges of the cells are on whole pixels. The cells are numbered row by row from the top left, so the cell of a row and a column has the index `row * columns + column`. `initial` gives the first states: one name for every cell, a list with one name per cell, or a function of the index. A grid of more than 5,000 cells is an error: let one cell stand for more than one unit, and say so in a label.

A state has a `color`, a `fill`, and a `dash`. `fill` is the opacity of the fill: 0.7 by default, and 0 draws the outline only. `dash: true` draws a dashed outline, for a thing that is not there. Without `states`, the grid has the states of `PAGE_STATES`, for pages of memory: `absent`, `zero`, `present`, `dirty`, `shared`, `copied`, `file`, and `swapped`. For another subject, give your own record, such as `states: { healthy: { color: 'green' }, draining: { color: 'yellow', fill: 0.3 }, down: { color: 'red', fill: 0, dash: true } }`. Without `initial`, every cell starts in the first state of the record.

In `grid.change(which, state)`, `which` is an index, a list of indexes, or a function of the index and the state of a cell. The grid calls the function when the film plays the change. So in `seq(grid.change([0, 1, 2], 'dirty'), grid.change((_, state) => state === 'dirty', 'copied'))`, the second change finds the cells that the first one made dirty. The fill, its opacity, and the outline fade to the new look, and the dashes switch half way. The whole change takes `duration`. By default, each cell takes three tenths of it, so the cells overlap and the sweep is smooth. `lag` sets the delay between two cells, as a fraction of the time of one cell. A cell that already has the state does not change and takes no time. An unknown state and an index outside the grid are errors where the film makes the change.

`grid.stateOf(index)`, `grid.count(state)`, and `grid.indexes(state)` read the states after the changes that the film has played so far, so a label can show a true count, such as `${grid.count('dirty')} dirty pages`. `grid.enter()` brings the cells in row by row. `grid.cells` holds one node per cell, and `grid.p(index)` gives the middle of a cell in the coordinates of the grid, for an arrow or a note.

`grid.legend(items)` makes a key: one sample cell and one name for each state, in one row. The key is a node of its own, so add it to the stage and place it. `.at(x, y)` places the left end of the row, and `y` is the middle line of the cells and the names. An item is the name of a state, or a `[state, label]` pair for a display name. Without `items`, the key lists the states that the grid has now, in the order of `states`. `size`, `gap`, and `font` set the look. The key does not wrap, so make two keys for many long names.

## Code and tokens

`codeBlock(source)` draws syntax-colored code as glyphs. `morphCode(a, b)` morphs one version into another, and tokens that stayed travel to their new places. `tokens(items)` draws a row of labeled cells for language models, and `attentionMatrix` draws a heatmap with labels.

`block.find(fragment)` returns the glyphs of a piece of the code, to color them or to move them. A fragment that is one token, such as `lo`, matches every such token. Any other fragment, such as `lo + hi`, matches as text inside a line, and spaces do not count. Pass a line, counted from 0, as the second argument to search that line only.

```ts
import { codeBlock, film } from 'vizmatic/stage'

export default film((s) => {
    const block = s.add(codeBlock('const mid = (lo + hi) / 2', { size: 60 }))
    for (const glyph of block.find('lo + hi')) glyph.color('red')
    s.wait(1)
})
```

```film paired label="One line of code morphs into a longer version"
import { codeBlock, film, morphCode, write } from 'vizmatic/stage'

export default film({ fps: 30 }, (s) => {
    const a = s.add(codeBlock('const area = w * h', { size: 70 }))
    const b = codeBlock('const area = (w * h) / 2', { size: 70 })
    s.play(write(a))
    s.play(morphCode(a, b))
    s.wait(0.8)
})
```

The `language` option names the syntax. The languages are `ts`, `python`, `rust`, `go`, `java`, `c`, `sql`, `bash`, and `plain`, and four languages for specifications and proofs: `tla` for TLA+, `dafny`, `lean`, and `smt` for SMT-LIB. In those four, a prime such as the one in `x'` is part of the code and does not start a string. An unknown name uses the TypeScript rules.

`arrayCells(values)` draws a row of cells for algorithms, and `neuralNet(layers)` draws a layered network whose edges you can pulse.

## Images

Prepare an image once, then place it with the existing `image` stage primitive.
Preparation validates its encoded bytes, dimensions and optional SHA-256 digest.
The returned asset is immutable and embeds its pixels, so a rendered film does
not depend on a filename or a network request.

```ts
import { fileURLToPath } from 'node:url'
import { film, image, resolveImageAsset } from 'vizmatic/stage'

const figure = await resolveImageAsset('./assets/figure.png', {
    baseDir: fileURLToPath(new URL('.', import.meta.url)),
    id: 'figure-1',
})

export default film((s) => {
    s.add(image(figure, { width: 1200, height: 700 }).at(0, -40))
    s.wait(2)
})
```

`resolveImageAsset` accepts a local filename in Node, a `Uint8Array`, or a PNG
base64 data URI. Browser callers pass embedded bytes or a data URI. It awaits
browser decoding before returning. It does not fetch remote URLs.

The supported format is a noninterlaced, 8-bit PNG, including RGB, RGBA,
grayscale and palette images. Transparency is retained. Encoded input is limited
to 16 MiB and decoded dimensions to 16,777,216 pixels. Animated PNGs and compressed
color profiles are rejected. Convert other image formats or PDF pages to a local
PNG before preparing them.

For a pinned asset manifest, pass `sha256`, `mediaType: 'image/png'`, `width` and
`height` in the preparation options. Width and height here are the expected
intrinsic pixel dimensions. The resolved asset reports its actual digest and
dimensions. A missing file, invalid PNG, wrong media type, dimension mismatch or
digest mismatch throws `ImageAssetError`.

### Fit, crop and transform

`image(asset, options)` returns a node centered on its origin. Its normal node
methods move, rotate, scale and fade it. `opacity` remains an image option too.

- `width` and `height` describe the destination box in stage units. A single
  dimension preserves the image proportions.
- `fit: 'contain'` is the default. It fits inside that box without stretching.
- `fit: 'cover'` fills the box by cropping the center of the source region.
- `crop: { x, y, width, height }` selects intrinsic source pixels before fitting.
  The crop must stay inside the image.

```ts
import { image, resolveImageAsset } from 'vizmatic/stage'
const figure = await resolveImageAsset('./assets/figure.png')
const detail = image(figure, {
    width: 900,
    height: 500,
    crop: { x: 100, y: 80, width: 800, height: 400 },
    fit: 'contain',
    opacity: 0.9,
})
```

The existing `image(src, { width, height, opacity })` constructor remains
available. Compilation awaits and embeds PNG, JPEG and GIF sources. GIF playback uses the first decoded frame for export. Relative local
filenames use the process working directory; explicit preparation with `baseDir`
makes the dependency clearer. Use a prepared asset for fit and crop options.
HTTP and HTTPS image strings remain supported and are fetched once during compilation. Set `imageNetwork: 'deny'` on the film or in compile overrides to require offline inputs. Node also honors `VIZMATIC_DISABLE_NETWORK=1` and `VIZMATIC_OFFLINE=1`. Requests have a ten-second timeout and the same encoded-byte limit. Missing files, failed HTTP requests and decoder errors are fatal. Use prepared PNG assets for reproducible local inputs.

### Migrating browser URL images in 0.8

Browser URL images must be fetchable under CORS. A cross-origin server must
permit the film's origin so compilation can read, validate and embed its bytes.
Earlier players could display a non-CORS image while tainting the canvas. That
display-only path now fails with a loading error. Copy the image into local
assets or embed its bytes and call `resolveImageAsset` for a reproducible film.
Use `imageNetwork: 'deny'` when the film must stay offline.

### Preflight and checks

Compilation awaits static and keyed image sources, including hidden pictures.
Use prepared assets when constructing nodes inside dynamic drawing callbacks.
An image discovered by a later dynamic frame uses the same awaited asset path
as other deferred stage resources, rather than painting an empty placeholder.

SVG and Node exports carry the embedded bytes. Browser playback uses the decoded
asset before its first paint. `paintFrame` outside a browser needs an `ImageLookup`
that supplies decoded pixels; an absent source throws instead of silently
dropping the image.

`checkFilm` reports an image preparation failure as `film.image_asset`, an error.
Image destination bounds now participate in `frameInkBounds`, blank-content
checks and `film.off_canvas` diagnostics. Fully transparent PNGs and crops do not
count as visible content; opaque white paper does. Legacy PNGs retain 16-bit and
Adam7 support with the same bounded scanline preflight. The checks inspect the image rectangle;
they do not read text inside raster pixels. Keep citations and captions as stage
text so ordinary text checks can inspect them.

Asset discovery and source citation belong to the calling application. Include
the image bytes and any source-document or preprocessing manifest in its render
fingerprint. Preparing an asset establishes its pixel identity, not permission to
use a source or the correctness of its caption.

# Guide: Presentations

## Chapters are steps

`s.chapter(title)` marks a point on the timeline. The player shows chapters as ticks on its timeline, and film pages list them.

With the `stepped` attribute, playback stops at each chapter. The right arrow key plays to the next chapter, and the left arrow key returns to the previous one.

```film open controls label="A short talk of three steps that stops at each chapter"
import { fadeIn, film, morphMatching, tex, text, write } from 'vizmatic/stage'

export default film({ title: 'A short talk' }, (s) => {
    s.chapter('The claim')
    const title = s.add(text('Why squares add up', { font: 'text-serif', size: 110 }).at(0, -300))
    s.play(fadeIn(title, { shift: [0, 24] }))
    const eq = s.add(tex('a^2 + b^2 = c^2', { size: 150 }))
    s.play(write(eq))
    s.wait(0.2)

    s.chapter('Rearrange')
    const next = tex('c^2 - a^2 = b^2', { size: 150 })
    s.play(morphMatching(eq, next))
    s.wait(0.2)

    s.chapter('Factor')
    const last = tex('(c - a)(c + a) = b^2', { size: 150 })
    s.play(morphMatching(next, last))
    s.wait(0.5)
})
```

Add the attribute when you embed the film.

```html
<vz-film src="./talk.js" stepped label="A short talk"></vz-film>
```

## Keep a larger look

For a room, use larger text and thicker strokes in the film itself, and keep one idea per chapter. Pair a film with a [dense figure](diagrams.md) only if people will read it afterwards.

## Keyboard

| Key | Action |
|---|---|
| Space or `k` | Play or pause. |
| Right arrow | Next step. Without `stepped`, seek forward 5 seconds. |
| Left arrow | Previous step. Without `stepped`, seek back 5 seconds. |
| `f` | Full screen. |

## Hand it out

Export the same film as an MP4 for people who missed the talk. Use `--frame` to get stills for the slides you still need.

```bash
npx vizmatic film talk.ts --out ./out --quality high
npx vizmatic film talk.ts --frame 2.5,7,12 --out ./stills
```

# Guide: Text and math

## Text

`text(content, options)` lays out a block of glyphs. The node's position is the middle of the block.

```film paired label="A serif headline and a smaller sans line write themselves"
import { film, text, write } from 'vizmatic/stage'

export default film({ fps: 30 }, (s) => {
    const head = s.add(text('Every glyph is a path.', { font: 'text-serif', size: 170 }).at(0, -80))
    const body = s.add(text('So every letter can move.', { size: 62, color: 'dim' }).at(0, 120))
    s.play(write(head), { duration: 1.6 })
    s.play(write(body), { duration: 1 })
    s.wait(0.8)
})
```

| Option | Meaning |
|---|---|
| `size` | Font size in stage units. |
| `font` | `'sans'` (Inter), `'serif'` (Instrument Serif), `'text-serif'` (Source Serif 4), `'mono'` (JetBrains Mono), or `'noto'`. |
| `weight` | `400`, `600`, or `700` where the family has that weight. |
| `italic` | Use the italic of the family. |
| `color` | A look token, CSS color, or gradient. |
| `align` | `'left'`, `'center'`, or `'right'` inside the block. |
| `anchor` | The point that `.at(x, y)` places: `'left'`, `'center'` (the default), or `'right'`. |
| `maxWidth` | Wrap to this width. |
| `leading` | Line height as a multiple of size. |
| `tracking` | Extra letter space as a fraction of size. |

`.at(x, y)` places the middle of a text block. For a label that starts or ends at a known x, such as a row of a table or the name of an axis, set `anchor`. The y of the point does not depend on the letters, so labels with different anchors on one `y` share a baseline.

```ts
import { text } from 'vizmatic/stage'

const metric = text('latency', { size: 32, anchor: 'right' }).at(-20, 0)   // ends at x = -20
const value = text('5.0 ms', { size: 32, anchor: 'left' }).at(20, 0)       // starts at x = 20
```

Use `'text-serif'` for sentences, labels, and numbers. Its strokes stay visible when a film plays at phone size, and it keeps `1` and `l`, and `O` and `0`, easy to tell apart. It also sits well next to TeX. Use `'serif'` for short titles at a large size. It is narrow and has fine strokes, and it has no Greek letters or math symbols.

If a font does not have a character, the engine draws that character with the next bundled font. `'serif'` tries Source Serif 4 first. Then every family tries Noto Sans and Noto Sans Math. If no bundled font has the character, the engine draws an empty box and `vizmatic check` reports `film.missing_glyph`.

Glyphs are in `node.chars`. `node.find('word')` returns the glyphs of a fragment, so you can color or animate one word.

```film paired label="One word turns yellow inside a sentence"
import { film, recolor, text } from 'vizmatic/stage'

export default film({ fps: 30 }, (s) => {
    const line = s.add(text('Look at this word.', { font: 'text-serif', size: 170 }))
    s.wait(0.5)
    s.play(...line.find('this').map((glyph) => recolor(glyph, 'yellow')))
    s.wait(1)
})
```

## Equations

`tex(source, options)` typesets TeX with a bundled engine, so you do not install LaTeX. The output is vector glyphs in the Computer Modern style.

```film paired label="The quadratic formula handwrites itself"
import { film, tex, write } from 'vizmatic/stage'

export default film({ fps: 30 }, (s) => {
    const eq = s.add(tex('x = \\frac{-b \\pm \\sqrt{b^2 - 4ac}}{2a}', { size: 130 }))
    s.play(write(eq, { duration: 2.4 }))
    s.wait(1)
})
```

The math engine is a separate download of about 1.6 MB in the browser. It loads only when a film first calls `tex`.

## Selecting terms

`eq.part('b^2')` returns the glyphs of a fragment. `eq.parts('x')` returns every occurrence. Use them to color a term or point at it.

```film paired label="Two terms of an equation change color"
import { film, recolor, tex } from 'vizmatic/stage'

export default film({ fps: 30 }, (s) => {
    const eq = s.add(tex('a^2 + b^2 = c^2', { size: 170 }))
    s.wait(0.4)
    s.play(...eq.part('a^2').map((glyph) => recolor(glyph, 'teal')))
    s.play(...eq.part('b^2').map((glyph) => recolor(glyph, 'orange')))
    s.wait(0.8)
})
```

## Derivations

`morphMatching` matches glyphs by what they are. A line of a derivation can grow out of the line before it.

```film paired label="An equation is solved over three morphs"
import { film, morphMatching, tex, write } from 'vizmatic/stage'

export default film({ fps: 30 }, (s) => {
    const step = (source: string) => tex(source, { size: 170 })
    const a = s.add(step('2x + 3 = 11'))
    const b = step('2x = 11 - 3')
    const c = step('2x = 8')
    const d = step('x = 4')
    s.play(write(a))
    s.play(morphMatching(a, b))
    s.play(morphMatching(b, c))
    s.play(morphMatching(c, d))
    s.wait(0.8)
})
```

## Readouts

`decimal(read, options)` draws a number that follows a function of the timeline. Use the `mono` font so digits do not jump.

# Guide: 3D

## A scene and a camera

`scene3d(options)` creates a view. Add 3D objects to it, and add the scene to the stage. `phi` is the angle down from straight overhead, and `theta` is the rotation around the vertical axis. Both live on the timeline.

```film paired label="A wave-like surface turns under a rotating camera"
import { axes3d, fadeIn, film, scene3d, surface } from 'vizmatic/stage'

export default film({ fps: 30 }, (s) => {
    const view = s.add(scene3d({ phi: 1.1, theta: -0.6, unit: 130 }))
    view.add(axes3d({ x: [-3, 3], y: [-3, 3], z: [-1, 2] }))
    const hill = surface((x, y) => Math.sin(x) * Math.cos(y), {
        x: [-3, 3], y: [-3, 3], resolution: 36, colors: ['blue', 'teal', 'yellow'],
    })
    view.add(hill)
    s.play(fadeIn(hill))
    s.play(view.orbit({ theta: 1.4 }, { duration: 4 }))
})
```

## Surfaces

`surface(f)` draws a height field. `parametricSurface(fn, u, v)` draws any shape from two parameters, and `sphere(radius)` draws a sphere. Faces sort from back to front on every frame, and they shade from a light you can move.

```film paired label="A shaded torus turns slowly"
import { fadeIn, film, parametricSurface, scene3d } from 'vizmatic/stage'

export default film({ fps: 30 }, (s) => {
    const view = s.add(scene3d({ phi: 1, theta: 0.4, unit: 170 }))
    const torus = parametricSurface((u, v) => [
        (2 + 0.8 * Math.cos(v)) * Math.cos(u),
        (2 + 0.8 * Math.cos(v)) * Math.sin(u),
        0.8 * Math.sin(v),
    ], [0, Math.PI * 2], [0, Math.PI * 2], { resolution: 36, colors: ['purple', 'blue'] })
    view.add(torus)
    s.play(fadeIn(torus))
    s.play(view.orbit({ theta: Math.PI }, { duration: 5 }))
})
```

## Curves, points, and trails

`curve3d(f, from, to)` draws a curve in space. `line3d`, `vector3d`, `points3d`, and `point3d` draw marks. `trail3d(point, from, to)` keeps the recent path of a moving point, and `label3d` places text that faces the camera.

```film paired label="A helix draws itself in space"
import { axes3d, curve3d, draw, film, scene3d } from 'vizmatic/stage'

export default film({ fps: 30 }, (s) => {
    const view = s.add(scene3d({ phi: 1.15, theta: -0.8, unit: 120 }))
    view.add(axes3d({ x: [-3, 3], y: [-3, 3], z: [-3, 3] }))
    const helix = curve3d((t) => [Math.cos(t) * 2, Math.sin(t) * 2, t / 4 - 1.5], 0, Math.PI * 6, { color: 'yellow', width: 5 })
    view.add(helix)
    s.play(draw(helix, { duration: 3 }))
    s.wait(0.6)
})
```

## Why it uses no GPU

Each frame projects the 3D objects to ordinary paths. The same code paints to a canvas in the browser and to a rasterizer during export, so the two match. Large meshes cost more than they would on a GPU. Keep `resolution` near what the frame needs, because a mesh of 40 by 40 cells is 1600 faces.

See the [gradient descent film](../films.html) for a surface with real iterates, and the [Lorenz film](../films.html) for trails.

# Guide: TSX animation and MP4

This page covers animation of TSX figures with `createAnimation(theme)`. It suits a figure that already exists and needs to change over time. For new motion work, use a [film](getting-started.md), which draws vector objects directly and renders many times faster.

Export an existing TSX animation directly with `vizmatic video`:

```bash
vizmatic video ./lesson.tsx --out ./videos --theme dark --fps 30
vizmatic video ./lesson.tsx --out ./videos --theme dark,light --fps 60 --audio narration.wav
```

The module exports `width`, `height`, and `createAnimation(theme)`, just like the
GIF examples. The video command samples the timeline at exact video timestamps,
renders each React scene, and streams it to FFmpeg. It writes `lesson_dark.mp4`
and a `video-manifest.json`. Use `--crf 18` for crisp text, `--preset medium` for
a smaller file, and `--scale 2` for twice the pixel dimensions. FFmpeg with
libx264 is required; audio export also needs AAC. MP4 requires an opaque
background, with the theme background used by default. Set `--background` to
an opaque CSS color to choose another fill. The browser builder exports GIF;
MP4 encoding runs in Node.js.

### Render a complete video example

The short [request lifecycle starter](https://github.com/bvolpato/vizmatic/blob/main/examples/videos/request-lifecycle.tsx)
shows one request moving through a three-step workflow. It uses only Vizmatic's
published package and keeps the motion tied to the work being explained. The
longer [From noise to signal film](https://github.com/bvolpato/vizmatic/blob/main/examples/videos/signal-to-story.tsx) shows a
chart, synthetic observations, and a fitted trend.

In a Node.js 20 or newer project, install Vizmatic and React. [Install FFmpeg](https://ffmpeg.org/download.html)
separately and check that its encoder list includes `libx264`. Audio output
also needs the `aac` encoder.

For a fresh app, add the [consumer security overrides](https://github.com/bvolpato/vizmatic#consumer-security-overrides)
before installing.

```bash
npm install --save-dev vizmatic@0.6.0 react
npx vizmatic video ./request-lifecycle.tsx --out ./dist/videos --theme dark --fps 30
```

The CLI writes `request-lifecycle_dark.mp4` and `video-manifest.json` under
`./dist/videos`. Add narration with `--audio ./narration.wav`; the audio is
trimmed or padded to the animation duration and encoded as AAC. Vizmatic renders
the picture and optional audio. Keep `.vtt` or `.srt` captions as separate files
and synchronize their cues to the finished timeline. The downloadable source
and a copyable command are also on the [website video example](https://bvolpato.github.io/vizmatic/#video).

The same workflow is available through the API:

```typescript
import { renderAnimationVideo } from "vizmatic"
import { createAnimation, width, height } from "./lesson"

await renderAnimationVideo(createAnimation("dark"), {
  width, height, fps: 30, theme: "dark",
  outputPath: "videos/lesson.mp4",
  crf: 18, preset: "medium",
  // audioPath: "narration.wav",
})
```

Video supports 1–120 fps independently of the GIF cadence. Canvas dimensions
after scaling must be positive even integers. Every frame is checked for
overflow; failures report its timestamp and timeline label and preserve an
existing output file. `signal`, audio alignment, and streaming use the same
contracts as `renderRgbaVideo` below. Keep a finite film's last state visible
long enough to read. Use `parallel` tracks for overlapping transitions and
path helpers for continuous motion.

Watch **[From noise to signal](https://bvolpato.github.io/vizmatic/#video)**,
an 18-second motion design example built from
[`examples/videos/signal-to-story.tsx`](https://github.com/bvolpato/vizmatic/blob/main/examples/videos/signal-to-story.tsx).
Its chart fits actual least-squares coefficients to explicitly synthetic data.
The source demonstrates staggered entrances, arc-length line reveals,
parallel tracks, camera motion, and an ending held on screen. From a repository
checkout, generate the film, posters, captions, and downloadable source with:

```bash
pnpm install --frozen-lockfile
pnpm render:video
```

The repository's `pnpm render:video` script regenerates the showcase poster,
captions, downloadable sources, and MP4. `pnpm site:build` regenerates the
website preview and source downloads without encoding the MP4. Those generated
media files live under `docs/assets/video/` and are excluded from Git. Pages
builds the MP4 and verifies it with `pnpm docs:check --require-video` before
publication. The website loads the poster until playback is requested and
provides chapter controls and a text description.

`renderRgbaVideo` streams caller-rendered RGBA frames to FFmpeg as an H.264 MP4.
It supports 1–120 fps, even pixel dimensions, optional audio, and backpressure,
without retaining the whole film in memory. Install FFmpeg with libx264 and AAC
support separately. Canvas painters can use the optional `@napi-rs/canvas` package:

```typescript
import { createCanvas } from '@napi-rs/canvas'
import { renderRgbaVideo, pointAlongPath, tracePolyline } from 'vizmatic'

const canvas = createCanvas(1280, 720)
const ctx = canvas.getContext('2d')
const path = [{ x: 150, y: 550 }, { x: 640, y: 200 }, { x: 1130, y: 550 }]

await renderRgbaVideo((frame) => {
  ctx.fillStyle = '#101827'
  ctx.fillRect(0, 0, 1280, 720)
  const progress = Math.min(1, frame.time / 2000)
  const visible = tracePolyline(path, progress)
  ctx.beginPath()
  visible.forEach((point, i) => i ? ctx.lineTo(point.x, point.y) : ctx.moveTo(point.x, point.y))
  ctx.strokeStyle = '#63deec'
  ctx.lineWidth = 5
  ctx.stroke()
  const point = pointAlongPath(path, progress)
  ctx.beginPath()
  ctx.arc(point.x, point.y, 14, 0, Math.PI * 2)
  ctx.fillStyle = '#f4d477'
  ctx.fill()
  return canvas.data()
}, {
  width: 1280, height: 720, duration: 3000, fps: 30,
  outputPath: 'lesson.mp4',
  // audioPath: 'narration.wav',
})
```

Durations and `frame.time` are milliseconds, as in typed animation timelines.
`videoFrameSchedule(duration, fps)` lazily yields the same frame timestamps;
the endpoint is not emitted as an extra frame. The callback can be async and
must return exactly `width * height * 4` bytes. Each frame is consumed before
requesting the next. Optional audio is padded or trimmed to the video duration
and encoded as stereo AAC at 48 kHz and 384 kbit/s. `signal` cancels an export. A failed
callback or encoder removes temporary files and preserves an existing output.

Use `defineAnimation` and `sampleAnimation(animation, frame.time)` to drive
multiple properties from a shared timeline. Sample per frame rather than
building a GIF frame array for a long video.

Motion helpers operate on `{ x, y }` points:

- `tracePolyline(points, progress)` reveals a path by actual arc length.
- `pointAlongPath(points, progress)` locates a moving marker on the same path.
- `morphPolyline(from, to, progress, { samples, easing })` smoothly changes one
  path into another, including paths with different vertex counts. It matches
  equal arc-length positions, uses 64 samples and `smoothstep` by default,
  and preserves the inputs. Use it for trajectories and changing curves.
- `createPolylineMorph(from, to, options)` prepares the same arc-length
  correspondence once and returns a `(progress) => points` sampler. Reuse it
  across frames to avoid repeating path measurement and resampling.
- `arcSwap(from, to, progress, { lift, easing })` exchanges positions along
  opposite arcs. `lift` is the maximum perpendicular displacement, defaulting
  to 80; the default easing is `smoothstep`.
- `smoothstep(progress)` clamps and eases progress between zero and one.

Path results use fresh points and handle repeated vertices. Empty paths trace
to an empty array; following an empty path throws. These helpers let a lesson
draw a curve, trace a computation graph, or exchange token positions while
keeping the underlying numerical state explicit.

`createCanvasComponents(painter)` provides reusable panels, labeled cards,
matrices, token rows, and moving particles for a Canvas video renderer. Inject
the renderer's drawing interface, including its containment and opacity
scopes. Every component uses those methods, so host layout checks and shape
metadata remain available. The components use no raw Canvas operations or
raster intermediates.

```typescript
import { createCanvasComponents, type CanvasPainter } from 'vizmatic'

// The host supplies checked primitives. Scope callbacks must keep those
// checks active, and opacity must restore drawing state with try/finally.
function drawLesson<Shape>(painter: CanvasPainter<Shape>) {
  const scene = createCanvasComponents(painter)
  const node = scene.card({
    x: 40, y: 40, width: 160, height: 68, label: 'query',
    fill: '#18283b', stroke: '#63deec', textColor: '#fff',
  })
  scene.matrix({
    values: [[1, -2], [3, 4]], x: 240, y: 40, cellSize: 80,
    style: { fill: '#18283b', textColor: '#fff', fontSize: 26 },
    cellStyle: (value) => ({ labelOpacity: value < 0 ? 0.5 : 1 }),
  })
  scene.tokenRow({
    values: ['dogs', 'chase', 'cats'], centerX: 300, y: 240,
    fill: '#18283b', textColor: '#fff', reveal: 0.6,
  })
  return node // Original host geometry, usable for connector anchors.
}
```

Panels default to a container scope with 10 pixels of padding and a 14 pixel
corner radius; pass `scope: 'label'` for a label-only scope. Cards center their
label with a baseline offset of `fontSize * 0.34`. `opacity` affects the whole
card; `labelOpacity` reveals the text independently. Panel and card geometry
remains available at zero opacity. Finite opacity values are clamped to the
range from zero to one before composing fades or token reveals. Non-finite
opacity values throw. Matrices and token rows skip invisible
cells or items. Matrix `format(value, row, column)` and
`cellStyle(value, row, column)` customize individual cells without rebuilding
the component.

Panels and cards accept `focusTarget`, including through a matrix's `cellStyle`.
Flow-graph nodes and edges, timeline lanes and intervals, and allocation parts
accept the same field. Grids accept `cellFocusTarget(row, column)`. These call
the optional host `painter.target(id, draw)` around the actual painted object,
retaining the original geometry and all host checks.

`createFocusDirector(painter, { cues })` samples already timed semantic cues.
The host applies `director.paint(draw, originalAlpha)` to its checked primitives
and `director.target(id, draw)` to related marks. The active target retains its
authored brightness while surrounding marks dim. `minimumContextAlpha` can keep
fading context legible without making a mark brighter than its authored alpha.
`reveal(id, draw)` preserves geometry before a cue, `progress(id, fallback)`
drives a transformation, and completed reveals remain visible. Supply measured
cue times from the narration provider; the director does not estimate speech.

`focusFrame({ bounds, strength, avoid, clip })` adds corner annotations through
the checked painter. Pass visible label bounds in `avoid` to move annotations
away from text. Crowded annotations are omitted while the brightness cue remains.
`mergeFocusRegions(parts)` groups adjacent parts of one semantic object and
keeps distant parts separate. All focus state is sampled per frame, so seeking
and parallel frame rendering preserve the same result.

`layoutTokenRow(values, options)` returns `{ text, x, width }` items for shared
layout and connector placement. Automatic widths use the literal spelling;
pass `width` for fixed-width cards. `x` takes precedence over `centerX`.
`tokenRow` adds staggered reveal, with each item's opacity equal to
`clamp(reveal * itemCount - index, 0, 1)`. Use `particle({ points, progress,
fill })` to move a marker by route length; endpoints remain unpainted.

`createTeachingCanvas(painter)` adds checked axes, traced curves, scatter plots,
positive bar charts, and explicit-layout flow graphs. Supply a `TeachingPainter`
with the same checked methods plus `math`, `line`, `arrow`, `path`, and `connect`.
Its `connect` method returns `{ from, to }` endpoints. No component bypasses the
host's paint, containment, or opacity checks.

### Construct a result before revealing it

The teaching canvas also offers five components for explanations with persistent
objects and computed intermediate states:

| Method | State and drawing contract |
|---|---|
| `vectorAccumulation` | Accumulate ordered weighted vectors. Each term retains its ID, contribution, and endpoints. Use an equal-unit two-dimensional plot or declare a projection. `revealTotal` controls the displayed result independently of construction progress. |
| `categoricalSampling` | Map supplied uniform draws through a categorical CDF. Declare probabilities or weights, retain draw IDs, and expose the empirical distribution with a separate `showEmpirical` flag after all retained draws arrive. |
| `routedTransfer` | Plan an orthogonal route around declared obstacles. Clearance includes the stroke and moving marker. An impossible route fails before painting. |
| `byteDifference` | Compare `Uint8Array` values with stable byte offsets, escaped controls, and explicit gaps. Scanning and the final equality verdict have separate controls. |
| `tokenHistory` | Align prompt, accepted, processed, KV, and emitted occurrence IDs. Dropping and replaying KV preserve accepted output and rebuild the processed prefix. |

The corresponding pure helpers are `weightedVectorState`,
`categoricalSampling`, `planTeachingRoutedTransfer`, `byteDifferenceState`, and
`tokenHistoryState`. They sample explicit inputs and support out-of-order frame
rendering. The lesson supplies the data, model assumptions, and narration cues.

Provide `painter.measureText(value, size, font)` to size moving token copies with
the same font metrics used by `painter.text`. The helper adds the label's padding
and keeps the copy inside its occurrence column. Without this optional method,
the copy retains the full column width so a readable whole-word label does not
shrink into a fixed three-character card.

[Compare the same moving word token in v0.7.0 and v0.7.2.](../assets/teaching/token-replay-label-width.png)

Keep each component's scope visible in the explanation. The byte comparison
aligns a common prefix and suffix with a positional middle; it does not compute
a minimum-edit diff or establish artifact provenance. Categorical sampling
uses the supplied draws and performs no hidden random experiment. Token
history models ordinary autoregressive decoding with at most one accepted
token awaiting processing. Speculative decoding needs its own verification
contract. `assertTokenHistoryTransition` checks authored drop and replay
transitions without storing mutable state in the renderer.

The package includes worked examples in `examples/_teaching/teaching-weighted-vector.ts`,
`examples/_teaching/categorical-tail-loss.ts`, `examples/_teaching/teaching-routed-transfer.ts`,
`examples/_teaching/teaching-byte-diff.ts`, and `examples/_teaching/teaching-token-history.ts`.
Render the prediction, intermediate, and payoff states at the intended output
resolution. Check moving marks against actual glyphs as well as declared card
bounds.

Use `changeCue` to mark a numerical update in an existing equation. The cue
briefly highlights the changed region, then settles into the normal text color.
Drive the plotted geometry and displayed value from the returned `value` so the
number and its consequence move together:

```typescript
import { createTeachingCanvas } from 'vizmatic'

const charts = createTeachingCanvas(painter)
const update = charts.changeCue({
  x: 120, y: 160, width: 260, height: 80,
  from: 20, to: 10, progress: updateProgress,
  color: '#F4D477', restColor: '#E9EFFF',
}, ({ value, color }) => {
  painter.math(`m=${value.toFixed(1)}`, 150, 215, 36, color)
})
// Plot the fitted line with update.value as its slope.
```

`valueChange` draws a labeled numeric card with the previous and target values
and a signed delta. Supply `format`, `fontSize`, and explicit bounds; give a
three-line card at least `fontSize * 3` height. Both methods return `value`,
`previous`, `target`, `delta`, `changed`, `emphasis`, and `color`.
`valueChangeState({ from, to, progress })` exposes the same numerical state and
emphasis envelope without drawing. Progress is clamped to zero through one;
unchanged values remain quiet. For an accelerated sequence, pass one steady
`emphasis` envelope across the changing interval to avoid flashing on every
iteration. All cues sample explicit inputs, so seeking and out-of-order frame
rendering produce the same result.

Scatter markers with centers inside the closed data domains remain fully
visible, including points on axis endpoints. Their circles can extend beyond
the axis rectangle; the host audits that complete geometry. Leave room around
the axes or pad the data domains when a surrounding container needs clearance.
Centers outside the data domains are omitted.

```typescript
import { createTeachingCanvas, sampleFunction, stableSoftmax } from 'vizmatic'

const charts = createTeachingCanvas(painter)
const plot = charts.axes({
  x: 160, y: 140, width: 640, height: 360,
  xDomain: [-3, 3], yDomain: [0, 1],
  xTicks: [-3, 0, 3], yTicks: [0, 0.5, 1],
  xLabel: 'Input', yLabel: 'Probability', gridColor: '#283447',
})
charts.curve({
  plot, points: sampleFunction(x => 1 / (1 + Math.exp(-x)), -3, 3),
  color: '#63deec', progress: 0.7,
})
charts.bars({
  x: 980, y: 140, width: 480, height: 360,
  values: stableSoftmax([2, 1, 0]), labels: ['cat', 'dog', 'bird'], max: 1,
})
```

Axes return `{ point(x, y), bounds, xDomain, yDomain }`; plot bounds contain the
data marks, while tick labels and titles occupy space outside the plot. Use
`axes({ ..., equalUnits: true })` to preserve angles and distances. This
fits both requested domains at equal pixels per data unit and centers the
resulting plot inside the requested rectangle. Returned bounds, axes, labels,
point projection, and curve clipping use that fitted plot. The default fills
the requested rectangle with independent horizontal and vertical scales. Curves
return the visible mapped polyline and delegate clipping to the checked host.
Scatter omits circles whose complete mark would extend beyond the plot.
Bars accept nonnegative values and return `{ bars, slots, bounds }`, preserving
input indices with `null` for zero-height bars. `slots` describe full-height
columns, including zero values. Progress is clamped to the unit interval.

`flowGraph({ nodes, edges })` takes explicit node bounds and ID-based edges. It
draws edges and optional moving particles behind the cards, then transfers host
geometry metadata onto the connector references. The returned `{ nodes, routes }`
supports subsequent annotations and movement. Keep topic-specific numerical
models in the lesson; share their chart and graph composition through this API.

`transfer` coordinates an attached connector and optional particle. Supply each
painted card and its matching local opacity. Both marks use the minimum endpoint
and edge opacity; a shared host frame scope multiplies that opacity once.

```typescript
const route = charts.transfer({
  source: { shape: source, opacity: 1 }, target: { shape: target, opacity: 0.6 },
  progress: 0.5, color: '#63deec', particle: { radius: 9, fill: '#ffce73' },
})
```

Hidden endpoints return `null`; progress outside `(0, 1)` omits the particle.
Gaps clear both cards by the marker/stroke extent plus `margin` (default 2),
preserving larger requests. Coincident centers or insufficient room throw.
The host must anchor rounded cards along their center ray and offset gaps along
that ray. Original routes and shape references retain audit evidence; the host
still checks intervening obstacles.

`grid` uses one panel and `rows + columns - 2` paths. `lineWidth` controls internal
lines; the border uses the host's rectangle stroke. Highlights and literal text
labels stay scoped; label dimensions must match the grid.

Supply `{ x, y, width, height, rows, columns }`; returns
`{ shape, bounds, cells, cell(row, column) }`. Connect to the painted `shape`;
cells are label/math geometry. `layoutCellGrid` returns layout without paint.

`timeline` draws supplied intervals on named lanes. One `time` controls elapsed
fills, completion flags, and the axis cursor. Intervals on the same lane must
not overlap; use separate lanes for concurrent work. Bounds include the lane
labels and time axis. Labels occupy their full planned span above the fill;
omit labels on spans too narrow for the chosen font. Future intervals paint
nothing, and visible interval labels use the same local opacity as their fill.

`allocation` draws a capacity-scaled strip and a value ledger from the same
nonnegative parts. Unused capacity remains visible; over-allocation throws.
Supply bounds tall enough for the header, strip, and one row per part plus Free.
`format` and `unit` apply to the computed totals and ledger values.

```typescript
const trace = charts.timeline({
  x: 140, y: 260, width: 1640, height: 300, domain: [0, 100], time: 50,
  lanes: [{ id: 'cpu', label: 'CPU' }, { id: 'gpu', label: 'GPU' }],
  intervals: [
    { id: 'submit', lane: 'cpu', start: 0, end: 20, label: 'Submit' },
    { id: 'kernel', lane: 'gpu', start: 20, end: 80, label: 'Kernel' },
  ],
})
const memory = charts.allocation({
  x: 260, y: 650, width: 1400, height: 370, capacity: 100, unit: 'GB',
  parts: [{ id: 'weights', label: 'Weights', value: 20 },
          { id: 'state', label: 'Optimizer state', value: 30 }],
})
// trace.intervals.kernel.elapsed === 30; memory.remaining === 50
```

Timeline results expose `time`, `point(time)` (the canvas x coordinate), live
lane shapes, and ID-keyed intervals with `shape`, `bounds`, `elapsed`,
`complete`, and local `opacity`.
Zero elapsed time or zero opacity returns a null interval shape. Allocation
returns `used`, `remaining`, capacity-strip `bounds`, `freeBounds`, ordered
part geometry, the painted strip `shape`, and per-part `shapes` (null at zero
area). Connect only to those painted references. `layoutAllocation` computes
pure strip geometry for custom diagrams, with the supplied bounds as the strip.

`createCueTrack` holds one simulation snapshot until an explicit beat cue.
Sample once and derive tables, metrics and decisions from that same snapshot.

```typescript
import { createCueTrack } from 'vizmatic'

const cutoffs = createCueTrack({
  initial: { id: 'morning', snapshot: { cutoff: 540, ageHours: 8 } },
  cues: [
    { id: 'noon', beat: 3, at: 0.2, snapshot: { cutoff: 720, ageHours: 3.5 } },
  ],
})
const { id, snapshot } = cutoffs.sample({ beat: state.beat, beatP: state.beatP })
```

Cue IDs are unique and positions increase strictly. Commits are inclusive;
beat-end `1` equals next-beat `0`. End snapshots hold outside the cues. Beats are
nonnegative safe integers; progress is `[0, 1]`. Definitions are copied and result
records frozen; snapshots retain identity without mutation or cloning. Treat
snapshots as immutable. The timeline still owns audio timing and motion.

Numerical helpers include `seededRandom(seed)`, `lerpArray`, `dotProduct`,
`multiplyMatrices`, `stableSoftmax(values, temperature)`, `entropy`,
`normalizeVector`, `sampleFunction`, `gaussian(mean, sigma, x)`, and
`sampleGaussian(random)`. Softmax subtracts the largest logit before scaling;
entropy accepts normalized probabilities; normalization preserves zero vectors.
Seeded sampling makes simulations reproducible during rendering and review.
Camera helpers keep world geometry and screen annotations aligned:

- `interpolateCamera(from, to, progress)` eases a camera's world-space center
  and interpolates zoom logarithmically. Optional `minZoom` and `maxZoom`
  constrain both endpoints.
- `cameraSvgTransform(camera, { width, height })` centers an SVG group on that
  camera. `projectWorldPoint(point, camera, viewport)` applies the same transform
  to a label anchor rendered outside the group.

Keep event progress separate from camera progress when cutting between views.
The event can continue from its previous state while each shot has its own
pan or zoom. Simulation time and camera movement can follow separate timelines.

Use `renderMathSvg` to typeset TeX into standalone SVG paths for a canvas,
figure, or video frame:

```typescript
import { renderMathSvg } from 'vizmatic'

const formula = renderMathSvg(String.raw`\frac{\partial L}{\partial W_x} + x^2`, {
  fontSize: 48,
  color: '#63deec',
})
// formula.svg contains glyph paths; width, height, and baseline are pixels.
```

The synchronous renderer supports AMS equations, fractions, subscripts, and
superscripts. It returns baseline metrics for alignment with other labels,
caches formulas, and isolates macro definitions between calls. Invalid TeX
throws an error. SVG paths render with local assets and require no browser or
network access.

# Component catalog

# Vizmatic components

Package: vizmatic@0.8.1. Catalog: 93 runnable examples across 17 categories.

Each example is a focused, runnable starting point. The generated Props lists mirror exported TypeScript signatures; `type` is TypeScript text, not a JSON schema, and props may accept values that JSON cannot represent. `required` records signature requiredness. Exhaustive finite choices use `allowedValuesRef`; mixed unions may use `literalValuesRef` for available JSON literal presets while callback or other non-literal variants remain source-only. Shared values are stored in the `propEnums` object in `components.json`. Default values are not inferred.

## Dense information



Word-sized charts, fact rows, and keys that pack real data into nodes and dashboards.



### Sparkline

A word-sized trend line to set beside a number.

Props:
- `area`: `boolean` (optional; allowed values: `propEnums.values1`): Fill under the line.
- `ariaLabel`: `string` (optional)
- `c`: `ThemeColors` (required)
- `color`: `string` (optional)
- `description`: `string` (optional): Context to include with the generated data summary.
- `endDot`: `boolean` (optional; allowed values: `propEnums.values1`): Mark the final value.
- `height`: `number` (optional)
- `id`: `string` (optional): Stable namespace for this chart's exported SVG metadata.
- `max`: `number` (optional)
- `min`: `number` (optional)
- `strokeWidth`: `number` (optional)
- `tone`: `ToneName` (optional; allowed values: `propEnums.ToneName`)
- `values`: `readonly number[]` (required)
- `width`: `number` (optional)

```tsx
width = 960
height = 480

<Scene padding={22} gap={18}>
  <Column
    gap={12}
    align='start'>
  <TextLabel
    text='Request rate · last 9 minutes'
    fontSize={14} />
  <Row
    gap={20}
    justify='start'>
  <MetricStrip
    width={120}
    metrics={[{ label: 'current', value: '18.4', unit: 'k/s' }]} />
  <Sparkline
    values={[12, 14, 13, 17, 16, 17.2, 19, 18, 18.4]}
    width={180}
    height={40} area
    tone='blue'
    ariaLabel='Request rate in thousands per second over nine minutes' />
  </Row>
  </Column>
</Scene>
```

### SmallMultiples

One chart repeated across services or regions, with an optional shared scale.

Props:
- `area`: `boolean` (optional; allowed values: `propEnums.values1`)
- `c`: `ThemeColors` (required)
- `cellHeight`: `number` (optional)
- `cellWidth`: `number` (optional)
- `columns`: `number` (optional)
- `items`: `readonly SmallMultipleItem[]` (required)
- `sharedScale`: `boolean` (optional; allowed values: `propEnums.values1`): Give all cells one y scale, so they compare directly.

```tsx
width = 960
height = 480

<Scene padding={22} gap={18}>
  <SmallMultiples
    columns={3} sharedScale
    items={[{ label: 'api', values: [42, 44, 41, 48, 52, 49], value: '49', unit: 'ms', status: 'ok', reference: 60 },
        { label: 'search', values: [80, 84, 92, 110, 118, 124], value: '124', unit: 'ms', status: 'warn', tone: 'warm', reference: 60 },
        { label: 'ledger', values: [30, 31, 29, 33, 32, 31], value: '31', unit: 'ms', status: 'ok', reference: 60 }]} />
</Scene>
```

### MetricStrip

Numbers with units side by side, without a card around each one.

Props:
- `c`: `ThemeColors` (required)
- `direction`: `"row" | "column"` (optional; allowed values: `propEnums.values2`): Lay out in a row with even columns, or stack.
- `metrics`: `readonly StripMetric[]` (required)
- `sparkWidth`: `number` (optional)
- `width`: `string | number` (optional)

```tsx
width = 960
height = 480

<Scene padding={22} gap={18}>
  <MetricStrip
    width={420}
    metrics={[{ label: 'p50', value: '42', unit: 'ms' },
        { label: 'p99', value: '310', unit: 'ms', tone: 'warm', bar: 0.78 },
        { label: 'error rate', value: '0.04', unit: '%', spark: [0.06, 0.05, 0.07, 0.05, 0.03, 0.04] }]} />
</Scene>
```

### FactRows

Tight key and value rows with optional capacity bars.

Props:
- `c`: `ThemeColors` (required)
- `fontSize`: `number` (optional)
- `gap`: `number` (optional)
- `mono`: `boolean` (optional; allowed values: `propEnums.values1`): Set values in monospace, so columns of numbers line up.
- `rows`: `readonly FactRow[]` (required)
- `width`: `string | number` (optional)

```tsx
width = 960
height = 480

<Scene padding={22} gap={18}>
  <FactRows
    width={240}
    rows={[{ key: 'region', value: 'us-east-1' },
        { key: 'replicas', value: '12 / 12' },
        { key: 'capacity', value: '71%', bar: 0.71 },
        { key: 'disk', value: '91%', bar: 0.91, tone: 'warm' }]} />
</Scene>
```

### InlineBar

A hairline capacity bar that sits under a value.

Props:
- `c`: `ThemeColors` (required)
- `color`: `string` (optional)
- `height`: `number` (optional)
- `tone`: `ToneName` (optional; allowed values: `propEnums.ToneName`)
- `value`: `number` (required)
- `warnAt`: `number` (optional): Past this fraction the bar turns to the warning color, for capacity.
- `width`: `string | number` (optional)

```tsx
width = 960
height = 480

<Scene padding={22} gap={18}>
  <Column
    gap={16}
    align='stretch'
    width={300}>
  <Column
    gap={6}
    align='stretch'>
  <TextLabel
    text='CPU · 42%'
    fontSize={14} />
  <InlineBar
    value={0.42} />
  </Column>
  <Column
    gap={6}
    align='stretch'>
  <TextLabel
    text='Memory · 71%'
    fontSize={14} />
  <InlineBar
    value={0.71}
    tone='blue' />
  </Column>
  <Column
    gap={6}
    align='stretch'>
  <TextLabel
    text='Disk · 93% (warning above 85%)'
    fontSize={14} />
  <InlineBar
    value={0.93}
    warnAt={0.85} />
  </Column>
  </Column>
</Scene>
```

### StatusDot

A small health marker: ok, warn, error, idle, or any tone.

Props:
- `c`: `ThemeColors` (required)
- `size`: `number` (optional)
- `status`: `DiagramStatus` (optional; allowed values: `propEnums.DiagramStatus`)

```tsx
width = 960
height = 480

<Scene padding={22} gap={18}>
  <Row
    gap={24}
    justify='start'>
  <Row
    gap={8}>
  <StatusDot
    status='ok' />
  <TextLabel
    text='Ready'
    fontSize={14} />
  </Row>
  <Row
    gap={8}>
  <StatusDot
    status='warn' />
  <TextLabel
    text='Warning'
    fontSize={14} />
  </Row>
  <Row
    gap={8}>
  <StatusDot
    status='error' />
  <TextLabel
    text='Error'
    fontSize={14} />
  </Row>
  <Row
    gap={8}>
  <StatusDot
    status='idle' />
  <TextLabel
    text='Idle'
    fontSize={14} />
  </Row>
  </Row>
</Scene>
```

### TagList

Small hairline tags for runtime, region, tier, or owner.

Props:
- `c`: `ThemeColors` (required)
- `tags`: `readonly React.ReactNode[]` (required)
- `tone`: `ToneName` (optional; allowed values: `propEnums.ToneName`)

```tsx
width = 960
height = 480

<Scene padding={22} gap={18}>
  <TagList
    tags={['rust', 'us-east-1', 'tier 0', 'on-call: payments']} />
</Scene>
```

### DiagramLegend

A one-line key with line samples and swatches.

Props:
- `c`: `ThemeColors` (required)
- `items`: `readonly DiagramLegendItem[]` (required)
- `title`: `React.ReactNode` (optional)
- `width`: `string | number` (optional)

```tsx
width = 960
height = 480

<Scene padding={22} gap={18}>
  <DiagramLegend
    width={420}
    title='Relationships'
    items={[{ label: 'synchronous', style: 'solid' },
        { label: 'asynchronous', style: 'dashed' },
        { label: 'event', style: 'dotted' },
        { label: 'degraded', style: 'dot', tone: 'warm' }]} />
</Scene>
```
## Image tensors



Exact RGB pixels, channel matrices, crops, and image-to-tensor explanations.



### PixelGrid

Byte-accurate RGB image and channel planes with square pixels, crop outlines, and active-cell focus.

Props:
- `activeCell`: `{ row: number; column: number; }` (optional)
- `c`: `ThemeColors` (optional)
- `channel`: `RgbChannel` (optional; allowed values: `propEnums.RgbChannel`)
- `height`: `number` (optional)
- `highlightColor`: `string` (optional)
- `pixels`: `readonly (readonly RgbPixel[])[]` (required)
- `region`: `PixelRegion` (optional)
- `width`: `number` (optional)

```tsx
width = 960
height = 480

<Scene padding={22} gap={18}>
  <Row
    gap={32}
    align='start'>
  <Column
    gap={14}
    align='center'>
  <TextLabel
    text='RGB image'
    fontSize={18} />
  <PixelGrid
    c={c}
    pixels={[[[255,0,0],
        [0,255,0]],
        [[0,0,255],
        [255,255,255]]]}
    width={240}
    height={240}
    activeCell={{row:0,column:1}} />
  <TextLabel
    text='Selected pixel: (0, 255, 0)'
    fontSize={14} />
  </Column>
  <Column
    gap={14}
    align='center'>
  <TextLabel
    text='Red channel'
    fontSize={18} />
  <PixelGrid
    c={c}
    pixels={[[[255,0,0],
        [0,255,0]],
        [[0,0,255],
        [255,255,255]]]}
    channel='red'
    width={240}
    height={240}
    activeCell={{row:0,column:1}} />
  <TextLabel
    text='Selected red value: 0 / 255'
    fontSize={14} />
  </Column>
  </Row>
</Scene>
```
## Foundations



Scene structure, responsive layout, and framed surfaces.



### Canvas

Root canvas with transparent or custom backgrounds and padding.

Props:
- `background`: `string` (optional)
- `c`: `ThemeColors` (required)
- `children`: `React.ReactNode` (optional)
- `justify`: `"center" | "flex-start" | "space-between" | "space-around"` (optional; allowed values: `propEnums.values3`)
- `padding`: `number` (optional)

```tsx
width = 960
height = 480

<Canvas
    padding={24}>
  <Column
    gap={14}>
  <TitleBar
    title='Canvas surface'
    subtitle='A theme aware root with alpha background support' />
  <MetricCard
    label='Render time'
    value='42 ms'
    tone='green'
    detail='sample p95 · 1,200 frames' />
  </Column>
  </Canvas>
```

### Board

Fixed-size canvas for freely positioned visuals in one coordinate space.

Props:
- `background`: `string` (optional)
- `c`: `ThemeColors` (required)
- `children`: `React.ReactNode` (optional)
- `height`: `number` (optional)
- `width`: `number` (optional)

```tsx
width = 960
height = 480

<Board
    width={960}
    height={480}
    background={c.bgSubtle}>
  <CanvasItem
    x={48}
    y={44}
    width={288}
    height={132}>
  <MetricCard
    label='Sample API latency'
    value='82 ms'
    detail='budget: 120 ms'
    tone='green' />
  </CanvasItem>
  <CanvasItem
    x={368}
    y={184}
    width={264}
    height={132}>
  <MetricCard
    label='Queue depth'
    value='14'
    detail='sample · steady over 5 min'
    tone='purple' />
  </CanvasItem>
  <CanvasItem
    x={672}
    y={52}
    width={240}
    height={116}>
  <MetricCard
    label='Deploy'
    value='healthy'
    detail='sample · all regions'
    tone='blue' />
  </CanvasItem>
  </Board>
```

### CanvasItem

Positions and sizes a visual relative to its parent Board.

Props:
- `children`: `React.ReactNode` (optional)
- `height`: `number` (optional)
- `width`: `number` (optional)
- `x`: `number` (required)
- `y`: `number` (required)

```tsx
width = 1040
height = 640

<Scene padding={22} gap={18}>
  <Board
    width={960}
    height={480}
    background={c.bgSubtle}>
  <CanvasItem
    x={52}
    y={48}
    width={300}
    height={138}>
  <MetricCard
    label='Sample API latency'
    value='82 ms'
    detail='budget: 120 ms'
    tone='green' />
  </CanvasItem>
  <CanvasItem
    x={404}
    y={196}
    width={360}
    height={96}>
  <Panel
    title='Worker pool'
    subtitle='positioned as one canvas item'
    tone='purple'
    width='100%'
    padding={16}>
  <StatusRow
    fontSize={13}
    label='Workers'
    detail='8 / 8 ready'
    status='check' />
  </Panel>
  </CanvasItem>
  </Board>
</Scene>
```

### Scene

Primary vertical scene layout with optional title and subtitle.

Props:
- `align`: `FlexAlign` (optional; allowed values: `propEnums.FlexAlign`)
- `background`: `string` (optional)
- `c`: `ThemeColors` (required)
- `children`: `React.ReactNode` (required)
- `contentStyle`: `React.CSSProperties` (optional)
- `contentWidth`: `string | number` (optional)
- `density`: `Density` (optional): Type, spacing, and stroke scale for this scene and everything inside it.
- `gap`: `number` (optional)
- `justify`: `"center" | "flex-start" | "space-between" | "space-around" | undefined` (optional; allowed values: `propEnums.values3`)
- `padding`: `number` (optional)
- `subtitle`: `string` (optional)
- `title`: `string` (optional)
- `titleAlign`: `"center" | "left"` (optional; allowed values: `propEnums.values4`)
- `titleFont`: `"sans" | "serif"` (optional; allowed values: `propEnums.values5`)

```tsx
width = 960
height = 480

<Scene
    title='Scene layout'
    subtitle='A title, content area, and consistent spacing'>
  <Row
    gap={14}>
  <MetricCard
    label='Build'
    value='ready'
    tone='green' />
  <MetricCard
    label='Review'
    value='2 items'
    tone='warm' />
  </Row>
  </Scene>
```

### TitleBar

Standalone heading and supporting copy.

Props:
- `align`: `"center" | "left"` (optional; allowed values: `propEnums.values4`)
- `c`: `ThemeColors` (required)
- `subtitle`: `string` (optional)
- `title`: `string` (required)
- `titleFont`: `"sans" | "serif"` (optional; allowed values: `propEnums.values5`): Title voice. Serif suits articles and covers. Sans suits technical figures.

```tsx
width = 960
height = 480

<Scene padding={22} gap={18}>
  <Column
    width={620}
    gap={24}
    align='stretch'>
  <TitleBar
    title='Release overview'
    subtitle='Build complete · review in progress' />
  <ProgressList
    fontSize={14}
    labelWidth={72}
    barHeight={8}
    rows={[{ label: 'Build', value: 1, tone: 'green' },
        { label: 'Review', value: 0.72, tone: 'purple' }]} />
  </Column>
</Scene>
```

### Row

Horizontal flex layout with stable gaps and alignment.

Props:
- `align`: `FlexAlign` (optional; allowed values: `propEnums.FlexAlign`)
- `children`: `React.ReactNode` (required)
- `gap`: `number` (optional)
- `height`: `string | number` (optional)
- `justify`: `FlexJustify` (optional; allowed values: `propEnums.FlexJustify`)
- `style`: `React.CSSProperties` (optional)
- `width`: `string | number` (optional)
- `wrap`: `boolean` (optional; allowed values: `propEnums.values1`)

```tsx
width = 960
height = 480

<Scene padding={22} gap={18}>
  <Row
    gap={12}
    align='stretch'>
  <StepCard
    title='Plan'
    subtitle='shape the story'
    tone='blue'
    width={220} />
  <StepCard
    title='Render'
    subtitle='check both themes'
    tone='purple'
    width={220} />
  <StepCard
    title='Ship'
    subtitle='publish the asset'
    tone='green'
    width={220} />
  </Row>
</Scene>
```

### Column

Vertical flex layout for stacked composition.

Props:
- `align`: `FlexAlign` (optional; allowed values: `propEnums.FlexAlign`)
- `children`: `React.ReactNode` (required)
- `gap`: `number` (optional)
- `height`: `string | number` (optional)
- `justify`: `FlexJustify` (optional; allowed values: `propEnums.FlexJustify`)
- `style`: `React.CSSProperties` (optional)
- `width`: `string | number` (optional)
- `wrap`: `boolean` (optional; allowed values: `propEnums.values1`)

```tsx
width = 960
height = 480

<Scene padding={22} gap={18}>
  <Column
    width={460}
    gap={14}
    align='stretch'>
  <StatusRow
    fontSize={14}
    label='Layout'
    detail='ready'
    status='check' />
  <StatusRow
    fontSize={14}
    label='Contrast'
    detail='inspect'
    status='pending' />
  <StatusRow
    fontSize={14}
    label='Export'
    detail='PNG + SVG'
    status='info' />
  </Column>
</Scene>
```

### Stack

Compact vertical grouping for related content.

Props:
- `align`: `"center" | "start" | "end" | "stretch"` (optional; allowed values: `propEnums.FlexAlign`)
- `children`: `React.ReactNode` (required)
- `direction`: `"vertical" | "horizontal"` (optional; allowed values: `propEnums.values6`)
- `gap`: `number` (optional)
- `wrap`: `boolean` (optional; allowed values: `propEnums.values1`)

```tsx
width = 960
height = 480

<Scene padding={22} gap={18}>
  <Stack
    gap={12}
    align='center'>
  <BadgePill
    text='INPUT'
    fontSize={14}
    tone='blue' />
  <Arrow
    direction='down'
    length={36} />
  <BadgePill
    text='OUTPUT'
    fontSize={14}
    tone='green' />
  </Stack>
</Scene>
```

### Panel

Titled container for grouped visual content.

Props:
- `accentHeight`: `number` (optional)
- `accentWidth`: `number` (optional)
- `align`: `FlexAlign` (optional; allowed values: `propEnums.FlexAlign`)
- `background`: `string` (optional)
- `bodyStyle`: `React.CSSProperties` (optional)
- `borderColor`: `string` (optional)
- `c`: `ThemeColors` (required)
- `children`: `React.ReactNode` (optional)
- `footer`: `React.ReactNode` (optional)
- `gap`: `number` (optional)
- `height`: `string | number` (optional)
- `justify`: `FlexJustify` (optional; allowed values: `propEnums.FlexJustify`)
- `minHeight`: `string | number` (optional)
- `minWidth`: `string | number` (optional)
- `padding`: `string | number` (optional)
- `radius`: `number` (optional)
- `shadow`: `boolean` (optional; allowed values: `propEnums.values1`)
- `subtitle`: `React.ReactNode` (optional)
- `title`: `React.ReactNode` (required)
- `titleFontSize`: `number` (optional)
- `tone`: `ToneName` (optional; allowed values: `propEnums.ToneName`)
- `width`: `string | number` (optional)

```tsx
width = 960
height = 480

<Scene padding={22} gap={18}>
  <Panel
    title='Sample service health'
    subtitle='API and background jobs'
    tone='green'
    width={500}
    padding={20}>
  <StatusList
    fontSize={14}
    rows={[{ label: 'API', detail: 'healthy', status: 'check' },
        { label: 'Queue', detail: '12 jobs', status: 'info' }]} />
  </Panel>
</Scene>
```

### Card

General-purpose surface with tone, padding, and shadow.

Props:
- `align`: `FlexAlign` (optional; allowed values: `propEnums.FlexAlign`)
- `background`: `string` (optional)
- `bodyStyle`: `React.CSSProperties` (optional)
- `borderColor`: `string` (optional)
- `c`: `ThemeColors` (required)
- `children`: `React.ReactNode` (optional)
- `footer`: `React.ReactNode` (optional)
- `gap`: `number` (optional)
- `height`: `string | number` (optional)
- `justify`: `FlexJustify` (optional; allowed values: `propEnums.FlexJustify`)
- `minHeight`: `string | number` (optional)
- `minWidth`: `string | number` (optional)
- `padding`: `string | number` (optional)
- `radius`: `number` (optional)
- `shadow`: `boolean` (optional; allowed values: `propEnums.values1`)
- `subtitle`: `React.ReactNode` (optional)
- `title`: `React.ReactNode` (optional)
- `tone`: `ToneName` (optional; allowed values: `propEnums.ToneName`)
- `width`: `string | number` (optional)

```tsx
width = 960
height = 480

<Scene padding={22} gap={18}>
  <Card
    title='Inference API'
    subtitle='Regional endpoint'
    tone='purple'
    width={430}
    padding={20} shadow>
  <TextLabel
    text='p95 latency'
    fontSize={12} />
  <MetricCard
    label='Latency'
    value='82 ms'
    tone='green'
    width={180} />
  </Card>
</Scene>
```

### WindowFrame

Application or terminal window framing.

Props:
- `background`: `string` (optional)
- `bodyStyle`: `React.CSSProperties` (optional)
- `c`: `ThemeColors` (required)
- `children`: `React.ReactNode` (optional)
- `dots`: `boolean` (optional; allowed values: `propEnums.values1`)
- `height`: `string | number` (optional)
- `minHeight`: `string | number` (optional)
- `minWidth`: `string | number` (optional)
- `padding`: `string | number` (optional)
- `radius`: `number` (optional)
- `shadow`: `boolean` (optional; allowed values: `propEnums.values1`)
- `title`: `React.ReactNode` (optional)
- `tone`: `ToneName` (optional; allowed values: `propEnums.ToneName`)
- `variant`: `"window" | "browser" | "terminal"` (optional; allowed values: `propEnums.values7`)
- `width`: `string | number` (optional)

```tsx
width = 960
height = 480

<Scene padding={22} gap={18}>
  <WindowFrame
    title='worker.log'
    variant='terminal'
    tone='green'
    width={620}>
  <Column
    gap={8}
    align='stretch'>
  <TextLabel
    text='$ vizmatic render scene.tsx' mono
    fontSize={13} />
  <TextLabel
    text='✓ dark and light previews created' mono
    fontSize={13}
    color={c.positiveLight} />
  </Column>
  </WindowFrame>
</Scene>
```
## Content



Labels, cards, code, comparisons, and reusable information blocks.



### ToneStrip

Short semantic color accent.

Props:
- `height`: `number` (optional)
- `tone`: `ToneName` (required; allowed values: `propEnums.ToneName`)
- `width`: `number` (optional)

```tsx
width = 960
height = 480

<Scene padding={22} gap={18}>
  <Row
    gap={14}
    align='center'>
  <ToneStrip
    tone='purple'
    width={64}
    height={8} />
  <TextLabel
    text='Accent follows the selected semantic tone.'
    fontSize={14} />
  </Row>
</Scene>
```

### Icon

Theme-aware technical icon set.

Props:
- `c`: `ThemeColors` (required)
- `color`: `string` (optional)
- `label`: `string` (optional)
- `muted`: `boolean` (optional; allowed values: `propEnums.values1`)
- `name`: `IconName` (required; allowed values: `propEnums.IconName`)
- `size`: `number` (optional)
- `strokeWidth`: `number` (optional)
- `tone`: `ToneName` (optional; allowed values: `propEnums.ToneName`)

```tsx
width = 960
height = 480

<Scene padding={22} gap={18}>
  <Row
    gap={18}
    align='center'>
  <Icon
    name='layers'
    tone='purple'
    size={34} />
  <Icon
    name='code'
    tone='blue'
    size={34} />
  <Icon
    name='spark'
    tone='green'
    size={34} />
  </Row>
</Scene>
```

### TextLabel

Wrapping-safe SVG or flex label.

Props:
- `align`: `"center" | "left" | "right"` (optional; allowed values: `propEnums.values8`)
- `c`: `ThemeColors` (required)
- `color`: `string` (optional)
- `fontSize`: `number` (optional)
- `fontWeight`: `number` (optional)
- `math`: `boolean` (optional; allowed values: `propEnums.values1`)
- `mono`: `boolean` (optional; allowed values: `propEnums.values1`)
- `text`: `string` (required)
- `variant`: `"code" | "title" | "subtitle" | "label" | "body" | "small" | "tiny" | "annotation"` (optional; allowed values: `propEnums.values9`)
- `width`: `string | number` (optional)

```tsx
width = 960
height = 480

<Scene padding={22} gap={18}>
  <Column
    gap={8}
    align='start'>
  <TextLabel
    text='A readable technical label'
    fontSize={20}
    fontWeight={800}
    color={c.textPrimary} />
  <TextLabel
    text='Use mono for identifiers, commands, and compact metadata.' mono
    fontSize={12} />
  </Column>
</Scene>
```

### MathText

Math-aware text formatting for React content.

Props:
- `text`: `string` (required)

```tsx
width = 960
height = 480

<Scene padding={22} gap={18}>
  <Column
    gap={10}
    align='start'>
  <TextLabel
    text={MathText({ text: 'x_i^2 + y_i^2' })}
    fontSize={22} mono />
  <TextLabel
    text='MathText formats inline notation for labels.'
    fontSize={12} />
  </Column>
</Scene>
```

### SvgMathText

Positioned math overlay aligned with custom SVG geometry.

Props:
- `dominantBaseline`: `string` (optional)
- `fill`: `string` (required)
- `fontFamily`: `string` (optional)
- `fontSize`: `number` (optional)
- `fontWeight`: `number` (optional)
- `opacity`: `number` (optional)
- `text`: `string` (required)
- `textAnchor`: `"start" | "end" | "middle"` (optional; allowed values: `propEnums.values10`)
- `x`: `number` (required)
- `y`: `number` (required)

```tsx
width = 960
height = 480

<Scene padding={22} gap={18}>
  <div
    style={{ position: 'relative', display: 'flex', width: 420, height: 100 }}>
  <SvgMathText
    text='E = mc^2'
    x={20}
    y={62}
    fill={c.textPrimary}
    fontSize={34}
    textAnchor='start' />
  </div>
</Scene>
```

### Badge

Compact semantic SVG label.

Props:
- `c`: `ThemeColors` (required)
- `color`: `ColorName` (optional; allowed values: `propEnums.ColorName`)
- `label`: `string` (required)

```tsx
width = 960
height = 480

<Scene padding={22} gap={18}>
  <Row
    gap={12}
    align='center'>
  <Badge
    label='stable'
    color='positive' />
  <Badge
    label='preview'
    color='warning' />
  <Badge
    label='blocked'
    color='critical' />
  </Row>
</Scene>
```

### BadgePill

Inline status, category, or metadata label.

Props:
- `c`: `ThemeColors` (required)
- `filled`: `boolean` (optional; allowed values: `propEnums.values1`)
- `fontSize`: `number` (optional)
- `height`: `string | number` (optional)
- `math`: `boolean` (optional; allowed values: `propEnums.values1`)
- `minWidth`: `string | number` (optional)
- `mono`: `boolean` (optional; allowed values: `propEnums.values1`)
- `padding`: `string | number` (optional)
- `radius`: `number` (optional)
- `text`: `React.ReactNode` (required)
- `tone`: `ToneName` (optional; allowed values: `propEnums.ToneName`)
- `width`: `string | number` (optional)

```tsx
width = 960
height = 480

<Scene padding={22} gap={18}>
  <Row
    gap={10}
    align='center'>
  <BadgePill
    text='API'
    tone='blue' />
  <BadgePill
    text='HEALTHY'
    tone='green' />
  <BadgePill
    text='P95 82 ms'
    tone='purple' />
  </Row>
</Scene>
```

### GradientChip

Emphasized label with tone gradient.

Props:
- `align`: `"center" | "left"` (optional; allowed values: `propEnums.values4`)
- `c`: `ThemeColors` (required)
- `gradient`: `string` (optional)
- `height`: `string | number` (optional)
- `math`: `boolean` (optional; allowed values: `propEnums.values1`)
- `minHeight`: `string | number` (optional)
- `minWidth`: `string | number` (optional)
- `padding`: `string | number` (optional)
- `radius`: `number` (optional)
- `shadow`: `boolean` (optional; allowed values: `propEnums.values1`)
- `subtitle`: `React.ReactNode` (optional)
- `subtitleMono`: `boolean` (optional; allowed values: `propEnums.values1`)
- `title`: `React.ReactNode` (required)
- `tone`: `ToneName` (optional; allowed values: `propEnums.ToneName`)
- `width`: `string | number` (optional)

```tsx
width = 960
height = 480

<Scene padding={22} gap={18}>
  <GradientChip
    title='Verified output'
    subtitle='dark + light themes'
    tone='green'
    width={320}
    minHeight={92}
    align='center' />
</Scene>
```

### ValuePill

Small label-value pair.

Props:
- `c`: `ThemeColors` (required)
- `detail`: `React.ReactNode` (optional)
- `label`: `React.ReactNode` (required)
- `math`: `boolean` (optional; allowed values: `propEnums.values1`)
- `tone`: `ToneName` (optional; allowed values: `propEnums.ToneName`)
- `value`: `React.ReactNode` (required)
- `width`: `string | number` (optional)

```tsx
width = 960
height = 480

<Scene padding={22} gap={18}>
  <Row
    gap={14}>
  <ValuePill
    label='Batch size'
    value='128'
    detail='tokens / step'
    tone='purple'
    width={170} />
  <ValuePill
    label='Throughput'
    value='940'
    detail='sample · items / second'
    tone='green'
    width={190} />
  </Row>
</Scene>
```

### EquationCard

Formula, result, and supporting detail.

Props:
- `align`: `"center" | "left"` (optional; allowed values: `propEnums.values4`)
- `c`: `ThemeColors` (required)
- `detail`: `React.ReactNode` (optional)
- `formula`: `React.ReactNode` (required)
- `height`: `string | number` (optional)
- `math`: `boolean` (optional; allowed values: `propEnums.values1`)
- `result`: `React.ReactNode` (optional)
- `title`: `React.ReactNode` (required)
- `tone`: `ToneName` (optional; allowed values: `propEnums.ToneName`)
- `width`: `string | number` (optional)

```tsx
width = 960
height = 480

<Scene padding={22} gap={18}>
  <EquationCard
    title='Throughput'
    formula='items ÷ seconds'
    result='940/s'
    detail='illustrative 5-minute sample'
    tone='purple'
    width={300} math />
</Scene>
```

### StepCard

Numbered or staged process card.

Props:
- `align`: `"center" | "left"` (optional; allowed values: `propEnums.values4`)
- `c`: `ThemeColors` (required)
- `eyebrow`: `React.ReactNode` (optional)
- `math`: `boolean` (optional; allowed values: `propEnums.values1`)
- `minHeight`: `string | number` (optional)
- `minWidth`: `string | number` (optional)
- `padding`: `string | number` (optional)
- `radius`: `number` (optional)
- `shadow`: `boolean` (optional; allowed values: `propEnums.values1`)
- `subtitle`: `React.ReactNode` (optional)
- `title`: `React.ReactNode` (required)
- `tone`: `ToneName` (optional; allowed values: `propEnums.ToneName`)
- `width`: `string | number` (optional)

```tsx
width = 960
height = 480

<Scene padding={22} gap={18}>
  <Row
    gap={14}>
  <StepCard
    eyebrow='01'
    title='Collect'
    subtitle='read source data'
    tone='blue'
    width={220} />
  <StepCard
    eyebrow='02'
    title='Validate'
    subtitle='check invariants'
    tone='purple'
    width={220} />
  <StepCard
    eyebrow='03'
    title='Publish'
    subtitle='write artifacts'
    tone='green'
    width={220} />
  </Row>
</Scene>
```

### MetricCard

KPI value with label and context.

Props:
- `align`: `"center" | "left"` (optional; allowed values: `propEnums.values4`)
- `c`: `ThemeColors` (required)
- `detail`: `React.ReactNode` (optional)
- `label`: `React.ReactNode` (required)
- `math`: `boolean` (optional; allowed values: `propEnums.values1`)
- `minHeight`: `string | number` (optional)
- `minWidth`: `string | number` (optional)
- `padding`: `string | number` (optional)
- `radius`: `number` (optional)
- `shadow`: `boolean` (optional; allowed values: `propEnums.values1`)
- `tone`: `ToneName` (optional; allowed values: `propEnums.ToneName`)
- `value`: `React.ReactNode` (required)
- `valueColor`: `string` (optional)
- `valueFontSize`: `number` (optional)
- `valueMono`: `boolean` (optional; allowed values: `propEnums.values1`)
- `width`: `string | number` (optional)

```tsx
width = 960
height = 480

<Scene padding={22} gap={18}>
  <Row
    gap={14}>
  <MetricCard
    label='Build success'
    value='98.7%'
    detail='sample · last 30 days'
    tone='green'
    width={220}
    minHeight={120} />
  <MetricCard
    label='Open issues'
    value='4'
    detail='sample · 2 need review'
    tone='warm'
    width={220}
    minHeight={120} />
  </Row>
</Scene>
```

### CalloutCard

Focused takeaway, warning, or recommendation.

Props:
- `align`: `"center" | "left"` (optional; allowed values: `propEnums.values4`)
- `c`: `ThemeColors` (required)
- `children`: `React.ReactNode` (optional)
- `detail`: `React.ReactNode` (optional)
- `detailFontSize`: `number` (optional)
- `filled`: `boolean` (optional; allowed values: `propEnums.values1`)
- `minHeight`: `string | number` (optional)
- `padding`: `string | number` (optional)
- `title`: `React.ReactNode` (optional)
- `titleFontSize`: `number` (optional)
- `tone`: `ToneName` (optional; allowed values: `propEnums.ToneName`)
- `width`: `string | number` (optional)

```tsx
width = 960
height = 480

<Scene padding={22} gap={18}>
  <CalloutCard
    title='Recommendation'
    detail='Keep the source editable and verify both color themes before publishing.'
    tone='cyan'
    width={560}
    minHeight={132} />
</Scene>
```

### DetailList

Compact list of supporting facts.

Props:
- `c`: `ThemeColors` (required)
- `fontSize`: `number` (optional)
- `gap`: `number` (optional)
- `items`: `React.ReactNode[]` (required)
- `math`: `boolean` (optional; allowed values: `propEnums.values1`)
- `mono`: `boolean` (optional; allowed values: `propEnums.values1`)
- `padding`: `string | number` (optional)
- `tone`: `ToneName` (optional; allowed values: `propEnums.ToneName`)

```tsx
width = 960
height = 480

<Scene padding={22} gap={18}>
  <DetailList
    tone='blue'
    items={['TypeScript source', 'Theme aware colors', 'PNG and SVG export']}
    fontSize={13} />
</Scene>
```

### CodeBlock

Code or command excerpt with optional line numbers.

Props:
- `background`: `string` (optional)
- `c`: `ThemeColors` (required)
- `fontSize`: `number` (optional)
- `lines`: `(React.ReactNode | CodeLineSpec)[]` (required)
- `math`: `boolean` (optional; allowed values: `propEnums.values1`)
- `minWidth`: `string | number` (optional)
- `padding`: `string | number` (optional)
- `radius`: `number` (optional)
- `shadow`: `boolean` (optional; allowed values: `propEnums.values1`)
- `showLineNumbers`: `boolean` (optional; allowed values: `propEnums.values1`)
- `title`: `React.ReactNode` (optional)
- `tone`: `ToneName` (optional; allowed values: `propEnums.ToneName`)
- `width`: `string | number` (optional)

```tsx
width = 960
height = 480

<Scene padding={22} gap={18}>
  <CodeBlock
    title='Check and render'
    tone='purple'
    fontSize={14} showLineNumbers
    lines={[{ text: 'vizmatic check scene.tsx', tone: 'purple' },
        { text: 'vizmatic render scene.tsx' },
        { text: 'vizmatic --help', tone: 'green' }]} />
</Scene>
```

### Comparison

Side-by-side alternatives or before/after states.

Props:
- `align`: `FlexAlign` (optional; allowed values: `propEnums.FlexAlign`)
- `c`: `ThemeColors` (required)
- `divider`: `React.ReactNode` (optional)
- `gap`: `number` (optional)
- `math`: `boolean` (optional; allowed values: `propEnums.values1`)
- `minHeight`: `string | number` (optional)
- `sides`: `ComparisonSideSpec[]` (required)
- `sideWidth`: `string | number` (optional)

```tsx
width = 960
height = 480

<Scene padding={22} gap={18}>
  <Comparison divider
    gap={10}
    minHeight={90}
    sides={[{ title: 'Before', tone: 'warm', lines: ['manual spacing', 'one theme'] },
        { title: 'After', tone: 'green', lines: ['shared layout', 'dark + light'] }]} />
</Scene>
```

### KeyValueList

Aligned technical metadata rows.

Props:
- `c`: `ThemeColors` (required)
- `divider`: `boolean` (optional; allowed values: `propEnums.values1`)
- `fontSize`: `number` (optional)
- `gap`: `number` (optional)
- `keyMono`: `boolean` (optional; allowed values: `propEnums.values1`)
- `keyWidth`: `string | number` (optional)
- `math`: `boolean` (optional; allowed values: `propEnums.values1`)
- `minWidth`: `string | number` (optional)
- `rows`: `KeyValueRow[]` (required)
- `title`: `React.ReactNode` (optional)
- `width`: `string | number` (optional)

```tsx
width = 960
height = 480

<Scene padding={22} gap={18}>
  <KeyValueList
    title='Render settings'
    rows={[{ key: 'format', value: 'PNG + SVG', tone: 'blue' },
        { key: 'themes', value: 'dark, light', tone: 'green' },
        { key: 'latency', value: '42 ms' }]}
    width={430} />
</Scene>
```

### Tile

Single labeled unit for feature or system maps.

Props:
- `align`: `"center" | "left"` (optional; allowed values: `propEnums.values4`)
- `c`: `ThemeColors` (required)
- `children`: `React.ReactNode` (optional)
- `eyebrow`: `React.ReactNode` (optional)
- `icon`: `React.ReactNode` (optional)
- `lines`: `React.ReactNode[]` (optional)
- `math`: `boolean` (optional; allowed values: `propEnums.values1`)
- `minHeight`: `string | number` (optional)
- `padding`: `string | number` (optional)
- `radius`: `number` (optional)
- `shadow`: `boolean` (optional; allowed values: `propEnums.values1`)
- `subtitle`: `React.ReactNode` (optional)
- `title`: `React.ReactNode` (required)
- `tone`: `ToneName` (optional; allowed values: `propEnums.ToneName`)
- `width`: `string | number` (optional)

```tsx
width = 960
height = 480

<Scene padding={22} gap={18}>
  <Tile
    title='Search index'
    subtitle='retrieval service'
    eyebrow='READY'
    icon={<Icon
    name='layers'
    tone='cyan'
    size={24} />}
    tone='cyan'
    lines={['p95 18 ms', '3 replicas']}
    width={260}
    minHeight={128} />
</Scene>
```

### TileGrid

Dense grid of related labeled units.

Props:
- `align`: `"center" | "left"` (optional; allowed values: `propEnums.values4`)
- `c`: `ThemeColors` (required)
- `columns`: `number` (optional)
- `gap`: `number` (optional)
- `math`: `boolean` (optional; allowed values: `propEnums.values1`)
- `minHeight`: `string | number` (optional)
- `tiles`: `TileSpec[]` (required)
- `tileWidth`: `string | number` (optional)

```tsx
width = 960
height = 480

<Scene padding={22} gap={18}>
  <TileGrid
    columns={3}
    gap={12}
    tileWidth={210}
    minHeight={112}
    tiles={[{ title: 'Ingest', subtitle: 'events', tone: 'blue' },
        { title: 'Transform', subtitle: 'schemas', tone: 'purple' },
        { title: 'Serve', subtitle: 'features', tone: 'green' }]} />
</Scene>
```

### Watermark

Frame metadata for text, image, or custom branding.

Props:
- `children`: `React.ReactNode` (optional)
- `color`: `string` (optional)
- `element`: `React.ReactNode` (optional)
- `icon`: `React.ReactNode` (optional)
- `image`: `string | WatermarkImageOptions` (optional)
- `opacity`: `number` (optional)
- `position`: `WatermarkPosition` (optional; allowed values: `propEnums.WatermarkPosition`)
- `text`: `string | false` (optional)

```tsx
width = 960
height = 480

<Scene padding={22} gap={18}>
  <Row
    width={680}
    height={300}
    align='center'
    justify='center'>{wrapWithWatermark(<Scene
    title='Published report'
    subtitle='Watermark stays anchored to the rendered frame'
    padding={22}>
  <MetricCard
    label='Sample runs'
    value='1,284'
    detail='this week'
    tone='green'
    width={250} />
  </Scene>, 640, 260, 'dark', <Watermark
    text='Vizmatic'
    color={c.textSecondary}
    opacity={0.9}
    position='bottom-right' />)}</Row>
</Scene>
```
## Flows



Processes, timelines, status, and progress.



### StatusRow

Single operational state with detail.

Props:
- `boxed`: `boolean` (optional; allowed values: `propEnums.values1`)
- `c`: `ThemeColors` (required)
- `detail`: `React.ReactNode` (optional)
- `fontSize`: `number` (optional)
- `label`: `React.ReactNode` (required)
- `math`: `boolean` (optional; allowed values: `propEnums.values1`)
- `status`: `StatusKind` (optional; allowed values: `propEnums.StatusKind`)
- `tone`: `ToneName` (optional; allowed values: `propEnums.ToneName`)
- `width`: `string | number` (optional)

```tsx
width = 960
height = 480

<Scene padding={22} gap={18}>
  <Column
    gap={10}
    width={620}
    align='stretch'>
  <StatusRow
    fontSize={13}
    label='Build'
    detail='passed'
    status='check' />
  <StatusRow
    fontSize={13}
    label='Review'
    detail='2 comments'
    status='pending' />
  <StatusRow
    fontSize={13}
    label='Security'
    detail='attention'
    status='warn' />
  </Column>
</Scene>
```

### StatusList

Grouped checks, warnings, and pending work.

Props:
- `boxed`: `boolean` (optional; allowed values: `propEnums.values1`)
- `c`: `ThemeColors` (required)
- `fontSize`: `number` (optional)
- `gap`: `number` (optional)
- `math`: `boolean` (optional; allowed values: `propEnums.values1`)
- `rows`: `StatusRowSpec[]` (required)
- `width`: `string | number` (optional)

```tsx
width = 960
height = 480

<Scene padding={22} gap={18}>
  <StatusList
    fontSize={13}
    width={620}
    rows={[{ label: 'Build', detail: 'passed', status: 'check' },
        { label: 'Review', detail: '2 comments', status: 'pending' },
        { label: 'Security', detail: 'attention', status: 'warn' }]} />
</Scene>
```

### Timeline

Ordered events in horizontal or vertical form.

Props:
- `c`: `ThemeColors` (required)
- `direction`: `"vertical" | "horizontal"` (optional; allowed values: `propEnums.values6`)
- `events`: `TimelineEventSpec[]` (required)
- `eventWidth`: `string | number` (optional)
- `gap`: `number` (optional)
- `markerSize`: `number` (optional)
- `math`: `boolean` (optional; allowed values: `propEnums.values1`)
- `subtitle`: `React.ReactNode` (optional)
- `title`: `React.ReactNode` (optional)
- `width`: `string | number` (optional)

```tsx
width = 960
height = 480

<Scene padding={22} gap={18}>
  <Timeline
    title='Release timeline'
    subtitle='Key steps from draft to publish'
    width={820}
    direction='horizontal'
    eventWidth={230}
    events={[{ time: '09:10', title: 'Draft', detail: 'write the scene', tone: 'blue' },
        { time: '09:24', title: 'Validate', detail: 'check layout', tone: 'purple' },
        { time: '09:31', title: 'Publish', detail: 'export assets', tone: 'green' }]} />
</Scene>
```

### Flow

Connected horizontal or vertical stages.

Props:
- `align`: `FlexAlign` (optional; allowed values: `propEnums.FlexAlign`)
- `c`: `ThemeColors` (required)
- `connectorLength`: `number` (optional)
- `connectorTone`: `ToneName` (optional; allowed values: `propEnums.ToneName`)
- `direction`: `"vertical" | "horizontal"` (optional; allowed values: `propEnums.values6`)
- `gap`: `number` (optional)
- `math`: `boolean` (optional; allowed values: `propEnums.values1`)
- `stages`: `FlowStageSpec[]` (required)

```tsx
width = 960
height = 480

<Scene padding={22} gap={18}>
  <Flow
    gap={12}
    connectorLength={30}
    stages={[{ title: 'Request', subtitle: 'source data', tone: 'blue', width: 190 },
        { title: 'Transform', subtitle: 'build scene', tone: 'purple', width: 190 },
        { title: 'Deliver', subtitle: 'render files', tone: 'green', width: 190 }]} />
</Scene>
```

### Pipeline

Compact process pipeline with shared title.

Props:
- `c`: `ThemeColors` (required)
- `stages`: `PipelineStage[]` (required)
- `title`: `string` (optional)

```tsx
width = 960
height = 480

<Scene padding={22} gap={18}>
  <Pipeline
    title='Release pipeline'
    stages={[{ label: 'Build', sublabel: 'TSX', icon: '◇', color: 'secondary' },
        { label: 'Check', sublabel: 'themes', icon: '✓', color: 'info' },
        { label: 'Export', sublabel: 'PNG', icon: '↓', color: 'positive' }]} />
</Scene>
```

### ProgressRow

Labeled progress bar and value.

Props:
- `barHeight`: `number` (optional)
- `c`: `ThemeColors` (required)
- `fontSize`: `number` (optional)
- `label`: `React.ReactNode` (required)
- `labelWidth`: `number` (optional)
- `muted`: `boolean` (optional; allowed values: `propEnums.values1`)
- `tone`: `ToneName` (optional; allowed values: `propEnums.ToneName`)
- `value`: `number` (required)
- `valueLabel`: `React.ReactNode` (optional)
- `valueWidth`: `number` (optional)

```tsx
width = 960
height = 480

<Scene padding={22} gap={18}>
  <Column
    width={620}
    gap={14}
    align='stretch'>
  <ProgressRow
    label='Layout'
    value={0.96}
    valueLabel='96%'
    tone='green' />
  <ProgressRow
    label='Contrast'
    value={0.82}
    valueLabel='82%'
    tone='purple' />
  </Column>
</Scene>
```

### ProgressList

Comparable progress across several measures.

Props:
- `barHeight`: `number` (optional)
- `c`: `ThemeColors` (required)
- `fontSize`: `number` (optional)
- `gap`: `number` (optional)
- `labelWidth`: `number` (optional)
- `rows`: `ProgressRowSpec[]` (required)
- `valueWidth`: `number` (optional)

```tsx
width = 960
height = 480

<Scene padding={22} gap={18}>
  <ProgressList
    labelWidth={100}
    rows={[{ label: 'Layout', value: 0.96, tone: 'green' },
        { label: 'Contrast', value: 0.82, tone: 'purple' },
        { label: 'Labels', value: 1, tone: 'blue' }]} />
</Scene>
```
## Diagrams



Networks, system architecture, directed graphs, and hierarchies.



### LayeredNetwork

Layered neural network or staged DAG.

Props:
- `activePath`: `number[]` (optional)
- `annotations`: `string[]` (optional)
- `ariaLabel`: `string` (optional)
- `c`: `ThemeColors` (required)
- `formula`: `string` (optional)
- `height`: `number` (optional)
- `layers`: `LayeredNetworkLayer[]` (required)
- `legend`: `string` (optional)
- `nodeSize`: `number` (optional)
- `showFormula`: `boolean` (optional; allowed values: `propEnums.values1`)
- `width`: `number` (optional)

```tsx
width = 960
height = 560

<Scene padding={22} gap={18}>
  <LayeredNetwork
    width={820}
    height={360}
    layers={[{ title: 'Input', nodes: ['x₁', 'x₂', 'x₃'], tone: 'blue' },
        { title: 'Hidden', nodes: ['h₁', 'h₂', 'h₃'], tone: 'purple' },
        { title: 'Output', nodes: ['y'], tone: 'green' }]}
    activePath={[1, 0, 0]}
    formula='y = f(Wx + b)' />
</Scene>
```

### GraphDiagram

Auto-laid architecture graph with nested groups, icons, and semantic edges.

Props:
- `annotations`: `GraphDiagramAnnotation[]` (optional): Notes with leader lines, pointing at nodes.
- `ariaLabel`: `string` (optional)
- `arrowSize`: `number` (optional)
- `c`: `ThemeColors` (required)
- `crossingBudget`: `number` (optional): Crossings a large map accepts before `vizmatic check` warns. Defaults to one for every six edges.
- `density`: `Density` (optional): Type, spacing, and stroke scale for this diagram.
- `description`: `string` (optional)
- `detailFontSize`: `number` (optional)
- `direction`: `"LR" | "RL" | "TB" | "BT"` (optional; allowed values: `propEnums.values11`)
- `edgeGap`: `number` (optional)
- `edges`: `GraphDiagramEdge[]` (required)
- `groupDirection`: `"LR" | "RL" | "TB" | "BT"` (optional; allowed values: `propEnums.values11`): Flow inside each group. Stack tiers top to bottom and read each tier left to right with direction="TB" and groupDirection="LR".
- `groups`: `GraphDiagramGroup[]` (optional)
- `height`: `number` (optional)
- `iconSize`: `number` (optional)
- `id`: `string` (optional)
- `labelFontSize`: `number` (optional)
- `layout`: `"auto" | "manual"` (optional; allowed values: `propEnums.values12`)
- `legend`: `boolean | DiagramLegendItem[]` (optional): A key below the diagram. Pass true to build one from the relationship kinds in use.
- `nodeGap`: `number` (optional)
- `nodeHeight`: `number` (optional): Minimum node height. Nodes grow when their content needs more.
- `nodes`: `GraphDiagramNode[]` (required)
- `nodeWidth`: `number` (optional): Fixed node width. Omit it to size each node to its content.
- `padding`: `number` (optional)
- `rankGap`: `number` (optional)
- `routing`: `GraphRouting` (optional; allowed values: `propEnums.GraphRouting`): Dagre draws smooth curves. Orthogonal routes avoid node boxes and group headers.
- `routingClearance`: `number` (optional): Minimum node clearance in logical pixels for orthogonal routing.
- `sizing`: `"content" | "fixed"` (optional; allowed values: `propEnums.values13`)
- `title`: `string` (optional)
- `width`: `number` (optional)

```tsx
width = 960
height = 480

<Scene padding={22} gap={18}>
  <GraphDiagram
    width={820}
    height={360}
    routing='dagre'
    routingClearance={12}
    nodes={[{ id: 'api', label: 'API', tone: 'blue' },
        { id: 'queue', label: 'Queue', tone: 'purple' },
        { id: 'worker', label: 'Worker', tone: 'green' }]}
    edges={[{ from: 'api', to: 'queue', label: 'enqueue' },
        { from: 'queue', to: 'worker', label: 'consume' }]} />
</Scene>
```

### EntityRelationshipDiagram

Database entities, typed attributes, primary and foreign keys, and crow-foot cardinalities.

Props:
- `c`: `ThemeColors` (required)
- `density`: `Density` (optional)
- `description`: `string` (optional)
- `direction`: `"LR" | "RL" | "TB" | "BT"` (optional; allowed values: `propEnums.values11`)
- `entities`: `readonly EntityRelationshipEntity[]` (required)
- `entityWidth`: `number` (optional): Minimum entity table width. Long names expand it.
- `fontSize`: `number` (optional)
- `gap`: `number` (optional): Minimum space between connected entity tables.
- `height`: `number` (optional): Canvas height. A smaller canvas raises an error instead of shrinking the text.
- `id`: `string` (optional)
- `padding`: `number` (optional)
- `relationships`: `readonly EntityRelationship[]` (optional)
- `showLegend`: `boolean` (optional; allowed values: `propEnums.values1`)
- `title`: `string` (optional)
- `width`: `number` (optional): Canvas width. A smaller canvas raises an error instead of shrinking the text.

```tsx
width = 960
height = 480

<Scene padding={22} gap={18}>
  <EntityRelationshipDiagram
    title='Customer orders'
    width={860}
    height={360}
    fontSize={14}
    entities={[{ id: 'customers', tone: 'blue', attributes: [{ name: 'id', type: 'uuid', primaryKey: true },
        { name: 'email', type: 'text', unique: true }] },
        { id: 'orders', tone: 'purple', attributes: [{ name: 'id', type: 'uuid', primaryKey: true },
        { name: 'customer_id', type: 'uuid', foreignKey: true },
        { name: 'note', type: 'text', nullable: true }] }]}
    relationships={[{ id: 'customer-orders', label: 'places', from: { entity: 'customers', attribute: 'id', cardinality: 'one' }, to: { entity: 'orders', attribute: 'customer_id', cardinality: 'zero-or-many' } }]} />
</Scene>
```

### TreeDiagram

Auto-laid hierarchy, taxonomy, or decision tree.

Props:
- `ariaLabel`: `string` (optional)
- `c`: `ThemeColors` (required)
- `height`: `number` (optional)
- `levelGap`: `number` (optional)
- `math`: `boolean` (optional; allowed values: `propEnums.values1`)
- `nodeHeight`: `number` (optional)
- `nodeWidth`: `number` (optional)
- `padding`: `number` (optional)
- `root`: `TreeNodeSpec` (required)
- `siblingGap`: `number` (optional)
- `subtitle`: `React.ReactNode` (optional)
- `title`: `React.ReactNode` (optional)
- `width`: `number` (optional)

```tsx
width = 960
height = 480

<Scene padding={22} gap={18}>
  <TreeDiagram
    width={820}
    height={340}
    root={{ id: 'root', label: 'Product', tone: 'purple', children: [{ id: 'api', label: 'API', tone: 'blue' },
        { id: 'web', label: 'Web app', tone: 'green' }] }} />
</Scene>
```
## Sequence



Time-ordered interactions, lifelines, activations, notes, and control fragments.



### SequenceDiagram

Typed participants, messages, notes, activations, and alt, loop, or parallel fragments.

Props:
- `activations`: `SequenceActivation[]` (optional)
- `ariaLabel`: `string` (optional)
- `c`: `ThemeColors` (required)
- `fragmentIndent`: `number` (optional)
- `fragments`: `SequenceFragment[]` (optional)
- `height`: `number` (optional)
- `items`: `SequenceItem[]` (optional): Ordered event input. When present, it is the source of row order.
- `lifelineDash`: `string` (optional)
- `messages`: `SequenceMessage[]` (optional)
- `notes`: `SequenceNote[]` (optional)
- `noteWidth`: `number` (optional)
- `padding`: `number` (optional)
- `participantGap`: `number` (optional)
- `participants`: `SequenceParticipant[]` (required)
- `participantWidth`: `number` (optional)
- `rowHeight`: `number` (optional)
- `subtitle`: `React.ReactNode` (optional)
- `title`: `React.ReactNode` (optional)
- `width`: `number` (optional)

```tsx
width = 960
height = 560

<Scene padding={22} gap={18}>
  <SequenceDiagram
    width={820}
    height={360}
    participants={[{ id: 'client', label: 'Client', kind: 'actor' },
        { id: 'api', label: 'API', kind: 'boundary' },
        { id: 'db', label: 'Database', kind: 'database' }]}
    messages={[{ id: 'request', from: 'client', to: 'api', kind: 'sync', label: 'GET /items' },
        { id: 'query', from: 'api', to: 'db', kind: 'sync', label: 'SELECT items' },
        { id: 'response', from: 'api', to: 'client', kind: 'return', label: '200 OK' }]} />
</Scene>
```
## Dataflow



Typed lineage across sources, transforms, stores, and sinks.



### DataflowDiagram

Schema-aware batch and streaming lineage compiled into a stable directed graph.

Props:
- `ariaLabel`: `string` (optional)
- `arrowSize`: `number` (optional)
- `boundaries`: `DataflowBoundary[]` (optional)
- `c`: `ThemeColors` (required)
- `description`: `string` (optional)
- `detailFontSize`: `number` (optional)
- `direction`: `"LR" | "RL" | "TB" | "BT" | undefined` (optional; allowed values: `propEnums.values11`)
- `edgeGap`: `number` (optional)
- `edges`: `DataflowEdge[]` (required)
- `height`: `number` (optional)
- `iconSize`: `number` (optional)
- `id`: `string` (optional)
- `labelFontSize`: `number` (optional)
- `nodeGap`: `number` (optional)
- `nodeHeight`: `number` (optional)
- `nodes`: `DataflowNode[]` (required)
- `nodeWidth`: `number` (optional)
- `padding`: `number` (optional)
- `rankGap`: `number` (optional)
- `routing`: `GraphRouting | undefined` (optional; allowed values: `propEnums.GraphRouting`)
- `routingClearance`: `number | undefined` (optional)
- `sizing`: `"content" | "fixed" | undefined` (optional; allowed values: `propEnums.values13`)
- `title`: `string` (optional)
- `width`: `number` (optional)

```tsx
width = 1040
height = 560

<Scene padding={22} gap={18}>
  <DataflowDiagram
    width={820}
    height={390}
    routing='dagre'
    routingClearance={12}
    direction='LR'
    nodes={[{ id: 'source', label: 'Events', kind: 'source', tone: 'blue', schema: 'Event.v1' },
        { id: 'transform', label: 'Validate', kind: 'transform', tone: 'purple' },
        { id: 'sink', label: 'Warehouse', kind: 'store', tone: 'green' }]}
    edges={[{ from: 'source', to: 'transform', mode: 'stream', label: 'Event.v1' },
        { from: 'transform', to: 'sink', mode: 'batch', label: 'Valid.v1' }]} />
</Scene>
```
## Deployment



Infrastructure boundaries, workloads, ports, protocols, and trust direction.



### DeploymentDiagram

Nested region, network, and runtime boundaries with typed network connections.

Props:
- `annotations`: `GraphDiagramAnnotation[]` (optional): Notes with leader lines, pointing at nodes.
- `ariaLabel`: `string` (optional)
- `arrowSize`: `number` (optional)
- `boundaries`: `DeploymentBoundary[]` (optional)
- `c`: `ThemeColors` (required)
- `connections`: `DeploymentConnection[]` (optional)
- `crossingBudget`: `number` (optional): Crossings a large map accepts before `vizmatic check` warns. Defaults to one for every six edges.
- `density`: `Density` (optional): Type, spacing, and stroke scale for this diagram.
- `description`: `string` (optional)
- `detailFontSize`: `number` (optional)
- `direction`: `"LR" | "RL" | "TB" | "BT"` (optional; allowed values: `propEnums.values11`)
- `edgeGap`: `number` (optional)
- `groupDirection`: `"LR" | "RL" | "TB" | "BT"` (optional; allowed values: `propEnums.values11`): Flow inside each group. Stack tiers top to bottom and read each tier left to right with direction="TB" and groupDirection="LR".
- `height`: `number` (optional)
- `iconSize`: `number` (optional)
- `id`: `string` (optional)
- `labelFontSize`: `number` (optional)
- `layout`: `"auto" | "manual"` (optional; allowed values: `propEnums.values12`)
- `legend`: `boolean | DiagramLegendItem[]` (optional): A key below the diagram. Pass true to build one from the relationship kinds in use.
- `nodeGap`: `number` (optional)
- `nodeHeight`: `number` (optional): Minimum node height. Nodes grow when their content needs more.
- `nodes`: `DeploymentNode[]` (required)
- `nodeWidth`: `number` (optional): Fixed node width. Omit it to size each node to its content.
- `padding`: `number` (optional)
- `rankGap`: `number` (optional)
- `routing`: `GraphRouting` (optional; allowed values: `propEnums.GraphRouting`): Dagre draws smooth curves. Orthogonal routes avoid node boxes and group headers.
- `routingClearance`: `number` (optional): Minimum node clearance in logical pixels for orthogonal routing.
- `sizing`: `"content" | "fixed"` (optional; allowed values: `propEnums.values13`)
- `title`: `string` (optional)
- `width`: `number` (optional)

```tsx
width = 1040
height = 560

<Scene padding={22} gap={18}>
  <DeploymentDiagram
    width={820}
    height={390}
    routing='dagre'
    routingClearance={12}
    direction='LR'
    boundaries={[{ id: 'region', label: 'us-east-1', kind: 'region', tone: 'blue' },
        { id: 'private', label: 'private', kind: 'subnet', parent: 'region', tone: 'purple' }]}
    nodes={[{ id: 'edge', label: 'Gateway', kind: 'gateway', tone: 'cyan' },
        { id: 'api', label: 'API', kind: 'service', boundary: 'private', tone: 'green' }]}
    connections={[{ id: 'https', from: 'edge', to: 'api', kind: 'cross-boundary', protocol: 'HTTPS', label: 'HTTPS/443' }]} />
</Scene>
```
## ML topology



Transformer blocks, tensor shapes, repeated layers, and distributed routes.



### TransformerTopology

Transformer computation topology with tensor metadata and collective routes.

Props:
- `annotations`: `GraphDiagramAnnotation[]` (optional): Notes with leader lines, pointing at nodes.
- `ariaLabel`: `string` (optional)
- `arrowSize`: `number` (optional)
- `blocks`: `TransformerBlock[]` (required)
- `c`: `ThemeColors` (required)
- `crossingBudget`: `number` (optional): Crossings a large map accepts before `vizmatic check` warns. Defaults to one for every six edges.
- `density`: `Density` (optional): Type, spacing, and stroke scale for this diagram.
- `description`: `string` (optional)
- `detailFontSize`: `number` (optional)
- `direction`: `"LR" | "RL" | "TB" | "BT"` (optional; allowed values: `propEnums.values11`)
- `edgeGap`: `number` (optional)
- `expandRepeats`: `boolean` (optional; allowed values: `propEnums.values1`): Expand repeat counts into individually addressable graph nodes.
- `groupDirection`: `"LR" | "RL" | "TB" | "BT"` (optional; allowed values: `propEnums.values11`): Flow inside each group. Stack tiers top to bottom and read each tier left to right with direction="TB" and groupDirection="LR".
- `height`: `number` (optional)
- `iconSize`: `number` (optional)
- `id`: `string` (optional)
- `labelFontSize`: `number` (optional)
- `layout`: `"auto" | "manual"` (optional; allowed values: `propEnums.values12`)
- `legend`: `boolean | DiagramLegendItem[]` (optional): A key below the diagram. Pass true to build one from the relationship kinds in use.
- `nodeGap`: `number` (optional)
- `nodeHeight`: `number` (optional): Minimum node height. Nodes grow when their content needs more.
- `nodeWidth`: `number` (optional): Fixed node width. Omit it to size each node to its content.
- `padding`: `number` (optional)
- `rankGap`: `number` (optional)
- `routes`: `TransformerRoute[]` (optional)
- `routing`: `GraphRouting` (optional; allowed values: `propEnums.GraphRouting`): Dagre draws smooth curves. Orthogonal routes avoid node boxes and group headers.
- `routingClearance`: `number` (optional): Minimum node clearance in logical pixels for orthogonal routing.
- `sizing`: `"content" | "fixed"` (optional; allowed values: `propEnums.values13`)
- `title`: `string` (optional)
- `width`: `number` (optional)

```tsx
width = 1040
height = 560

<Scene padding={22} gap={18}>
  <TransformerTopology
    width={820}
    height={390}
    routing='dagre'
    routingClearance={12}
    direction='LR'
    blocks={[{ id: 'embed', label: 'Embed', kind: 'embedding', outputShape: { dims: ['B', 'T', 'd'], dtype: 'bf16' } },
        { id: 'attn', label: 'Attention', kind: 'attention', shape: { dims: ['B', 'T', 'd'], dtype: 'bf16' } },
        { id: 'logits', label: 'Logits', kind: 'output', outputShape: { dims: ['B', 'T', 'V'], dtype: 'bf16' } }]}
    routes={[{ id: 'hidden', from: 'embed', to: 'attn', kind: 'activation', label: 'hidden state' },
        { id: 'scores', from: 'attn', to: 'logits', kind: 'activation', label: 'scores' }]} />
</Scene>
```
## Data



Tables, grids, matrices, and compact distributions.



### Matrix

Numeric or symbolic matrix with headers.

Props:
- `ariaLabel`: `string` (optional)
- `c`: `ThemeColors` (required)
- `cellHeight`: `number` (optional)
- `cellSize`: `number` (optional)
- `cellWidth`: `number` (optional)
- `colLabels`: `string[]` (optional)
- `colorDomain`: `readonly [number, number]` (optional)
- `colorize`: `boolean` (optional; allowed values: `propEnums.values1`)
- `colorScale`: `"heat" | "strength"` (optional; allowed values: `propEnums.values14`)
- `data`: `number[][]` (required)
- `description`: `string` (optional): Context to include with the generated data summary.
- `fontSize`: `number` (optional)
- `format`: `"decimal" | "percent" | "integer"` (optional; allowed values: `propEnums.values15`)
- `highlightCell`: `{ row: number; col: number; }` (optional)
- `highlightCol`: `number` (optional)
- `highlightColor`: `string` (optional)
- `highlightRow`: `number` (optional)
- `id`: `string` (optional): Stable namespace for this chart's exported SVG metadata.
- `labels`: `React.ReactNode[][]` (optional)
- `rowLabels`: `string[]` (optional)
- `rowLabelWidth`: `number` (optional)
- `title`: `string` (optional)

```tsx
width = 960
height = 480

<Scene padding={22} gap={18}>
  <Matrix
    title='Attention weights'
    rowLabels={['q₁', 'q₂', 'q₃']}
    colLabels={['k₁', 'k₂', 'k₃']}
    data={[[0.82, 0.14, 0.04],
        [0.18, 0.66, 0.16],
        [0.06, 0.21, 0.73]]}
    cellWidth={58}
    cellHeight={42}
    rowLabelWidth={34} />
</Scene>
```

### Heatmap

Color-scaled matrix for intensity and correlation.

Props:
- `ariaLabel`: `string` (optional)
- `c`: `ThemeColors` (required)
- `cellHeight`: `number` (optional)
- `cellSize`: `number` (optional)
- `cellWidth`: `number` (optional)
- `colorDomain`: `readonly [number, number]` (optional)
- `colorScale`: `"heat" | "strength"` (optional; allowed values: `propEnums.values14`)
- `data`: `number[][]` (required)
- `description`: `string` (optional): Context to include with the generated data summary.
- `fontSize`: `number` (optional)
- `highlightCell`: `{ row: number; col: number; }` (optional)
- `highlightCol`: `number` (optional)
- `highlightColor`: `string` (optional)
- `highlightRow`: `number` (optional)
- `id`: `string` (optional): Stable namespace for this chart's exported SVG metadata.
- `labels`: `React.ReactNode[][]` (optional)
- `rowLabelWidth`: `number` (optional)
- `title`: `string` (optional)
- `xLabels`: `string[]` (required)
- `yLabels`: `string[]` (required)

```tsx
width = 960
height = 480

<Scene padding={22} gap={18}>
  <Heatmap
    title='Task coverage · fraction passing'
    xLabels={['api', 'docs', 'sdk']}
    yLabels={['setup', 'auth', 'tests']}
    data={[[0.92, 0.4, 0.2],
        [0.3, 0.86, 0.55],
        [0.18, 0.52, 0.95]]}
    cellWidth={84}
    cellHeight={48}
    rowLabelWidth={56}
    colorScale='strength' />
</Scene>
```

### TiledMatrix

Matrix with named regions and boundaries.

Props:
- `ariaLabel`: `string` (optional)
- `c`: `ThemeColors` (required)
- `cellSize`: `number` (optional)
- `cols`: `number` (required)
- `crossedOut`: `boolean` (optional; allowed values: `propEnums.values1`)
- `description`: `string` (optional): Context to include with the generated data summary.
- `gap`: `number` (optional)
- `id`: `string` (optional): Stable namespace for this chart's exported SVG metadata.
- `muted`: `boolean` (optional; allowed values: `propEnums.values1`)
- `regions`: `TiledMatrixRegion[]` (optional)
- `rows`: `number` (required)
- `subtitle`: `React.ReactNode` (optional)
- `title`: `React.ReactNode` (optional)
- `tone`: `ToneName` (optional; allowed values: `propEnums.ToneName`)

```tsx
width = 960
height = 480

<Scene padding={22} gap={18}>
  <TiledMatrix
    title='Block sparse attention'
    subtitle='Named regions within one matrix'
    rows={8}
    cols={10}
    cellSize={25}
    gap={3}
    tone='cyan'
    regions={[{ rowStart: 0, rowEnd: 4, colStart: 0, colEnd: 4, tone: 'purple' },
        { rowStart: 4, rowEnd: 8, colStart: 4, colEnd: 10, tone: 'green' }]} />
</Scene>
```

### DataTable

Structured rows and columns with headers.

Props:
- `align`: `readonly ("center" | "left" | "right")[]` (optional): Text alignment per column. Defaults to left for the first column and right for the rest.
- `c`: `ThemeColors` (required)
- `cellHeight`: `number` (optional)
- `cellWidth`: `number` (optional): Fixed column width. Omit it to size columns to their content.
- `firstColWidth`: `number` (optional)
- `fontSize`: `number` (optional)
- `gap`: `number` (optional)
- `headerCols`: `number` (optional)
- `headerRows`: `number` (optional)
- `math`: `boolean` (optional; allowed values: `propEnums.values1`)
- `rows`: `React.ReactNode[][]` (required)
- `width`: `string | number` (optional)

```tsx
width = 960
height = 480

<Scene padding={22} gap={18}>
  <Column
    width={540}
    gap={16}
    align='stretch'>
  <TextLabel
    text='Service health · sample five-minute window'
    fontSize={18} />
  <DataTable
    cellWidth={160}
    firstColWidth={180}
    cellHeight={42}
    fontSize={14}
    rows={[[ 'Service', 'p95 latency', 'State' ],
        [ 'API', '82 ms', 'healthy' ],
        [ 'Search', '126 ms', 'healthy' ],
        [ 'Worker', '340 ms', 'degraded' ],
        [ 'Exports', '210 ms', 'healthy' ]]} />
  </Column>
</Scene>
```

### Grid

General labeled cell grid.

Props:
- `c`: `ThemeColors` (required)
- `cellHeight`: `number` (optional)
- `cellWidth`: `number` (optional)
- `fontSize`: `number` (optional)
- `gap`: `number` (optional)
- `headerCols`: `number` (optional)
- `headerRows`: `number` (optional)
- `math`: `boolean` (optional; allowed values: `propEnums.values1`)
- `radius`: `number` (optional)
- `rows`: `(React.ReactNode | GridCell)[][]` (required)

```tsx
width = 960
height = 480

<Scene padding={22} gap={18}>
  <Grid
    cellWidth={72}
    cellHeight={42}
    headerRows={1}
    headerCols={1}
    rows={[[ 'Region', 'US', 'EU' ],
        [ 'API',
        { label: 'ready', tone: 'green' },
        { label: 'watch', tone: 'warm' } ],
        [ 'Queue',
        { label: 'ready', tone: 'green' },
        { label: 'ready', tone: 'green' }]]} />
</Scene>
```

### MiniBarChart

Compact categorical bars for dense layouts.

Props:
- `ariaLabel`: `string` (optional)
- `barWidth`: `number` (optional)
- `c`: `ThemeColors` (required)
- `data`: `MiniBarDatum[]` (required)
- `description`: `string` (optional): Context to include with the generated data summary.
- `fontSize`: `number` (optional)
- `gap`: `number` (optional)
- `height`: `number` (optional)
- `id`: `string` (optional): Stable namespace for this chart's exported SVG metadata.
- `max`: `number` (optional)
- `minBarHeight`: `number` (optional)
- `radius`: `number` (optional)
- `showValues`: `boolean` (optional; allowed values: `propEnums.values1`)

```tsx
width = 960
height = 480

<Scene padding={22} gap={18}>
  <Column
    gap={20}
    align='start'>
  <TextLabel
    text='Stage pass rates · 100 runs per stage'
    fontSize={18} />
  <MiniBarChart
    height={190}
    max={100}
    barWidth={58}
    gap={22}
    fontSize={14} showValues
    data={[{ label: 'plan', value: 74, valueLabel: '74%', tone: 'blue' },
        { label: 'build', value: 92, valueLabel: '92%', tone: 'purple' },
        { label: 'check', value: 86, valueLabel: '86%', tone: 'cyan' },
        { label: 'ship', value: 98, valueLabel: '98%', tone: 'green' }]} />
  </Column>
</Scene>
```
## Charts



Common comparisons, trends, distributions, and analytical plots.



### ChartFrame

Shared chart title, subtitle, legend, and footer.

Props:
- `ariaLabel`: `string` (optional)
- `c`: `ThemeColors` (required)
- `children`: `React.ReactNode` (required)
- `description`: `string` (optional): Context to include with the generated data summary.
- `footer`: `string` (optional)
- `framed`: `boolean` (optional; allowed values: `propEnums.values1`): Draw a card around the chart. Off by default, since most charts already sit in a panel.
- `height`: `string | number` (optional)
- `id`: `string` (optional): Stable namespace for this chart's exported SVG metadata.
- `legend`: `ChartLegendItem[]` (optional)
- `padding`: `number` (optional)
- `subtitle`: `string` (optional)
- `title`: `string` (optional)
- `width`: `string | number` (optional)

```tsx
width = 960
height = 600

<Scene padding={22} gap={18}>
  <ChartFrame
    title='Weekly quality'
    subtitle='A reusable title, legend, and footer'
    width={680}
    footer='Illustrative evaluation data · not measured results'>
  <BarChart
    width={620}
    height={220}
    data={[{ label: 'Mon', value: 0.72, color: 'secondary' },
        { label: 'Wed', value: 0.84, color: 'primary' },
        { label: 'Fri', value: 0.96, color: 'positive' }]}
    format='percent' />
  </ChartFrame>
</Scene>
```

### DonutChart

Part-to-whole composition with center label.

Props:
- `ariaLabel`: `string` (optional)
- `c`: `ThemeColors` (required)
- `centerLabel`: `React.ReactNode` (optional)
- `centerValue`: `React.ReactNode` (optional)
- `description`: `string` (optional): Context to include with the generated data summary.
- `footer`: `string` (optional)
- `format`: `ChartValueFormat` (optional; literal presets: `propEnums.ChartValueFormat` (other non-literal variants remain TSX-only))
- `height`: `number` (optional)
- `id`: `string` (optional): Stable namespace for this chart's exported SVG metadata.
- `segments`: `DonutChartSegment[]` (required)
- `showLegend`: `boolean` (optional; allowed values: `propEnums.values1`)
- `size`: `number` (optional)
- `subtitle`: `string` (optional)
- `thickness`: `number` (optional)
- `title`: `string` (optional)
- `width`: `number` (optional)

```tsx
width = 960
height = 520

<Scene title="Aster Studio · first-session activation" subtitle="SAMPLE DATA · 2,400 new workspaces" padding={22} gap={18}>
  <DonutChart
    title='72% created a project before leaving'
    subtitle='What 2,400 new teams did in their first 10 minutes'
    width={840}
    height={300}
    size={244}
    thickness={38}
    format='integer'
    centerValue='72%'
    centerLabel='activated'
    segments={[{ label: 'Created a project', value: 1728, color: 'positive' },
        { label: 'Explored templates', value: 432, color: 'primary' },
        { label: 'Left before setup', value: 240, color: 'critical' }]} />
</Scene>
```

### PieChart

Part-to-whole composition with a full circular radius.

Props:
- `ariaLabel`: `string` (optional)
- `c`: `ThemeColors` (required)
- `description`: `string` (optional): Context to include with the generated data summary.
- `footer`: `string` (optional)
- `format`: `ChartValueFormat` (optional; literal presets: `propEnums.ChartValueFormat` (other non-literal variants remain TSX-only))
- `height`: `number` (optional)
- `id`: `string` (optional): Stable namespace for this chart's exported SVG metadata.
- `segments`: `DonutChartSegment[]` (required)
- `showLegend`: `boolean` (optional; allowed values: `propEnums.values1`)
- `size`: `number` (optional)
- `subtitle`: `string` (optional)
- `title`: `string` (optional)
- `width`: `number` (optional)

```tsx
width = 960
height = 570

<Scene title="Aster Studio · launch-day acquisition" subtitle="SAMPLE DATA · 42,600 first-touch visits" padding={22} gap={18}>
  <PieChart
    title='Community made the biggest first impression'
    subtitle='42,600 first-touch visits on launch day'
    width={840}
    height={320}
    size={250}
    format='integer'
    segments={[{ label: 'Community', value: 18700, color: 'positive' },
        { label: 'Search', value: 12400, color: 'primary' },
        { label: 'Social', value: 7900, color: 'secondary' },
        { label: 'Partners', value: 3600, color: 'warning' }]}
    footer='Sample launch data · first touch only' />
</Scene>
```

### StackedBar

Part-to-whole values along one dimension.

Props:
- `ariaLabel`: `string` (optional)
- `c`: `ThemeColors` (required)
- `description`: `string` (optional): Context to include with the generated data summary.
- `height`: `number` (optional)
- `id`: `string` (optional): Stable namespace for this chart's exported SVG metadata.
- `segments`: `StackedBarSegment[]` (required)
- `showLegend`: `boolean` (optional; allowed values: `propEnums.values1`)
- `subtitle`: `string` (optional)
- `title`: `string` (optional)
- `width`: `number` (optional)

```tsx
width = 960
height = 340

<Scene title="Aster Studio · device mix" subtitle="SAMPLE DATA · 48,000 launch sessions" padding={22} gap={18}>
  <StackedBar
    title='Mobile already brings 38% of launch traffic'
    subtitle='Share of 48,000 sessions by device'
    width={840}
    height={76}
    segments={[{ label: 'Desktop', value: 54, valueLabel: '54%', color: 'primary' },
        { label: 'Mobile', value: 38, valueLabel: '38%', color: 'positive' },
        { label: 'Tablet', value: 8, valueLabel: '8%', color: 'warning' }]} />
</Scene>
```

### BarChart

Categorical comparison with labels and values.

Props:
- `ariaLabel`: `string` (optional)
- `c`: `ThemeColors` (required)
- `data`: `BarChartDatum[]` (required)
- `description`: `string` (optional): Context to include with the generated data summary.
- `footer`: `string` (optional)
- `format`: `ChartValueFormat` (optional; literal presets: `propEnums.ChartValueFormat` (other non-literal variants remain TSX-only))
- `height`: `number` (optional)
- `id`: `string` (optional): Stable namespace for this chart's exported SVG metadata.
- `max`: `number` (optional)
- `min`: `number` (optional)
- `showGrid`: `boolean` (optional; allowed values: `propEnums.values1`)
- `showValues`: `boolean` (optional; allowed values: `propEnums.values1`)
- `subtitle`: `string` (optional)
- `title`: `string` (optional)
- `width`: `number` (optional)
- `yAxisLabel`: `string` (optional)

```tsx
width = 960
height = 600

<Scene title="Aster Studio · signups in week one" subtitle="SAMPLE DATA · daily new workspaces" padding={22} gap={18}>
  <BarChart
    title='Friday set a new signup record'
    subtitle='Daily new workspaces · target was 250'
    width={840}
    height={360}
    min={0}
    max={380}
    format='integer'
    yAxisLabel='new workspaces'
    footer='Sample launch week · 1,626 workspaces total'
    data={[{ label: 'Mon', value: 186, color: 'primary' },
        { label: 'Tue', value: 204, color: 'primary' },
        { label: 'Wed', value: 231, color: 'secondary' },
        { label: 'Thu', value: 278, color: 'positive' },
        { label: 'Fri', value: 356, color: 'positive' },
        { label: 'Sat', value: 242, color: 'secondary' },
        { label: 'Sun', value: 129, color: 'neutral' }]} />
</Scene>
```

### LineChart

One or more trends across an ordered axis.

Props:
- `ariaLabel`: `string` (optional)
- `c`: `ThemeColors` (required)
- `description`: `string` (optional): Context to include with the generated data summary.
- `directLabels`: `boolean` (optional; allowed values: `propEnums.values1`): Name each line at its end instead of in a legend. On when there are several series.
- `footer`: `string` (optional)
- `format`: `ChartValueFormat` (optional; literal presets: `propEnums.ChartValueFormat` (other non-literal variants remain TSX-only))
- `height`: `number` (optional)
- `id`: `string` (optional): Stable namespace for this chart's exported SVG metadata.
- `labels`: `string[]` (optional)
- `max`: `number` (optional)
- `min`: `number` (optional)
- `series`: `LineChartSeries[]` (required)
- `showGrid`: `boolean` (optional; allowed values: `propEnums.values1`)
- `showPoints`: `boolean` (optional; allowed values: `propEnums.values1`)
- `subtitle`: `string` (optional)
- `title`: `string` (optional)
- `width`: `number` (optional)
- `yAxisLabel`: `string` (optional)

```tsx
width = 960
height = 600

<Scene title="Aster Studio · guided setup experiment" subtitle="SAMPLE DATA · activation over seven days" padding={22} gap={18}>
  <LineChart
    title='The checklist added 19 points of activation'
    subtitle='Share of new workspaces with a first project · week after launch'
    width={840}
    height={360}
    labels={['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']}
    min={0}
    max={1}
    yAxisLabel='activated'
    format='percent'
    directLabels={false}
    footer='Sample experiment · guided setup vs. original flow'
    series={[{ name: 'Original flow', points: [0.36, 0.39, 0.42, 0.45, 0.47, 0.5, 0.51], color: 'neutral' },
        { name: 'Guided checklist', points: [0.35, 0.4, 0.48, 0.55, 0.61, 0.68, 0.7], color: 'positive', area: true }]} />
</Scene>
```

### ScatterPlot

Point distribution across two numeric axes.

Props:
- `ariaLabel`: `string` (optional)
- `c`: `ThemeColors` (required)
- `description`: `string` (optional): Context to include with the generated data summary.
- `footer`: `string` (optional)
- `formatX`: `ChartValueFormat` (optional; literal presets: `propEnums.ChartValueFormat` (other non-literal variants remain TSX-only))
- `formatY`: `ChartValueFormat` (optional; literal presets: `propEnums.ChartValueFormat` (other non-literal variants remain TSX-only))
- `height`: `number` (optional)
- `id`: `string` (optional): Stable namespace for this chart's exported SVG metadata.
- `points`: `ScatterPoint[]` (required)
- `showGrid`: `boolean` (optional; allowed values: `propEnums.values1`)
- `subtitle`: `string` (optional)
- `title`: `string` (optional)
- `width`: `number` (optional)
- `xAxisLabel`: `string` (optional)
- `xMax`: `number` (optional)
- `xMin`: `number` (optional)
- `yAxisLabel`: `string` (optional)
- `yMax`: `number` (optional)
- `yMin`: `number` (optional)

```tsx
width = 960
height = 620

<Scene title="Aster Studio · onboarding flows" subtitle="SAMPLE DATA · time to first project and activation" padding={22} gap={18}>
  <ScatterPlot
    title='Shorter paths reach activation more often'
    subtitle='Six onboarding flows · time to first project vs. completion rate'
    width={840}
    height={380}
    xMin={0}
    xMax={9}
    yMin={0}
    yMax={1}
    xAxisLabel='minutes to first project'
    yAxisLabel='activated workspaces'
    formatX='integer'
    formatY='percent'
    footer='Sample cohort · one point per guided path'
    points={[{ x: 1.1, y: 0.86, label: 'One-click', color: 'positive', size: 10 },
        { x: 2.3, y: 0.79, label: 'Template', color: 'primary' },
        { x: 3.6, y: 0.73, label: 'Guided', color: 'secondary' },
        { x: 4.7, y: 0.65, label: 'Team invite', color: 'info' },
        { x: 5.8, y: 0.55, label: 'Import', color: 'warning' },
        { x: 7.6, y: 0.39, label: 'Blank start', color: 'neutral' }]} />
</Scene>
```

### ParetoBarChart

Descending category counts with a cumulative percentage line.

Props:
- `ariaLabel`: `string` (optional)
- `c`: `ThemeColors` (required)
- `data`: `BarChartDatum[]` (required)
- `description`: `string` (optional): Context to include with the generated data summary.
- `footer`: `string` (optional)
- `format`: `ChartValueFormat` (optional; literal presets: `propEnums.ChartValueFormat` (other non-literal variants remain TSX-only))
- `height`: `number` (optional)
- `id`: `string` (optional): Stable namespace for this chart's exported SVG metadata.
- `showGrid`: `boolean` (optional; allowed values: `propEnums.values1`)
- `showValues`: `boolean` (optional; allowed values: `propEnums.values1`)
- `subtitle`: `string` (optional)
- `title`: `string` (optional)
- `width`: `number` (optional)

```tsx
width = 960
height = 610

<Scene title="Aster Studio · onboarding support" subtitle="SAMPLE DATA · 240 first-week tickets" padding={22} gap={18}>
  <ParetoBarChart
    title='Three fixes could remove 84% of support tickets'
    subtitle='First-week onboarding issues · 240 tickets, sorted by count'
    width={840}
    height={370}
    format='integer'
    footer='The line tracks cumulative share of all tickets.'
    data={[{ label: 'Setup', value: 91, color: 'critical' },
        { label: 'Invites', value: 65, color: 'warning' },
        { label: 'Import', value: 46, color: 'secondary' },
        { label: 'Billing', value: 24, color: 'primary' },
        { label: 'Other', value: 14, color: 'neutral' }]} />
</Scene>
```

### ParetoChart

Automatic non-dominated frontier across competing numeric objectives.

Props:
- `ariaLabel`: `string` (optional)
- `c`: `ThemeColors` (required)
- `description`: `string` (optional): Context to include with the generated data summary.
- `footer`: `string` (optional)
- `formatX`: `ChartValueFormat` (optional; literal presets: `propEnums.ChartValueFormat` (other non-literal variants remain TSX-only))
- `formatY`: `ChartValueFormat` (optional; literal presets: `propEnums.ChartValueFormat` (other non-literal variants remain TSX-only))
- `frontierColor`: `string` (optional)
- `goalLabel`: `string` (optional)
- `height`: `number` (optional)
- `id`: `string` (optional): Stable namespace for this chart's exported SVG metadata.
- `points`: `ScatterPoint[]` (required)
- `showGoal`: `boolean` (optional; allowed values: `propEnums.values1`)
- `showGrid`: `boolean` (optional; allowed values: `propEnums.values1`)
- `subtitle`: `string` (optional)
- `title`: `string` (optional)
- `width`: `number` (optional)
- `xAxisLabel`: `string` (optional)
- `xMax`: `number` (optional)
- `xMin`: `number` (optional)
- `xObjective`: `ParetoObjective` (optional; allowed values: `propEnums.ParetoObjective`)
- `xScale`: `ParetoXScale` (optional; allowed values: `propEnums.ParetoXScale`)
- `yAxisLabel`: `string` (optional)
- `yMax`: `number` (optional)
- `yMin`: `number` (optional)
- `yObjective`: `ParetoObjective` (optional; allowed values: `propEnums.ParetoObjective`)

```tsx
width = 960
height = 620

<Scene title="Aster Studio · renderer trade-offs" subtitle="SYNTHETIC BENCHMARK · render time and image score" padding={22} gap={18}>
  <ParetoChart
    title='Studio is the quickest preset above 90 quality'
    subtitle='Sample render benchmark · image score vs. seconds per frame'
    width={840}
    height={380}
    xMin={0}
    xMax={9}
    yMin={65}
    yMax={105}
    xAxisLabel='render time · seconds'
    yAxisLabel='image score · / 100'
    formatX='decimal'
    formatY='integer'
    xObjective='minimize'
    yObjective='maximize'
    footer='Synthetic benchmark · aim for the upper-left frontier.'
    points={[{ x: 0.6, y: 71, label: 'Swift', color: 'info' },
        { x: 1.3, y: 82, label: 'Balanced', color: 'primary' },
        { x: 2.2, y: 91, label: 'Studio', color: 'positive', size: 10 },
        { x: 4.7, y: 96, label: 'Cinema', color: 'secondary' },
        { x: 7.6, y: 98, label: 'Ultra', color: 'primary' },
        { x: 3.4, y: 79, label: 'Legacy', color: 'neutral' },
        { x: 3.8, y: 88, label: 'Oversharp', color: 'warning' }]}
    frontierColor='positive'
    goalLabel='less time · higher score' />
</Scene>
```

### QuadrantChart

Numeric decision map split by configurable policy thresholds.

Props:
- `ariaLabel`: `string` (optional)
- `c`: `ThemeColors` (required)
- `description`: `string` (optional): Context to include with the generated data summary.
- `footer`: `string` (optional)
- `formatX`: `ChartValueFormat` (optional; literal presets: `propEnums.ChartValueFormat` (other non-literal variants remain TSX-only))
- `formatY`: `ChartValueFormat` (optional; literal presets: `propEnums.ChartValueFormat` (other non-literal variants remain TSX-only))
- `height`: `number` (optional)
- `id`: `string` (optional): Stable namespace for this chart's exported SVG metadata.
- `points`: `ScatterPoint[]` (required)
- `regions`: `{ topLeft: QuadrantRegion; topRight: QuadrantRegion; bottomLeft: QuadrantRegion; bottomRight: QuadrantRegion; }` (required)
- `showGrid`: `boolean` (optional; allowed values: `propEnums.values1`)
- `showTicks`: `boolean` (optional; allowed values: `propEnums.values1`)
- `subtitle`: `string` (optional)
- `title`: `string` (optional)
- `width`: `number` (optional)
- `xAxisLabel`: `string` (optional)
- `xMax`: `number` (optional)
- `xMin`: `number` (optional)
- `xThreshold`: `number` (optional)
- `yAxisLabel`: `string` (optional)
- `yMax`: `number` (optional)
- `yMin`: `number` (optional)
- `yThreshold`: `number` (optional)

```tsx
width = 960
height = 620

<Scene title="Aster Studio · next sprint" subtitle="SAMPLE ROADMAP · effort and retention impact" padding={22} gap={18}>
  <QuadrantChart
    title='Templates are a quick win; team spaces are the big bet'
    subtitle='Effort vs. estimated lift in week-four retention'
    width={840}
    height={380}
    xMin={0}
    xMax={1}
    yMin={0}
    yMax={1}
    xAxisLabel='engineering effort'
    yAxisLabel='retention lift'
    formatX='percent'
    formatY='percent' showTicks showGrid
    footer='Sample roadmap discussion · thresholds split the four regions.'
    regions={{ topLeft: { label: 'Quick wins', detail: 'High impact, lower effort', color: 'positive', emphasis: true }, topRight: { label: 'Big bets', detail: 'Worth a larger investment', color: 'primary' }, bottomLeft: { label: 'Polish later', detail: 'Small lift, easy work', color: 'neutral' }, bottomRight: { label: 'Too costly', detail: 'Revisit with new evidence', color: 'warning' } }}
    points={[{ x: 0.2, y: 0.79, label: 'Templates', color: 'positive', size: 10 },
        { x: 0.78, y: 0.82, label: 'Team spaces', color: 'primary', size: 10 },
        { x: 0.81, y: 0.24, label: 'Billing portal', color: 'warning' },
        { x: 0.22, y: 0.22, label: 'Icon polish', color: 'neutral' },
        { x: 0.44, y: 0.67, label: 'Guided tour', color: 'secondary' }]} />
</Scene>
```

### IntervalPlot

Low, midpoint, and high ranges by category.

Props:
- `ariaLabel`: `string` (optional)
- `axisLabel`: `string` (optional)
- `c`: `ThemeColors` (required)
- `data`: `IntervalDatum[]` (required)
- `description`: `string` (optional): Context to include with the generated data summary.
- `footer`: `string` (optional)
- `format`: `ChartValueFormat` (optional; literal presets: `propEnums.ChartValueFormat` (other non-literal variants remain TSX-only))
- `height`: `number` (optional)
- `id`: `string` (optional): Stable namespace for this chart's exported SVG metadata.
- `max`: `number` (optional)
- `min`: `number` (optional)
- `subtitle`: `string` (optional)
- `title`: `string` (optional)
- `width`: `number` (optional)

```tsx
width = 960
height = 600

<Scene title="Aster Studio · export timing" subtitle="DEMO TIMINGS · 100 sample renders" padding={22} gap={18}>
  <IntervalPlot
    title='SVG stays quick; animation takes the longest'
    subtitle='Median and observed range across 100 sample renders'
    width={840}
    height={360}
    min={0}
    max={1000}
    axisLabel='render time · milliseconds'
    format='integer'
    footer='Same 1,200 × 630 source frame · demo timings'
    data={[{ label: 'SVG', low: 120, mid: 180, high: 260, color: 'positive' },
        { label: 'PNG', low: 220, mid: 360, high: 510, color: 'primary' },
        { label: 'GIF', low: 420, mid: 650, high: 940, color: 'warning' }]} />
</Scene>
```

### Legend

Reusable color key for charts and diagrams.

Props:
- `c`: `ThemeColors` (required)
- `items`: `LegendItem[]` (required)
- `title`: `string` (optional)

```tsx
width = 960
height = 480

<Scene padding={22} gap={18}>
  <Legend
    title='Result types'
    items={[{ label: 'Success', color: 'positive' },
        { label: 'Queued', color: 'warning' },
        { label: 'Blocked', color: c.critical, style: 'dashed' }]} />
</Scene>
```
## Math



Number lines and geometric constructions for clear mathematical reasoning.



### NumberLine

Signed intervals, labeled values, and markers on a numeric line.

Props:
- `ariaLabel`: `string` (optional)
- `c`: `ThemeColors` (required)
- `height`: `number` (optional)
- `interval`: `NumberLineInterval` (optional)
- `markers`: `NumberLineMarker[]` (optional)
- `max`: `number` (required)
- `min`: `number` (required)
- `ticks`: `NumberLineTick[]` (optional)
- `title`: `string` (optional)
- `width`: `number` (optional)

```tsx
width = 960
height = 640

<Scene padding={22} gap={18}>
  <NumberLine
    c={c}
    min={-3}
    max={3}
    ticks={[{ value: -2 },
        { value: -1 },
        { value: 0 },
        { value: 1 },
        { value: 2 }]}
    markers={[{ id: 'origin', label: '0', value: 0, color: c.info }]}
    interval={{ from: -1.5, to: 2, includeFrom: true, includeTo: false, label: '−3/2 ≤ x < 2', color: c.primary }}
    title='A half-open interval' />
</Scene>
```

### GeometryDiagram

Labeled points, measured segments, and angle constructions.

Props:
- `angles`: `GeometryAngle[]` (optional)
- `ariaLabel`: `string` (optional)
- `c`: `ThemeColors` (required)
- `height`: `number` (optional)
- `points`: `GeometryPoint[]` (required)
- `segments`: `GeometrySegment[]` (optional)
- `title`: `string` (optional)
- `width`: `number` (optional)

```tsx
width = 960
height = 640

<Scene padding={22} gap={18}>
  <GeometryDiagram
    c={c}
    width={620}
    height={380}
    points={[{ id: 'A', x: 0, y: 0 },
        { id: 'B', x: 4, y: 0 },
        { id: 'C', x: 4, y: 3 }]}
    segments={[{ from: 'A', to: 'B', label: '4' },
        { from: 'B', to: 'C', label: '3' },
        { from: 'A', to: 'C', label: '5' }]}
    angles={[{ vertex: 'B', from: 'A', to: 'C', label: '90°' }]}
    title='A 3–4–5 right triangle' />
</Scene>
```
## Physics



Force vectors and wave superposition with explicit axes and measurements.



### FreeBodyDiagram

Forces, components, and the resultant acting on one body.

Props:
- `ariaLabel`: `string` (optional)
- `c`: `ThemeColors` (required)
- `forces`: `ForceVector[]` (required)
- `height`: `number` (optional)
- `showComponents`: `boolean` (optional; allowed values: `propEnums.values1`)
- `showResultant`: `boolean` (optional; allowed values: `propEnums.values1`)
- `title`: `string` (optional)
- `width`: `number` (optional)

```tsx
width = 960
height = 640

<Scene padding={22} gap={18}>
  <FreeBodyDiagram
    c={c}
    width={620}
    height={380}
    forces={[{ id: 'weight', label: 'weight · 9.8 N', x: 0, y: -9.8, color: c.primary },
        { id: 'normal', label: 'normal · 9.8 N', x: 0, y: 9.8, color: c.info },
        { id: 'push', label: 'push · 8 N', x: 8, y: 0, color: c.positive },
        { id: 'drag', label: 'drag · 3 N', x: -3, y: 0, color: c.warning }]} showResultant showComponents
    title='Net force · 5 N to the right' />
</Scene>
```

### WaveDiagram

Component waves and their superposition across position and time.

Props:
- `ariaLabel`: `string` (optional)
- `c`: `ThemeColors` (required)
- `height`: `number` (optional)
- `probeX`: `number` (optional)
- `sampleCount`: `number` (optional)
- `showComponents`: `boolean` (optional; allowed values: `propEnums.values1`)
- `showSum`: `boolean` (optional; allowed values: `propEnums.values1`)
- `time`: `number` (required)
- `title`: `string` (optional)
- `waves`: `Wave[]` (required)
- `width`: `number` (optional)
- `xMax`: `number` (required)
- `xMin`: `number` (required)
- `yMax`: `number` (optional)
- `yMin`: `number` (optional)

```tsx
width = 1040
height = 640

<Scene padding={22} gap={18}>
  <WaveDiagram
    c={c}
    width={760}
    height={420}
    waves={[{ amplitude: 1, wavelength: 4, phase: 0, color: c.info, label: 'wave 1 · A' },
        { amplitude: 1, wavelength: 4, phase: 0, color: c.primary, label: 'wave 2 · A' }]}
    time={0}
    xMin={-4}
    xMax={4}
    yMin={-2.4}
    yMax={2.4} showComponents showSum
    probeX={1}
    title='In-phase sum · amplitude 2A' />
</Scene>
```
## Chemistry



Molecular structures, balanced equations, and reaction energy profiles.



### MoleculeDiagram

Element-labeled atoms, bond orders, charges, and isotopes.

Props:
- `atoms`: `MoleculeAtom[]` (required)
- `bonds`: `MoleculeBond[]` (required)
- `c`: `ThemeColors` (required)
- `height`: `number` (optional)
- `title`: `string` (optional)
- `width`: `number` (optional)

```tsx
width = 960
height = 640

<Scene padding={22} gap={18}>
  <MoleculeDiagram
    c={c}
    width={540}
    height={340}
    atoms={[{ id: 'O', element: 'O', x: 0, y: 0 },
        { id: 'H1', element: 'H', x: -0.79, y: 0.61 },
        { id: 'H2', element: 'H', x: 0.79, y: 0.61 }]}
    bonds={[{ from: 'O', to: 'H1', order: 1 },
        { from: 'O', to: 'H2', order: 1 }]}
    title='Water · bent molecular geometry' />
</Scene>
```

### ReactionEquation

Reactants and products with coefficients, states, and conditions.

Props:
- `c`: `ThemeColors` (required)
- `conditions`: `string` (optional)
- `products`: `ReactionTerm[]` (required)
- `reactants`: `ReactionTerm[]` (required)
- `reversible`: `boolean` (optional; allowed values: `propEnums.values1`)
- `width`: `number` (optional)

```tsx
width = 960
height = 640

<Scene padding={22} gap={18}>
  <ReactionEquation
    c={c}
    width={760}
    reactants={[{ formula: 'H₂', coefficient: 2, state: 'g', composition: { H: 2 } },
        { formula: 'O₂', state: 'g', composition: { O: 2 } }]}
    products={[{ formula: 'H₂O', coefficient: 2, state: 'l', composition: { H: 2, O: 1 } }]}
    conditions='ignition' />
</Scene>
```

### ReactionEnergyDiagram

Reactant and product energy with activation barriers.

Props:
- `ariaLabel`: `string` (optional)
- `barrier`: `number` (required): Absolute peak energy for the uncatalyzed path, in the supplied unit.
- `c`: `ThemeColors` (required)
- `catalyzedBarrier`: `number` (optional): Absolute peak energy for the catalyzed path; endpoints remain unchanged.
- `height`: `number` (optional)
- `productLabel`: `string` (optional)
- `products`: `number` (required): Explicit relative energy for the product endpoint, in the supplied unit.
- `progress`: `number` (optional): Static path reveal from 0 to 1; does not change the path or its endpoints.
- `reactantLabel`: `string` (optional)
- `reactants`: `number` (required): Explicit relative energy for the reactant endpoint, in the supplied unit.
- `unit`: `string` (optional): Caller-supplied energy unit. Values are not converted or simulated.
- `width`: `number` (optional)

```tsx
width = 960
height = 640

<Scene padding={22} gap={18}>
  <ReactionEnergyDiagram
    c={c}
    width={760}
    height={380}
    reactants={0}
    products={-100}
    barrier={140}
    catalyzedBarrier={70}
    unit='relative energy'
    reactantLabel='reactants'
    productLabel='products' />
</Scene>
```
## AI



Normalized causal attention weights and query-to-token relationships.



### AttentionMatrix

Probability-normalized attention with causal masks and focus highlights.

Props:
- `activeKey`: `number` (optional)
- `activeQuery`: `number` (optional)
- `c`: `ThemeColors` (required)
- `height`: `number` (optional)
- `keyLabels`: `readonly string[]` (optional)
- `mask`: `readonly (readonly boolean[])[]` (optional): True permits attention. Masked entries must be zero.
- `queryLabels`: `readonly string[]` (optional)
- `title`: `string` (optional)
- `weights`: `readonly (readonly number[])[]` (required)
- `width`: `number` (optional)

```tsx
width = 1040
height = 640

<Scene padding={22} gap={18}>
  <AttentionMatrix
    c={c}
    width={760}
    height={420}
    queryLabels={['The', 'robot', 'turns', 'left']}
    keyLabels={['The', 'robot', 'turns', 'left']}
    weights={[[1, 0, 0, 0],
        [0.35, 0.65, 0, 0],
        [0.2, 0.3, 0.5, 0],
        [0.08, 0.18, 0.24, 0.5]]}
    mask={[[true, false, false, false],
        [true, true, false, false],
        [true, true, true, false],
        [true, true, true, true]]}
    activeQuery={3}
    activeKey={2}
    title='Causal attention · each row sums to 1' />
</Scene>
```
## Drawing



Low-level SVG geometry for custom technical figures.



### Box

Rounded SVG box with label and tone.

Props:
- `c`: `ThemeColors` (required)
- `color`: `ColorName` (optional; allowed values: `propEnums.ColorName`)
- `fontSize`: `number` (optional)
- `gradient`: `boolean` (optional; allowed values: `propEnums.values1`): Use gradient background instead of solid
- `height`: `number` (optional)
- `icon`: `string` (optional): Optional icon/emoji before label
- `label`: `string` (required)
- `outlined`: `boolean` (optional; allowed values: `propEnums.values1`): Outlined style instead of filled
- `radius`: `number` (optional): Border radius
- `sublabel`: `string` (optional): Optional sub-label below main label
- `width`: `number` (optional)

```tsx
width = 960
height = 480

<Scene padding={22} gap={18}>
  <Row
    gap={14}
    align='center'>
  <Box
    label='Input'
    sublabel='request'
    color='primary' gradient
    width={180}
    height={100} />
  <Arrow
    direction='right'
    length={50} />
  <Box
    label='Output'
    sublabel='result'
    color='positive'
    width={180}
    height={100} />
  </Row>
</Scene>
```

### Arrow

Straight labeled SVG arrow.

Props:
- `c`: `ThemeColors` (required)
- `color`: `string` (optional)
- `direction`: `"left" | "right" | "down" | "up"` (optional; allowed values: `propEnums.values16`)
- `label`: `string` (optional)
- `length`: `number` (optional)

```tsx
width = 960
height = 480

<Scene padding={22} gap={18}>
  <Arrow
    direction='right'
    label='request'
    length={220}
    color={c.infoLight} />
</Scene>
```

### ArrowMarkerDef

Reusable SVG arrowhead definition.

Props:
- `color`: `string` (required)
- `direction`: `"start" | "end"` (optional; allowed values: `propEnums.values17`)
- `id`: `string` (required)
- `size`: `number` (optional)

```tsx
width = 960
height = 480

<Scene padding={22} gap={18}>
  <Column
    gap={8}
    align='center'>
  <svg
    width='460'
    height='170'
    viewBox='0 0 460 170'>
  <defs>
  <ArrowMarkerDef
    id='catalog-arrow'
    color={c.infoLight}
    size={8} />
  </defs>
  <path
    d='M 42 85 C 140 8 280 162 410 85'
    fill='none'
    stroke={c.infoLight}
    strokeWidth={4}
    markerEnd='url(#catalog-arrow)' />
  </svg>
  <TextLabel
    text='Reusable SVG arrow marker'
    fontSize={14} />
  </Column>
</Scene>
```

### FlowArrow

Straight directional connector with label and tone.

Props:
- `c`: `ThemeColors` (required)
- `color`: `string` (optional)
- `direction`: `"left" | "right" | "down" | "up"` (optional; allowed values: `propEnums.values16`)
- `label`: `string` (optional)
- `length`: `number` (optional)
- `tone`: `ToneName` (optional; allowed values: `propEnums.ToneName`)

```tsx
width = 960
height = 480

<Scene padding={22} gap={18}>
  <FlowArrow
    direction='right'
    label='verified result'
    length={240}
    tone='green' />
</Scene>
```

### Connector

Alias for flow connectors in diagram code.

Props:
- `c`: `ThemeColors` (required)
- `color`: `string` (optional)
- `direction`: `"left" | "right" | "down" | "up"` (optional; allowed values: `propEnums.values16`)
- `label`: `string` (optional)
- `length`: `number` (optional)
- `tone`: `ToneName` (optional; allowed values: `propEnums.ToneName`)

```tsx
width = 960
height = 480

<Scene padding={22} gap={18}>
  <Connector
    direction='right'
    label='dependency'
    length={240}
    tone='purple' />
</Scene>
```

### VectorArrow

Vector with optional endpoints and arrowhead.

Props:
- `arrowSize`: `number` (optional): Arrowhead length as a multiplier of strokeWidth (default 2.2). Short segments cap the head at the segment length and omit the shaft; clipped heads stay wider than the shaft.
- `color`: `string` (required)
- `key`: `React.Key` (optional)
- `opacity`: `number` (optional)
- `strokeWidth`: `number` (optional)
- `x1`: `number` (required)
- `x2`: `number` (required)
- `y1`: `number` (required)
- `y2`: `number` (required)

```tsx
width = 960
height = 480

<Scene padding={22} gap={18}>
  <Column
    gap={8}
    align='center'>
  <svg
    width='460'
    height='180'
    viewBox='0 0 460 180'>
  <VectorArrow
    x1={64}
    y1={140}
    x2={390}
    y2={38}
    color={c.positiveLight}
    strokeWidth={5}
    arrowSize={5} />
  <SvgPoint
    cx={64}
    cy={140}
    r={8}
    fill={c.positiveLight} />
  </svg>
  <TextLabel
    text='Vector with arrowhead'
    fontSize={15} />
  </Column>
</Scene>
```

### VectorSegment

Line segment for geometric constructions.

Props:
- `color`: `string` (required)
- `dotRadius`: `number` (optional)
- `key`: `React.Key` (optional)
- `opacity`: `number` (optional)
- `showEndDot`: `boolean` (optional; allowed values: `propEnums.values1`)
- `showStartDot`: `boolean` (optional; allowed values: `propEnums.values1`)
- `strokeWidth`: `number` (optional)
- `x1`: `number` (required)
- `x2`: `number` (required)
- `y1`: `number` (required)
- `y2`: `number` (required)

```tsx
width = 960
height = 480

<Scene padding={22} gap={18}>
  <Column
    gap={8}
    align='center'>
  <svg
    width='460'
    height='180'
    viewBox='0 0 460 180'>
  <VectorSegment
    x1={64}
    y1={130}
    x2={396}
    y2={54}
    color={c.secondaryLight}
    strokeWidth={5} showStartDot showEndDot />
  </svg>
  <TextLabel
    text='Segment with endpoint dots'
    fontSize={15} />
  </Column>
</Scene>
```

### SvgFrame

SVG plotting frame with border and fill.

Props:
- `c`: `ThemeColors` (required)
- `fill`: `string` (optional)
- `height`: `number` (required)
- `key`: `React.Key` (optional)
- `opacity`: `number` (optional)
- `rx`: `number` (optional)
- `stroke`: `string` (optional)
- `strokeWidth`: `number` (optional)
- `width`: `number` (required)
- `x`: `number` (optional)
- `y`: `number` (optional)

```tsx
width = 960
height = 480

<Scene padding={22} gap={18}>
  <Column
    gap={8}
    align='center'>
  <svg
    width='460'
    height='180'
    viewBox='0 0 460 180'>
  <SvgFrame
    x={18}
    y={18}
    width={424}
    height={144}
    rx={18}
    fill={c.bgCard}
    stroke={c.borderLight}
    strokeWidth={2} />
  </svg>
  <TextLabel
    text='Theme aware plot frame'
    fontSize={15} />
  </Column>
</Scene>
```

### SvgPoint

Labeled point for SVG figures.

Props:
- `cx`: `number` (required)
- `cy`: `number` (required)
- `fill`: `string` (required)
- `key`: `React.Key` (optional)
- `opacity`: `number` (optional)
- `r`: `number` (optional)
- `stroke`: `string` (optional)
- `strokeWidth`: `number` (optional)

```tsx
width = 960
height = 480

<Scene padding={22} gap={18}>
  <Column
    gap={8}
    align='center'>
  <svg
    width='460'
    height='180'
    viewBox='0 0 460 180'>
  <SvgFrame
    width={460}
    height={180}
    c={c} />
  <SvgPoint
    cx={230}
    cy={90}
    r={26}
    fill={c.positiveLight}
    stroke={c.bg}
    strokeWidth={5} />
  </svg>
  <TextLabel
    text='SVG point with outline'
    fontSize={15} />
  </Column>
</Scene>
```

### DotPoint

Point marker with optional text label.

Props:
- `c`: `ThemeColors` (required)
- `color`: `string` (required)
- `label`: `string` (required)
- `labelOffset`: `{ x?: number; y?: number; }` (optional)
- `size`: `number` (optional)
- `x`: `number` (required)
- `y`: `number` (required)

```tsx
width = 960
height = 480

<Scene padding={22} gap={18}>
  <Column
    gap={12}
    align='center'>
  <div
    style={{ position: 'relative', display: 'flex', width: 460, height: 100 }}>{DotPoint({ x: 230, y: 60, label: 'deploy', color: c.infoLight, size: 14 })}</div>
  <TextLabel
    text='Deployment marker at (230, 60)'
    fontSize={15} />
  </Column>
</Scene>
```

### DashedLine

Dotted or dashed SVG guide line.

Props:
- `color`: `string` (required)
- `dotSize`: `number` (optional)
- `dotSpacing`: `number` (optional)
- `x1`: `number` (required)
- `x2`: `number` (required)
- `y1`: `number` (required)
- `y2`: `number` (required)

```tsx
width = 960
height = 480

<Scene padding={22} gap={18}>
  <Column
    gap={12}
    align='center'>
  <div
    style={{ position: 'relative', display: 'flex', width: 460, height: 100 }}>{DashedLine({ x1: 36, y1: 50, x2: 424, y2: 50, color: c.warningLight, dotSpacing: 12, dotSize: 3 })}</div>
  <TextLabel
    text='Threshold guide · evenly spaced dots'
    fontSize={15} />
  </Column>
</Scene>
```

### AxisPlot

Coordinate plane for points, vectors, and paths.

Props:
- `ariaLabel`: `string` (optional)
- `axisColor`: `string` (optional)
- `c`: `ThemeColors` (required)
- `children`: `React.ReactNode` (optional)
- `description`: `string` (optional): Context to include with the generated data summary.
- `frameFill`: `string` (optional)
- `frameRx`: `number` (optional)
- `frameStroke`: `string` (optional)
- `gridCount`: `number` (optional)
- `height`: `number` (required)
- `id`: `string` (optional): Stable namespace for this chart's exported SVG metadata.
- `padding`: `number` (optional)
- `paths`: `AxisPlotPath[]` (optional)
- `points`: `AxisPlotPoint[]` (optional)
- `showAxes`: `boolean` (optional; allowed values: `propEnums.values1`)
- `showFrame`: `boolean` (optional; allowed values: `propEnums.values1`)
- `showGrid`: `boolean` (optional; allowed values: `propEnums.values1`)
- `vectors`: `AxisPlotVector[]` (optional)
- `width`: `number` (required)
- `xAxisLabel`: `string` (optional)
- `xMax`: `number` (optional)
- `xMin`: `number` (optional)
- `yAxisLabel`: `string` (optional)
- `yMax`: `number` (optional)
- `yMin`: `number` (optional)

```tsx
width = 960
height = 560

<Scene padding={22} gap={18}>
  <AxisPlot
    width={700}
    height={380}
    xMin={-1}
    xMax={1}
    yMin={-1}
    yMax={1} showGrid
    xAxisLabel='x'
    yAxisLabel='y'
    points={[{ x: -0.5, y: 0.3, r: 6, tone: 'blue' },
        { x: 0.6, y: 0.7, r: 7, tone: 'green' }]}
    vectors={[{ x1: 0, y1: 0, x2: 0.45, y2: 0.55, tone: 'purple', arrow: true }]} />
</Scene>
```
## Shared prop value sets

Finite allowed values referenced by component props:

- `propEnums.values1`: [false,true]
- `propEnums.ToneName`: ["blue","purple","green","warm","cyan","pink","red","critical","neutral","sunset","ocean","dark"]
- `propEnums.values2`: ["row","column"]
- `propEnums.DiagramStatus`: ["ok","warn","error","idle","critical","neutral","blue","purple","green","warm","cyan","pink","red","sunset","ocean","dark"]
- `propEnums.RgbChannel`: ["rgb","red","green","blue"]
- `propEnums.values3`: ["center","flex-start","space-between","space-around"]
- `propEnums.FlexAlign`: ["start","center","end","stretch"]
- `propEnums.values4`: ["center","left"]
- `propEnums.values5`: ["sans","serif"]
- `propEnums.FlexJustify`: ["start","center","end","space-between","space-around"]
- `propEnums.values6`: ["vertical","horizontal"]
- `propEnums.values7`: ["window","browser","terminal"]
- `propEnums.IconName`: ["agent","bucket","browser","cache","chart","check","cluster","code","cloud","container","database","file","firewall","gateway","git","globe","image","layers","lock","load-balancer","mobile","monitor","network","play","queue","server","spark","storage","stream","terminal","tool","user","warning"]
- `propEnums.values8`: ["center","left","right"]
- `propEnums.values9`: ["code","title","subtitle","label","body","small","tiny","annotation"]
- `propEnums.values10`: ["start","end","middle"]
- `propEnums.ColorName`: ["primary","secondary","positive","warning","critical","info","accent","neutral","violet"]
- `propEnums.WatermarkPosition`: ["top-left","top-right","bottom-left","bottom-right"]
- `propEnums.StatusKind`: ["check","cross","warn","info","pending","dot"]
- `propEnums.values11`: ["LR","RL","TB","BT"]
- `propEnums.values12`: ["auto","manual"]
- `propEnums.GraphRouting`: ["dagre","straight","orthogonal"]
- `propEnums.values13`: ["content","fixed"]
- `propEnums.values14`: ["heat","strength"]
- `propEnums.values15`: ["decimal","percent","integer"]
- `propEnums.ChartValueFormat`: ["decimal","percent","integer","compact"]
- `propEnums.ParetoObjective`: ["minimize","maximize"]
- `propEnums.ParetoXScale`: ["linear","log"]
- `propEnums.values16`: ["left","right","down","up"]
- `propEnums.values17`: ["start","end"]

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.