Circuit Trace Generators
Public Made by Adomby adom
Two dependency-free, seeded circuit-background generators: a seamless torus tile with ambient or progressive reveal, and the v9 continuous field with obstacles, scroll reveal, and five ambient systems.
Reveal build, the board growing itself in
A copy of the occupancy-grid engine that grows the board instead of showing it finished. Nothing in the original files changed:
| classic | reveal | |
|---|---|---|
| engine | circuit-background.js |
circuit-background-reveal.js |
| page | index-classic.html |
index-reveal.html |
| URL (port 8930) | / |
/reveal |
What happens
Copper grows out across the background as one front: each route draws itself out of an end via, with a white-hot head riding the growing tip and pads landing as the tip reaches them. When the last route lands, the hot layer cross-fades into the real SVG artwork.
The look: heat, not paint
A single bright colour everywhere reads as a neon sheet, not as something being made. Two rules fix that:
- Copper cools. The tip is near-white, the colour sits just behind it, and
everything already laid fades exponentially (
revealCool) toward a dim settled trace. At any instant the screen shows a bright rim, a warm wake, and a quiet interior, depth instead of one flat brightness. The default is 25 ms, the bottom of the knob and the shortest the engine can honour (the tail's tightening floors at 24), which leaves each route a wake about a tip-length long rather than a board that stays warm. Even there the fade only removes 48.7% per frame at 60fps, still short of the >50% a multiplicative fade needs to reach zero on its own, so the generation buffers below are doing real work at every setting. - Colour comes in regions. Hue is sampled from a smooth screen-space noise
field (
revealZonepx) across the engine's own teal → blue → violet ramp, so the board grows in broad zones of colour rather than confetti or one tone.
The settled copper keeps a trace of its zone's hue (revealSettle), so the
closing cross-fade into the uniform near-black artwork is a settling, not a cut.
Timing, the same motion language as the live signals
The reveal and the idle signal animation are one system, so the reveal borrows the signal system's timing rules rather than inventing its own:
- Constant speed, linear. A tip travels at
revealSpeedunits/sec, the same units and the same default (420) assignalSpeed. It is never eased and never scaled by a route's length, so every tip on screen moves at one pace and a long bus takes longer than a short comb tap. - The same stroke stack: a ~2.4-wide core over a 7-wide glow, like a signal's head and halo.
- The same arrival blip. A pad grows into place and rings out, the ring runs
from the pad radius out to ~8 over
revealBlip, matching the ring a signal leaves when it lands on a via. - The same palette (
revealPaletteis the signal palette verbatim).
Nothing appears or vanishes in one frame:
| moment | before | now |
|---|---|---|
| board at the start | cut to hidden | fades out over 260 ms under the growth |
| a route's head | popped in, vanished on landing | fades in (130 ms), fades out after landing (260 ms) |
| a pad | stamped at full size | scales up and rings out before being stamped into the settled layer |
| the via hole | drilled when the pop ended | annular from the first frame |
| signals after the reveal | a dozen appeared mid-flight at once | the fountain fills from empty at its own cadence |
| resize mid-reveal | restarted the whole growth | settles, but only on a REAL size change; the observer also fires when nothing moved, which was ending the reveal ~200 ms in |
| the handoff to the static board | cross-faded, dipped and shifted hue | invisible: the board comes up under an opaque canvas, then only the glow fades |
The layer that was missing. The single loudest "traces pop in fully formed" symptom had nothing to do with the schedule: the final composite drew only the settled and tip layers, so the two heat generations were sized, cleared, faded, rotated and stroked into every frame, and never reached the screen. A route's copper lives ONLY in the heat buffer until it lands, so every route was invisible while it grew and appeared whole the instant it was stamped into the settled layer. All that was visible mid-growth was the tip spark, and there was no glow at all. Measured by running the engine against a recording canvas: before, 2 of 4 layers were drawn into but never composited; after, all 4 composite and 3.5M px of stroke per reveal reaches the screen through the heat buffer.
The release queue was also sorted by metric, not by delay, while the release loop walks it with a single cursor. Jitter (±37 ms) is ~80x wider than the gap between neighbouring start times (0.91 ms median), which left 84% of the queue out of order and head-of-line blocked routes up to 71 ms behind the cursor before letting them out in one burst, the opposite of what the jitter is for. It is now sorted by delay.
A route starts growing when it is first drawn, not at the wall-clock moment
it was scheduled (startedAt, capped at REVEAL_CATCHUP ms of catch-up).
Deriving growth from the schedule means any slow frame, a GC pause, a busy tab,
a machine waking up, releases every route whose start time has passed already
fully grown: they pop into existence instead of animating. Measured before the
fix, with ~700 ms hitches in the frame stream: 941 of 2070 routes were born
complete, the median route born 106 px long against a typical length of ~78 px.
After: zero, and never more than 16 px. Pads are timed off the same start, so
a late route does not sprinkle its pads ahead of its own tip, and the closing
tail waits for the growth to finish rather than for the clock.
Start times also carry ±35 ms of scatter (REVEAL_JITTER). Without it the first
wave of short routes all landed on the same frame and their pads popped together
, a visible batch. Measured frame-to-frame ink change went from 7.8% of the board
to 3.0%, and the only frames above 2.5% are now the mid-point of the closing
cross-fade, where a fade is supposed to be steep.
The glow, and why it now reaches exactly zero
The glow accumulates in a buffer that is faded a little every frame, one fill,
whatever is warm. That is what keeps it O(1). But a multiplicative fade never
reaches zero: at 8 bits a pixel stops moving as soon as alpha x decay rounds
back to itself, so a multiply only lands on 0 if it removes more than half per
frame. Measured at a 150 ms heat fade (5.19%/frame): alpha ran
255 → 151 → 31 → 9, and stayed 9 forever, 3.5% of #00e6dc sitting on the
copper permanently, turning rgb(21, 27, 37) into rgb(20, 34, 43). That residue is
why the growing board read lighter and bluer than the finished one. Canvas has no
subtractive alpha operation, so no choice of fade curve fixes it.
The fix is a multiply by zero, applied where it costs nothing: the buffer is kept in two generations. New glow always goes into the current one. The other keeps decaying but is also composited through a falling ramp, and once that ramp reaches zero its buffer is cleared outright. Every pixel therefore ends at exactly zero, the clear happens at a moment when it changes nothing on screen, and the per-frame cost is still two fills and two composites regardless of how much of the board is glowing.
(An earlier attempt derived the whole glow from the routes every frame, in floating point. It was exact, but its cost scales with how much copper is warm, at a long heat fade that is the whole board, and it showed.)
Verified live at a 150 ms fade, sampling every fully-opaque pixel of the growth layer: the dominant colour is rgb(21, 27, 37) at t = 1.5 s, 3 s and 5 s, the artwork's own copper, exactly. Frame pacing with it: 4.2 ms median, 9.2 ms p95.
Matching the static board exactly
Measured against the SVG board it hands over to (same seed, same density, ink weighted by coverage):
| how the copper is laid | coverage | vs SVG | ink colour |
|---|---|---|---|
| SVG | 0.14129 | , | rgb(19.9, 25.8, 35.4) |
| one stroke per route, butt caps + miter joins | 0.14125 | −0.03% | rgb(19.9, 25.8, 35.4) |
| per-frame pieces (what it used to do) | 0.14261 | +0.93% | rgb(19.8, 25.7, 35.4) |
The colour was never the problem, it is identical either way. What differed was weight: stroking a route in per-frame pieces blends each piece's antialiased edge over the last one, leaving the canvas board ~1% denser than the SVG. So a route in progress is now redrawn whole on its own layer each frame, and stamped into the persistent layer exactly once when it lands, with the artwork's own caps and joins. Cost of redrawing the in-flight routes: 0.28 ms/frame (only ~4% of the board is growing at any instant; stroking the entire board once costs 7 ms).
That layer sits UNDER the glow. Copper painted over its own glow reads as a dark line with a coloured fringe instead of hot metal.
The tail is two phases, and that ordering matters. First the remaining glow is
cooled to nothing, the time constant tightens over handoff
(clamp(revealCool x 1.6, 120, 1100)) ms so the glow leaves as glow, at its
own rate. That floor was 260 ms, which at a short heat fade left the
finished board sitting untouched for ~150 ms with nothing left to cool; at the
25 ms default the whole reveal is now 4.3 s end to end instead of 5.3 s, with
the growth itself unchanged. Only then does the canvas dissolve
(REVEAL_HANDOFF, 260 ms), by which point it holds nothing but copper identical
to the SVG already at full opacity beneath it. Fading the canvas while it was
still glowing yanked the warm rim away instead of letting it cool, which reads as
a colour change, most obviously with heat fade at minimum, where the fade and
the decay ran at the same short duration. Measured at the dissolve: peak glow
above the board colour drops 187 → 16 (−97%) before anything fades.
The handoff deserves its own note. Cross-fading the growth layer out while the
SVG faded in looks obvious for two reasons: alpha compositing is not additive, so
two complementary alphas over identical copper dip to ~75% opacity halfway
through; and the settled copper carried a little of its zone's hue, so the whole
board changed colour at the same moment. Now the settled layer is stroked in the
artwork's exact colour (revealSettle: 1), the real SVG snaps to full opacity
underneath the still-opaque canvas the instant growth ends, and the canvas fades
off the top of it, so all that disappears is the glow. The growth
layers also match devicePixelRatio (capped at 2), or a soft board would hand
over to a crisp one.
A trap worth remembering: Chrome restores form-control values a beat after a
reload and fires change on each one. The tuning sliders' change handler
regenerates the board, so every reload silently kicked off two or three extra
reveals a few seconds in, which looked exactly like the animation randomly
restarting. autocomplete="off" on the form fixes it.
Why it is a canvas, not an animated SVG
The board is ONE tile in <defs>, instanced ~25× by <use>. That instancing is
what makes the background cheap, and it is also why the obvious implementations
do not work:
- Web Animations on the referenced content paint nothing. Chrome runs them
on the original element,
getComputedStyleshows the dash offset advancing perfectly, but<use>instances mirror DOM and attribute state only, not the animation's effect. The board looked frozen while the DOM said it was growing. (Do not "verify" an animation by reading the DOM. Look at pixels.) - Writing attributes every frame does propagate, but costs ~260 ms/frame. Every mutation on referenced content resyncs all 25 instances.
So the reveal renders on a <canvas> above the SVG, and the SVG is hidden until
the handoff. Two layers: settled (persistent, each route stamped once, when
it lands) and work (cleared every frame, growing copper, its glow, the heads
and the pads). Measured: 8.4 ms median frame, 13 ms p95 on a 3000×2000
viewport.
What keeps it cheap:
- Landed copper is free. A route is stroked into the persistent layer exactly once; after that it costs nothing to keep on screen. Only what is still moving or still warm is redrawn.
- Screen-space scheduling. Every (route × screen tile) is its own run with its own start time, so the growth is one front across the whole background rather than the same blob repeated in each tile. ~11k runs sort once at start; per-frame work tracks only what is moving.
Start times are spread by ink, not by run count: a third of the records are pad-only formations with no line to draw, and pacing by count gave the reveal a two-second opening where only vias sprinkled in, which read as a field of dots rather than anything growing.
Styles (REVEAL_PRESETS)
- Instant, off; the board is there.
- Etch, build order: pads, then the nets between them, then filler, then tap combs. The router's own passes, replayed everywhere at once.
- Flood, outward from the cursor (or centre), nudged by how many hops of routing separate each trace from its tile's seed, so the front follows copper.
- Sweep, a shallow diagonal scan across the screen.
- Bloom, rings expanding from the cursor.
- Scatter, no order; the whole board stipples in.
Config
reveal, revealDuration (whole board), revealSpeed (units/sec a tip
travels), revealHead, revealBlip, revealSpark, revealPalette,
revealTip, revealZone, revealSwell, revealBloom, revealCool (heat time
constant), revealSettle. All six styles and seven knobs are on the page;
Replay ⟳ regrows the board without re-routing it
(circuit.replayReveal(style)).
Signals wait for the reveal to finish before spawning, a pulse running down a
trace that is not there yet reads as a glitch. prefers-reduced-motion skips the
reveal entirely, and exporting mid-reveal snaps to the finished board first, so
downloads are never half-grown.
Verifying changes
Headless Chrome in this container never produces a screenshot, --screenshot
hangs even on a data: URL, with or without virtual time, so the offline check
node checks/frames.mjs: it runs the REAL engine against a recording canvas on a
synthetic rAF clock and reports which layers reach the visible context
and how much stroke goes with them. That is what caught the missing heat
composite; reading the draw code had not. node checks/timing.mjs does the same
for the schedule (route durations, queue ordering, release bursts) without any
DOM at all. COOL=150 node checks/frames.mjs exercises the generation rotation,
which never fires at the default 1200 ms cool within one reveal.
The pup window is usually occluded, so Chrome paint-throttles it to ~1 Hz and
screenshots lag by seconds, several rounds of "it still looks wrong" were stale
frames. To capture an exact instant, freeze the page (cancel its timers and
rAF) and take the screenshot after a few seconds, or have the page rasterize
itself: clone the SVG, serialize, draw it into a canvas, and read the data URL
back through browser_eval.
# Reveal build, the board growing itself in
A copy of the occupancy-grid engine that **grows** the board instead of showing
it finished. Nothing in the original files changed:
| | classic | reveal |
| --- | --- | --- |
| engine | `circuit-background.js` | `circuit-background-reveal.js` |
| page | `index-classic.html` | `index-reveal.html` |
| URL (port 8930) | `/` | `/reveal` |
## What happens
Copper grows out across the background as one front: each route draws itself out
of an end via, with a white-hot head riding the growing tip and pads landing as
the tip reaches them. When the last route lands, the hot layer cross-fades into
the real SVG artwork.
## The look: heat, not paint
A single bright colour everywhere reads as a neon sheet, not as something being
made. Two rules fix that:
- **Copper cools.** The tip is near-white, the colour sits just behind it, and
everything already laid fades exponentially (`revealCool`) toward a dim
settled trace. At any instant the screen shows a bright rim, a warm wake, and
a quiet interior, depth instead of one flat brightness. The default is **25
ms**, the bottom of the knob and the shortest the engine can honour (the
tail's tightening floors at 24), which leaves each route a wake about a
tip-length long rather than a board that stays warm. Even there the fade only
removes 48.7% per frame at 60fps, still short of the >50% a multiplicative
fade needs to reach zero on its own, so the generation buffers below are doing
real work at every setting.
- **Colour comes in regions.** Hue is sampled from a smooth screen-space noise
field (`revealZone` px) across the engine's own teal → blue → violet ramp, so
the board grows in broad zones of colour rather than confetti or one tone.
The settled copper keeps a trace of its zone's hue (`revealSettle`), so the
closing cross-fade into the uniform near-black artwork is a settling, not a cut.
## Timing, the same motion language as the live signals
The reveal and the idle signal animation are one system, so the reveal borrows
the signal system's timing rules rather than inventing its own:
- **Constant speed, linear.** A tip travels at `revealSpeed` units/sec, the same
units and the same default (420) as `signalSpeed`. It is never eased and never
scaled by a route's length, so every tip on screen moves at one pace and a long
bus takes longer than a short comb tap.
- **The same stroke stack**: a ~2.4-wide core over a 7-wide glow, like a signal's
head and halo.
- **The same arrival blip.** A pad grows into place and rings out, the ring runs
from the pad radius out to ~8 over `revealBlip`, matching the ring a signal
leaves when it lands on a via.
- **The same palette** (`revealPalette` is the signal palette verbatim).
Nothing appears or vanishes in one frame:
| moment | before | now |
| --- | --- | --- |
| board at the start | cut to hidden | fades out over 260 ms under the growth |
| a route's head | popped in, vanished on landing | fades in (130 ms), fades out after landing (260 ms) |
| a pad | stamped at full size | scales up and rings out before being stamped into the settled layer |
| the via hole | drilled when the pop ended | annular from the first frame |
| signals after the reveal | a dozen appeared mid-flight at once | the fountain fills from empty at its own cadence |
| resize mid-reveal | restarted the whole growth | settles, but only on a REAL size change; the observer also fires when nothing moved, which was ending the reveal ~200 ms in |
| the handoff to the static board | cross-faded, dipped and shifted hue | invisible: the board comes up under an opaque canvas, then only the glow fades |
**The layer that was missing.** The single loudest "traces pop in fully formed"
symptom had nothing to do with the schedule: the final composite drew only the
settled and tip layers, so the two heat generations were sized, cleared, faded,
rotated and stroked into every frame, and never reached the screen. A route's
copper lives ONLY in the heat buffer until it lands, so every route was invisible
while it grew and appeared whole the instant it was stamped into the settled
layer. All that was visible mid-growth was the tip spark, and there was no glow
at all. Measured by running the engine against a recording canvas: before, 2 of 4
layers were drawn into but never composited; after, all 4 composite and 3.5M px
of stroke per reveal reaches the screen through the heat buffer.
The release queue was also sorted by **metric**, not by delay, while the release
loop walks it with a single cursor. Jitter (±37 ms) is ~80x wider than the gap
between neighbouring start times (0.91 ms median), which left 84% of the queue
out of order and head-of-line blocked routes up to 71 ms behind the cursor before
letting them out in one burst, the opposite of what the jitter is for. It is now
sorted by delay.
A route starts growing when it is first **drawn**, not at the wall-clock moment
it was scheduled (`startedAt`, capped at `REVEAL_CATCHUP` ms of catch-up).
Deriving growth from the schedule means any slow frame, a GC pause, a busy tab,
a machine waking up, releases every route whose start time has passed *already
fully grown*: they pop into existence instead of animating. Measured before the
fix, with ~700 ms hitches in the frame stream: **941 of 2070 routes were born
complete**, the median route born 106 px long against a typical length of ~78 px.
After: **zero**, and never more than 16 px. Pads are timed off the same start, so
a late route does not sprinkle its pads ahead of its own tip, and the closing
tail waits for the growth to finish rather than for the clock.
Start times also carry ±35 ms of scatter (`REVEAL_JITTER`). Without it the first
wave of short routes all landed on the same frame and their pads popped together
, a visible batch. Measured frame-to-frame ink change went from 7.8% of the board
to 3.0%, and the only frames above 2.5% are now the mid-point of the closing
cross-fade, where a fade is supposed to be steep.
### The glow, and why it now reaches exactly zero
The glow accumulates in a buffer that is faded a little every frame, one fill,
whatever is warm. That is what keeps it O(1). But a multiplicative fade never
reaches zero: at 8 bits a pixel stops moving as soon as `alpha x decay` rounds
back to itself, so a multiply only lands on 0 if it removes more than half per
frame. Measured at a 150 ms heat fade (5.19%/frame): alpha ran
255 → 151 → 31 → **9, and stayed 9 forever**, 3.5% of `#00e6dc` sitting on the
copper permanently, turning rgb(21, 27, 37) into rgb(20, 34, 43). That residue is
why the growing board read lighter and bluer than the finished one. Canvas has no
subtractive alpha operation, so no choice of fade curve fixes it.
The fix is a multiply by **zero**, applied where it costs nothing: the buffer is
kept in two **generations**. New glow always goes into the current one. The other
keeps decaying but is also composited through a falling ramp, and once that ramp
reaches zero its buffer is cleared outright. Every pixel therefore ends at exactly
zero, the clear happens at a moment when it changes nothing on screen, and the
per-frame cost is still two fills and two composites regardless of how much of the
board is glowing.
(An earlier attempt derived the whole glow from the routes every frame, in
floating point. It was exact, but its cost scales with how much copper is warm,
at a long heat fade that is the whole board, and it showed.)
Verified live at a 150 ms fade, sampling every fully-opaque pixel of the growth
layer: the dominant colour is rgb(21, 27, 37) at t = 1.5 s, 3 s and 5 s, the
artwork's own copper, exactly. Frame pacing with it: **4.2 ms median, 9.2 ms p95**.
### Matching the static board exactly
Measured against the SVG board it hands over to (same seed, same density, ink
weighted by coverage):
| how the copper is laid | coverage | vs SVG | ink colour |
| --- | --- | --- | --- |
| SVG | 0.14129 |, | rgb(19.9, 25.8, 35.4) |
| one stroke per route, butt caps + miter joins | 0.14125 | −0.03% | rgb(19.9, 25.8, 35.4) |
| per-frame pieces (what it used to do) | 0.14261 | +0.93% | rgb(19.8, 25.7, 35.4) |
The colour was never the problem, it is identical either way. What differed was
weight: stroking a route in per-frame pieces blends each piece's antialiased edge
over the last one, leaving the canvas board ~1% denser than the SVG. So a route
in progress is now redrawn whole on its own layer each frame, and stamped into
the persistent layer exactly once when it lands, with the artwork's own caps and
joins. Cost of redrawing the in-flight routes: **0.28 ms/frame** (only ~4% of the
board is growing at any instant; stroking the entire board once costs 7 ms).
That layer sits UNDER the glow. Copper painted over its own glow reads as a dark
line with a coloured fringe instead of hot metal.
The tail is two phases, and that ordering matters. First the remaining glow is
cooled to nothing, the time constant tightens over `handoff`
(`clamp(revealCool x 1.6, 120, 1100)`) ms so the glow leaves *as glow*, at its
own rate. That floor was 260 ms, which at a short heat fade left the
finished board sitting untouched for ~150 ms with nothing left to cool; at the
25 ms default the whole reveal is now 4.3 s end to end instead of 5.3 s, with
the growth itself unchanged. Only then does the canvas dissolve
(`REVEAL_HANDOFF`, 260 ms), by which point it holds nothing but copper identical
to the SVG already at full opacity beneath it. Fading the canvas while it was
still glowing yanked the warm rim away instead of letting it cool, which reads as
a colour change, most obviously with **heat fade at minimum**, where the fade and
the decay ran at the same short duration. Measured at the dissolve: peak glow
above the board colour drops 187 → 16 (−97%) before anything fades.
The handoff deserves its own note. Cross-fading the growth layer out while the
SVG faded in looks obvious for two reasons: alpha compositing is not additive, so
two complementary alphas over identical copper dip to ~75% opacity halfway
through; and the settled copper carried a little of its zone's hue, so the whole
board changed colour at the same moment. Now the settled layer is stroked in the
artwork's exact colour (`revealSettle: 1`), the real SVG snaps to full opacity
underneath the still-opaque canvas the instant growth ends, and the canvas fades
off the top of it, so all that disappears is the glow. The growth
layers also match `devicePixelRatio` (capped at 2), or a soft board would hand
over to a crisp one.
**A trap worth remembering:** Chrome restores form-control values a beat after a
reload and fires `change` on each one. The tuning sliders' change handler
regenerates the board, so every reload silently kicked off two or three extra
reveals a few seconds in, which looked exactly like the animation randomly
restarting. `autocomplete="off"` on the form fixes it.
## Why it is a canvas, not an animated SVG
The board is ONE tile in `<defs>`, instanced ~25× by `<use>`. That instancing is
what makes the background cheap, and it is also why the obvious implementations
do not work:
- **Web Animations on the referenced content paint nothing.** Chrome runs them
on the original element, `getComputedStyle` shows the dash offset advancing
perfectly, but `<use>` instances mirror DOM and attribute state only, not the
animation's effect. The board looked frozen while the DOM said it was growing.
(Do not "verify" an animation by reading the DOM. Look at pixels.)
- **Writing attributes every frame does propagate, but costs ~260 ms/frame.**
Every mutation on referenced content resyncs all 25 instances.
So the reveal renders on a `<canvas>` above the SVG, and the SVG is hidden until
the handoff. Two layers: **settled** (persistent, each route stamped once, when
it lands) and **work** (cleared every frame, growing copper, its glow, the heads
and the pads). Measured: **8.4 ms median frame, 13 ms p95** on a 3000×2000
viewport.
What keeps it cheap:
- **Landed copper is free.** A route is stroked into the persistent layer exactly
once; after that it costs nothing to keep on screen. Only what is still moving
or still warm is redrawn.
- **Screen-space scheduling.** Every (route × screen tile) is its own run with
its own start time, so the growth is one front across the whole background
rather than the same blob repeated in each tile. ~11k runs sort once at start;
per-frame work tracks only what is moving.
Start times are spread by **ink**, not by run count: a third of the records are
pad-only formations with no line to draw, and pacing by count gave the reveal a
two-second opening where only vias sprinkled in, which read as a field of dots
rather than anything growing.
## Styles (`REVEAL_PRESETS`)
- **Instant**, off; the board is there.
- **Etch**, build order: pads, then the nets between them, then filler, then tap
combs. The router's own passes, replayed everywhere at once.
- **Flood**, outward from the cursor (or centre), nudged by how many hops of
routing separate each trace from its tile's seed, so the front follows copper.
- **Sweep**, a shallow diagonal scan across the screen.
- **Bloom**, rings expanding from the cursor.
- **Scatter**, no order; the whole board stipples in.
## Config
`reveal`, `revealDuration` (whole board), `revealSpeed` (units/sec a tip
travels), `revealHead`, `revealBlip`, `revealSpark`, `revealPalette`,
`revealTip`, `revealZone`, `revealSwell`, `revealBloom`, `revealCool` (heat time
constant), `revealSettle`. All six styles and seven knobs are on the page;
**Replay ⟳** regrows the board without re-routing it
(`circuit.replayReveal(style)`).
Signals wait for the reveal to finish before spawning, a pulse running down a
trace that is not there yet reads as a glitch. `prefers-reduced-motion` skips the
reveal entirely, and exporting mid-reveal snaps to the finished board first, so
downloads are never half-grown.
## Verifying changes
Headless Chrome in this container never produces a screenshot, `--screenshot`
hangs even on a `data:` URL, with or without virtual time, so the offline check
`node checks/frames.mjs`: it runs the REAL engine against a recording canvas on a
synthetic rAF clock and reports which layers reach the visible context
and how much stroke goes with them. That is what caught the missing heat
composite; reading the draw code had not. `node checks/timing.mjs` does the same
for the schedule (route durations, queue ordering, release bursts) without any
DOM at all. `COOL=150 node checks/frames.mjs` exercises the generation rotation,
which never fires at the default 1200 ms cool within one reveal.
The pup window is usually occluded, so Chrome paint-throttles it to ~1 Hz and
screenshots lag by seconds, several rounds of "it still looks wrong" were stale
frames. To capture an exact instant, freeze the page (cancel its timers and
rAF) and take the screenshot after a few seconds, or have the page rasterize
itself: clone the SVG, serialize, draw it into a canvas, and read the data URL
back through `browser_eval`.