figure Card
Figure Cards
A figure is a small, embeddable interactive graphic that demonstrates one thing — a p5.js sketch, a three.js scene, a D3/SVG graphic, or a canvas-loop TEA sketch.
The card's markdown body describes what the figure demonstrates (its intent). The runnable code does not live in the body — it lives in a .ts file inside the card's attach scope, and the entry field points to it.
Making a good figure usable
Whatever a figure is for, these make it usable and clear:
- One clear focus. Show one thing; resist cramming. Clarity beats completeness.
- Signal the affordances. What's interactive should look interactive — a draggable part, a slider, a button should read as such (visible handles, hover cues), not blend into the scene.
- Tell the viewer what to do. A short on-figure instruction ("drag the H⁺ to the base", "click a category") removes the guesswork; don't rely on the viewer discovering the interaction.
- Key what isn't self-evident. If colours, symbols, or marks carry meaning, include a small legend; if it responds to the keyboard, name the keys.
- Legible and unclipped. Readable text, enough contrast, no overlapping or garbled labels; lay it out so nothing is clipped. Match the box's quiet visual style — no decorative noise.
- Fit the container. Size from
mount.clientWidth(fall back if 0, e.g.|| 360), watch it with aResizeObserver(disconnect in teardown), and derive layout from the current canvas size — never a fixed pixel width or asizeparam. Give DOM controlswidth: 100%. A figure must work at phone width (~390px). - Verify it renders before you call it done. Open it and screenshot it: confirm it renders, the controls and instructions are visible, and nothing is clipped.
Frontmatter
runtime:— one ofp5js,three,d3,canvas-loop. Selects the mount harness. (p5js for canvas sketches & animation, three for 3D scenes, d3 for data-driven SVG — including node-link graphs, canvas-loop for deterministic TEA sketches with auto-generated controls and a headless verify loop.)entry:— required. Path to the source, e.g.attach/sketch.ts(resolved in the card's<basename>.attach/scope, like anyattach/ref).data:— optional free-form object of author config the sketch can read. Custom top-level frontmatter keys are stripped on load, so script config must live underdata, not as loose keys.params:— optional declared embed parameters, each{name, type, description?, default?}. Values are supplied by the embed link's query string, not by the card itself.
The source (entry)
The entry module is authored in the runtime's own style (not React) and default-exports (lib, { mount, figure }) => teardown:
lib— the runtime library (p5/three/d3), provided by the harness. Do not import it — it arrives as this argument.mount— the DOM element to render into.figure—{ params, data, meta, file }: the coerced embedparams, the card'sdata, the validated frontmattermeta, andfilehelpers for loading box files.- Return a teardown function (or nothing). It is required for runtimes that hold resources: p5 needs
instance.remove(), three needs animation- frame cancellation + disposal, D3 needs listener/DOM cleanup.
(The mount element and figure context travel together in the second argument because positional params are capped at two.)
// attach/sketch.ts (p5js)
export default function (p5, { mount, figure }) {
const width = () => Math.min(mount.clientWidth || 360, 640);
const instance = new p5((p) => {
p.setup = () => p.createCanvas(width(), Math.round(width() * 0.75));
p.draw = () => { /* derive layout from p.width / p.height, not a constant */ };
}, mount);
const ro = new ResizeObserver(() => {
if (instance.width !== width()) instance.resizeCanvas(width(), Math.round(width() * 0.75));
});
ro.observe(mount);
return () => { ro.disconnect(); instance.remove(); };
}
Embedding
Embed a figure inline in a card or document body with the image/embed syntax —  (a plain box path, like an image) — passing parameters in the query string. It renders frameless (just the figure) in place. A plain [label](…figure.card) link (no !) stays a navigable link, not an embed.
The sketch reads those values from figure.params; the caption (alt text) shows beneath the figure.
canvas-loop figures (deterministic TEA sketches)
For runtime: canvas-loop the entry is a TEA sketch module — canvas-loop's Elm-style contract: named exports params/init/update/draw (+ optional canvas) — plus a one-line default figure factory beneath them:
export default (cl, { mount, figure }) =>
cl.mountSketch(mount, { module: { params, init, update, draw }, initialParams: figure.params });
The module literal lists only the exports you declared.
paramsandcanvasare both optional (mountSketchdefaults: no controls, 400×300) — a sketch with both uses{ params, canvas, init, update, draw }; one with neither uses{ init, update, draw }. Referencing an undeclared identifier in the literal ships aReferenceErrorfigure (esbuild won't flag it), andinitialParamsonly applies to params the module declares.import typeONLY from@ianbicking/canvas-loop— the package is not resolvable inside a box, so a value import fails the compile (type-only imports are erased). The runtime API arrives as the factory'sclargument.Interactive controls come from the module's
export const params(canvas-loop's declaration: number/boolean/select/trigger) — sliders, checkboxes, selects, and trigger buttons render automatically.The card-level
params:field keeps its usual meaning — embed-query declarations — and maps onto module params by identical name. A param exposed both ways is declared in both vocabularies:# card frontmatter: the embed-query declaration params: - { name: speed, type: number, default: 1 }// module: the control declaration the boxholder sees export const params = { speed: { type: "number", min: 0, max: 5, default: 1 } } as const satisfies ParamsDecl;An embed link's
?speed=2then starts that slider at 2.The two vocabularies differ — map deliberately. The card
params:type is one ofstring/number/booleanonly (the card schema enforces this); the moduleparamstype isnumber/boolean/select/trigger. Bridge them by this table (a mismatch warns in the browser console and falls back to the declared default, so get it right):module param card type:note numbernumberdirect booleanbooleandirect selectstringthe embed value must be one of the select's optionstrigger— not embed-controllable (a trigger carries no value); omit it from card params:A card param matching no module param (or declared for a
trigger) is dropped — its query value does nothing.export const canvas = { width, height }is authoritative for the drawing size — the card'swidth/heightfields don't apply to canvas-loop figures.Verify headlessly (write → render → read the frame-tagged transcript; the canvas-loop authoring loop) rather than opening a browser to screenshot.
The contains: field
Give this card a one-sentence contains: — the prime retrieval field for bbx search and listings. How to write a good one (carry the information when it's concise, never a list of parts, under 200 characters) is in the agent guide's ABOUT_CARDS section.
Templates
figure
An embeddable interactive figure (p5.js / three.js / D3 / canvas-loop); scaffolds a runnable starter sketch
bbx create <path>.figure.card -t figure
Template: figure Description: An embeddable interactive figure (p5.js / three.js / D3 / canvas-loop); scaffolds a runnable starter sketch Card types: figure
Arguments: runtime: Runtime: p5js | three | d3 | canvas-loop title (optional): Display title