# CIRCUIT — design document

**Replaces:** the `circuit` entry in `oracle-games-pack.js` (today: `tags: ['maze','chase','collect','pursuit','levels']`, icon 👾, "Drain every node. Four sentries would rather you did not." — a Pac-Man clone wearing an electronics name).

**Keeps:** the id `circuit` and the name **Circuit**. The owner's complaint was never that the name was wrong — it was that the game did not match the name. Renaming would dodge the note instead of answering it.

**New one-line pitch:** *Lay copper. Current takes every path you give it — you only choose the proportions.*

> **Status of this document.** Nothing here is argued from taste alone. Every number below was measured from a working prototype, and every claim that turned out to be false when measured is recorded as such. Prototype: `…\scratchpad\bld\` (`circuit.js` 1,303 lines, playable at `index.html`; `probe.js` / `scenes.js` / `shorttest.js` are the headless measurement harnesses; `FINAL-PROBE.txt` is the raw run). **No product file was touched by this run.**

---

## Verdict — which of the three, and why

Three complete designs were built and measured. All three are continuous simulations, all three
correctly refuse the logic-gate trap, and all three are honest about their own faults. They are
not equally right for this brief.

| | **A — Circuit** (this document) | **B — Circuit / hold-the-gate** | **C — Hot Copper** |
|---|---|---|---|
| Player's hand | **authors** the conductor | **vetoes** one of seven | authors, under a ramp |
| Verb | drag to route, linger to widen | press-and-hold a MOSFET | drag, stroke-*speed* sets gauge |
| Board | built by the player | fixed 7-column ladder | sparse peg lattice |
| First 30 s | draw a line, a light comes on | read 7 meters, deduce | draw a line, it overheats |
| Measured frame | **0.50 ms median / 0.80 p95** | 1.4–2.2 ms p95 | not stated |
| ×6 phone | **3.0 ms of 16.7** | ~13 ms of 16.7 ("inside, but not comfortably" — its own words) | unknown |
| Framework | none needed (verified below) | none needed | none needed |
| Ceiling | endless burn-in + par chase | author admits knobs exhaust ≈ L55 | endless ramp |

**A wins, on the owner's criteria in his order.**

1. **Genuinely about current flow.** All three simulate it; only A makes the player *make* the
   circuit. In B the player never manipulates anything electrical — they hold one of seven
   buttons and the board is scenery. That matters against the actual complaint, which was that
   the game did not look or behave like the thing it is named after. "You drew that copper and
   the current you gave it went everywhere at once" is an answer; "you held a button and seven
   needles moved" is a weaker one. C is the same family as A with less board identity — its
   render reads as *perfboard*, a dotted lattice, not a fabricated PCB.
2. **Fun at 30 seconds and at level 50.** This is where B fails hardest, and its own bench says
   so: the naive move — touch whatever is worst — measures **three times worse than doing
   nothing** (dies at 5.5 s vs 17 s). A game whose intuitive first action is actively punished
   has a comprehension cliff in front of the fun, on a phone, in an arcade. A's first thirty
   seconds are a drag and a light coming on, and it teaches current division wordlessly by
   letting the player watch a lit module dim when they add a second path.
3. **Phone.** B wins this one outright — press-and-hold needs no precision at all. A is
   acceptable (a 38 px routing pitch is a lane you draw *along*, not a target you must hit) but
   carries the real risk in the set: dwell-for-width, untested by a thumb. It is de-risked by a
   graft below rather than waved at. C is worst here: stroke-*speed* as a gauge means the game
   demands slowness at exactly the moment it has made the player panic.
4. **60 fps with this art.** A is the only one with a measured answer that is comfortably inside
   budget, and it is the only one that found a real performance bug by measuring
   (91.1 ms → 1.10 ms, §11).
5. **Different from the 34 shipped.** All three clear this. None of them is reachable by
   reskinning anything in the pack.

**Two decisive facts, verified against the real files rather than taken from any design's prose:**

- **A does *not* need a framework change.** Its own weakness list claimed it did, and the research
  brief agreed. Both are wrong. `oracle-games.js` lines 1841–1949: `WANT_PTR = !!spec.pointer ||
  TKIND === 'dragX'`; `pointerdown` calls `cv.setPointerCapture(pid)` and sets `g.pdown` and
  `g.tapN`; `pointermove` writes `g.ptrX/g.ptrY` in canvas space *before* the `pid === null`
  guard, so a captured drag keeps reporting after the thumb leaves the canvas; `pointerup` is
  also bound on `window` so a release off-element still ends the gesture. That is continuous
  free-form drag with capture, available today via `spec.pointer: true`. This deletes A's
  weakness #8 and, with it, the need to touch a shared file another workflow is editing.
- **A's prototype is the most complete and the most self-critical.** 1,303 lines, playable, ten
  measured scenes, and a probe that *contradicts its own designer three times* — copper does not
  run away (positive temperature coefficient is a ballast: two parallel routes converge and go
  flat, `FINAL-PROBE.txt` §G); a uniformly thin trace *cannot* burn (it chokes its own current
  and settles at heat 0.50, §E control); and modelling only diagonal crossings meant 0 of 7 vias
  were ever spent. A design that publishes the measurements that killed its own ideas is the one
  to trust.

**What B and C were right about, and what is now grafted in:** §13.

---

## 0. The strategic argument, in one paragraph

Not one of the 34 shipped games *simulates* anything. Every one is discrete-state — turns, tiles, toggles, collisions. `snake`, `blockescape`, `slide15`, `2048`, `lightsout`, `minesweeper`, `nonogram`, `sudoku`, `ricochet`, `pulse`, `simon`, `reaction`, `floppykai`, `wallrun`, `flipside`: all of them resolve to integers on a grid. A **continuous real-valued network where one local edit changes every value on the board** is a mechanic the pack does not have in any form, and it is not reachable by reskinning any of them. That is the reason to build this rather than the eleven other "circuit game" ideas, all of which collapse into something already shipped (logic gates → `lightsout` XOR parity; deduce-from-edge-clues → `nonogram`; tap-in-phase → `pulse`; aim-and-bank → `ricochet`).

---

## 1. Core loop

A **board** arrives unrouted: a 5 V input terminal with a **fuse**, a hatched **ground pour** along the foot, a scatter of **ground stitching vias**, and two to five **power modules** that each need a minimum current before their PGOOD indicator lights.

1. **Route.** Drag copper from a rail pad toward a module. Current begins flowing the instant the loop closes.
2. **Watch it divide.** Every new path re-divides the current *everywhere*, in proportion to conductance. Feeding one module well starves its neighbour. There is no "correct path"; there is a budget.
3. **Hold.** When every module is at or above demand simultaneously, a **BRING-UP** bar starts filling. Hold for ~3 s and the board passes.
4. **Except the copper is warming.** Resistance rises as it heats, the delivered current droops, and a board that worked at t=0 can drop a module out at t=20 s. The hold bar resets.
5. **Fix it and re-hold.** Widen, re-route, cut, add a parallel feed. One more.

The whole loop is 25–70 seconds. A run is a ladder of boards inside `GC.arcade`, with the harness's free lives / continue / level banner.

**Why it is tense:** three constraints that are *mutually coupled by physics*, so satisfying one worsens another.

| Squeeze | Failure | What it forces |
|---|---|---|
| Copper too thin | Module never reaches demand → timeout | Spend width |
| One segment too thin inside fat copper | That segment **burns** — permanent scar, route gone | Spend attention |
| Copper too fat everywhere | Total draw exceeds the fuse → **blown** | Spend width *only where needed* |

Measured, on a real board: two modules both fed at w=6 draw **10.86 A** against an **11.6 A** fuse and both light. The same two at w=12 draw **14.54 A** and blow it. Full width everywhere is not a strategy, it is the loss condition.

---

## 2. The player verb, and its gesture

**One verb: DRAW COPPER. Linger to make it thicker.**

- **Drag** from any live node (rail pad, ground stitch, ground pour, load pin, or your own existing copper). Octilinear only — eight neighbours, so every trace is orthogonal or 45°.
- **Width is dwell time in a grid cell.** Enter a cell and leave immediately → minimum copper (3 px). Rest your thumb there for 0.8 s → maximum (12 px). Nothing else changes width: no pinch, no toggle, no mode, no menu.
- **Hold on existing copper (0.40 s) to cut it.** A drill circle closes on the segment; release inside the ring and that segment is gone.

That is the entire input vocabulary: **press, linger, drag, release.** One finger, one-handed, and identical on a desktop mouse.

**Why dwell-for-width is the right gesture, not a gimmick:**

- It carries two variables (topology and cross-section) in one gesture with no mode switch — the phone-native problem the d-pad deletion was about.
- It has an honest cost curve: **width costs time, thinness costs risk.** Rushing gives you fragile copper *right now*; patience gives you robust copper while the clock runs and the fuse budget shrinks. That asymmetry is the whole difficulty knob.
- It is physically evocative — a plating head that moves slower deposits more — and it produces the correct engineering result by accident: copper naturally comes out **fat near the source and thin at the tips**, which is exactly how real power distribution is routed, because that is where the current is.
- It is self-teaching. You will dwell by accident within the first ten seconds and watch the copper swell under your thumb.

**Second stroke plates the copper (grafted, and it is the safety net under dwell).** Re-drawing
along a segment you already own adds **one width step** to it. Same gesture, same finger, no mode.
It is physically the right story — a second pass of the plating head deposits more copper — and it
is what makes width *deliberate* rather than *emergent*: minimum copper is always what you get by
default, and every step above it is paid for in a stroke you chose to make. It also gives the
player a repair verb that is not "cut it and start again", which is the only other thing they can
currently do to a route that is too thin.

The hook already exists in the prototype and is one line: `addSeg()` returns `'dup'` when the drag
re-enters a segment that is already there (`circuit.js:410`), and `Game.prototype.widen(s, w)`
(`:437`) already does the conductance and copper-budget bookkeeping. `'dup'` → `widen(s, s.w + step)`.
Segments already carry a `plated: 0` field for the render.

This is what makes the dwell risk survivable. **Ship both.** If dwell tests well, the two
compound — you can lay fat copper in one pass or build it up in two. If dwell tests badly, delete
it and retrace alone is a complete, unambiguous width system that never fires by accident. That is
a much better fallback than the corner toggle this document previously conceded to, because it is
still one continuous gesture and still costs *time*, which is the difficulty knob the whole design
is built on.

**Framework note — corrected.** An earlier draft of this document claimed the verb needed a new
`GC.arcade` touch kind. **It does not, and that was checked against the file, not remembered.**
`oracle-games.js:1845` sets `WANT_PTR` from `!!spec.pointer`; `:1879` calls `setPointerCapture` on
`pointerdown`; `:1888` maintains `g.tapN`; `:1890/:1897` write `g.ptrX/g.ptrY` in canvas space on
both down and move, with the move write placed *above* the `if (pid === null) return` guard; and
`:1949` binds `pointerup` on `window` so a release outside the canvas still ends the drag. So
`spec.pointer: true` plus the game's own handling in `step()` is the whole input layer, and **no
shared file needs to change.** Snap distance must still be converted from glass px to canvas units
the same way `SWIPE_MIN` is (24 px measured on the glass, `:1846`), or it changes meaning with board
size. Hit-testing for cut and for node snap is **nearest-target-within-N**, never rectangle
containment — B's prototype settled on 42 design units ≈ 39 CSS px at 375 wide, and that number is
worth starting from.

---

## 3. What a level looks like

Portrait, 540×1080 canvas (`fit` scales it), a 13×22 routing grid on a **38 px pitch** — 27.8 real px on a 375-wide phone, which is a routing lane you draw *along*, not a button you have to hit.

```
 ┌──────────────────────────────────────────┐
 │ J1  +5V IN                    F1  11.6A  │  terminal block + glass fuse
 │ ●━━━━━━━━━━━━●━━━━━━━━━━━━●   ▭▭▭▭▭▭▭▭▭  │  three rail pads
 │                                          │
 │      ⊕G                    ⊕G            │  ground stitching vias
 │            ┌────┐                        │
 │            │ U1 │  ← module, 62×46,      │
 │            └────┘    demand bar in silk  │
 │   ⊕G                          ⊕G         │
 │            ┌────┐                        │
 │            │ U2 │                        │
 │            └────┘                        │
 │▨▨▨▨▨▨▨▨ GROUND POUR ▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨▨│  hatched, dull, the return
 │ TP1   TP2   TP3   TP4   J3 ●●●●●●●●      │
 └──────────────────────────────────────────┘
   BRING-UP ▮▮▮▮▮▮▮▯▯▯▯     FUSE F1 ▮▮▮▮▮▮▯▯▯
   LV 4      VIAS 5/7       CU 73      T 89
```

**The HUD is silkscreen**, rendered by a `drawHud` function on the dark bezel *outside* the board rectangle — so the rule "nothing on the board is bright unless it is current or heat" survives contact with the UI. No amps are ever printed. The fuse is a gauge; demand is a row of ticks; the hold is a row of ticks. There is not one number on screen that the player is expected to compute.

### The five-second read
A green fibreglass board with brown copper traces, cyan sparks streaming along them from a fused input terminal into little black modules, moving faster where the copper is narrow. Nobody will ask what it is about.

---

## 4. How it teaches itself, with no words at all

No tutorial. No Ohm's law. Four boards, each of which can only teach one thing:

- **Level 1 — geometry teaches the verb.** One module, placed *directly beneath the centre rail pad*. The only thing a thumb can do is pull a straight line down. The module lights. That is the tutorial.
- **Level 2 — the same board, but demand is raised so a rushed route fails.** Measured: at w=3 the module gets 4.33 A cold, droops to **3.34 A** as the copper warms, and never reaches its 4.10 A demand. At w=7 it gets 6.94 A cold, settles at **5.96 A**, and holds. The player rushes, watches it die, redraws it slower, and has learned dwell-for-width without reading anything.
- **Level 3 — two modules teaches current division.** Feed one fat and one thin and *watch the thin one go dark*. Measured: w=[12,3] splits **74.3 % / 25.7 %** and starves the second; w=[6,6] splits **57.5 % / 42.5 %** and lights both. The lesson — *helping one starves the other* — is delivered by the board, not by a sentence.
- **Level 5 — the fuse teaches the budget.** The board is winnable, and the naive answer (maximum copper everywhere) blows the fuse in front of you. The glass fogs and the element parts.

The one concession to words is **GCHOW**, which sits behind a button and is never in the way (§9).

---

## 5. The physics — laws exact, constants calibrated, and stated

All of this is implemented and measured in the prototype.

| Quantity | Law | Note |
|---|---|---|
| Network | Nodal analysis, warm-started Jacobi-preconditioned CG | 5 iterations / **0.006 ms** warm on a 41-node board |
| Trace resistance | `R = K·L/w` | correct form; `K` is a game constant, see below |
| Ampacity | `I_max = 4.0·(w/3)^0.725` A | IPC-2221's exponent, exactly |
| Inner layer | ampacity **×0.5**, resistance **×1.6** | IPC's internal `k` really is half the external `k` |
| Copper heating | `dh/dt = 0.396·(I/I_max)² − 0.55·h`, burn at `h ≥ 1` | τ = 1.8 s |
| Temperature coefficient | `R(h) = R₂₀·(1 + 0.90·h)` | ≈ 0.00393/K over a ~230 K rise |
| Fuse | `df/dt = 0.42·(I/I_fuse)² − 0.75·f`, blow at `f ≥ 1` | a real I²t curve, not a trip level |

**The one honest fudge, named.** Real copper is too good a conductor for routing to matter at signal currents. The prototype's copper is roughly 100× more resistive than 1 oz copper at this scale. This is not a physics error, it is a **choice of regime**: the game is set in **power distribution**, the one domain where trace resistance and ampacity genuinely dominate design, and the constant is tuned so a board-sized route has a board-sized effect. Every *law* is exact; one *constant* is a game knob, and it is written down rather than hidden.

*Measured, before and after that calibration was corrected:* at the first value (`K=0.0030`, 0.62 Ω loads) the width sweep spanned 5.06 A → 6.57 A and **every** width satisfied the load. The trace was decoration. At the shipped value (`K=0.0075`, 0.34 Ω loads) the same sweep spans 4.33 A → 8.55 A and w=3 starves. That correction only happened because the sweep was run, not reasoned about.

### Three findings that contradicted the plan

**1. Parallel copper self-balances. There is no thermal runaway, and the design must not claim one.**
The intuitive escalation — a hot trace gets more resistive, pushes current onto its neighbour, which heats, cascade — is **wrong for copper in parallel**. A positive temperature coefficient is a *ballast*: the hotter branch sheds current. Measured over 25 s on two parallel routes (w=6 and w=4): currents 5.01/2.84 A → 4.50/2.74 A and then **flat**, heats settled at 0.28/0.18. The real escalation is the **IR-drop droop** — warming copper raises total resistance, delivered current falls, and modules drop out of spec — plus demand that ramps. That is what makes a board fail at t=20 s, and it is the honest version.

**2. A uniformly thin trace cannot burn. The *weakest link* burns.**
Same reason: a thin trace chokes its own current and settles. Measured control — a whole route at w=3 settles at 3.34 A / heat 0.50 and never fails. But **one rushed node inside a w=9 route** carries the full 7.03 A of the fat copper either side of it, and its own resistance is too small a share of the loop to save it: heat 0.02 → 1.00 and it **lets go at t=1.48 s**. This is a far better mechanic than the one originally planned. *Your route is only as strong as its narrowest segment*, so the punishment for rushing is specific, spatial, and teaches you to look back over your own copper.

**3. Two thin traces beat one fat one, for free, and it costs board space.**
Because the ampacity exponent is 0.725 < 1: one w=12 trace carries **10.93 A**; two w=6 traces at *identical total conductance* carry **13.22 A** — **21 % more**, at twice the area. This is why real power boards run parallel copper, and it is a genuine strategy the player can discover with no tutorial and no unlock. It emerges from the exponent.

### The short circuit
The pour is 18 rows from the rail, so a stray trace to the pour is not a short — it is a resistor (measured 9.1 A into a 10.5 A fuse; nothing happens). **Ground stitching vias** fix this and two other things at once. Measured, on a working two-module board:

| t | I_total | fuse heat | modules lit |
|---|---|---|---|
| — | 10.87 A | 0.36 | 2 |
| 0.00 s | **24.4 A** | 0.38 | **1** |
| 0.30 s | 21.2 A | 0.73 | 1 |
| 0.50 s | 20.2 A | **0.87** | 1 |
| 0.55 s | 6.3 A | 0.82 | 1 — *the shorting copper vaporised* |

Every branch dims **at once**, which is the single most legible proof that the rule is proportional. The player has **~0.55 s** to cut it. The fuse peaks at 0.87 of 1.0 — a near miss — and then the shorting copper, being the weakest thing in the loop, destroys itself and saves the fuse. You lose that route permanently. That window is a skill expression, not a death.

---

## 6. Board space, crossings, and vias

Two segments that share a grid node share copper. So **a route cannot pass through a node another net already owns** without merging with it — which is sometimes what you want (a second parallel feed) and sometimes a short. This is the "limited board space" pressure, and it is stronger than a crossing rule: the board fills up as nets claim nodes.

When a step would collide with foreign copper, the drag **automatically dives to the back layer** — via barrel, dark green trace under the mask, via barrel — for **2 from the via budget**. No new gesture; the decision is *whether to route through there*, which is the interesting one. Going under costs three ways: the budget, **+60 % resistance**, and **half the ampacity** (IPC's internal constant). With the budget spent, the step is simply refused.

*This was measured wrong first.* Modelling only diagonal-vs-diagonal crossings meant **0 of 7 vias** were used on a level-16 board — a whole mechanic that never fired. With node occupancy included, the same board spends **6 of 7** and renders three back-layer runs. See `scene-8-late-board.png`.

---

## 7. Difficulty, level 1 → 50 → forever

Each band adds **one pressure**, not one mechanic. The mechanic count stays at five: route, dwell, cut, dive, hold.

| Levels | New pressure |
|---|---|
| 1–4 | One module, then two. Current division, taught silently. |
| 5–8 | The fuse budget starts to bite. Third module. |
| 9–12 | **Floating modules** — both legs must be routed. The return path stops being free. |
| 13–16 | Node collisions force **vias**; the budget is finite. |
| 17–20 | **Heat droop** turns on properly: boards that pass cold fail warm. Build with margin. |
| 21–24 | **Keep-outs** and pre-placed parts shrink the board. Space becomes the scarce resource. |
| 25–28 | **Gates** *(grafted, §13.2 — replaces the old "switched loads" band)*. A MOSFET sits in some branches; **press and hold one to choke it and every other branch strengthens**. Close τ 90 ms, open τ 260 ms, so a hold is a commitment. Each module runs on a **hold-up capacitor** for a few seconds when starved, which is both the leash on the verb and the game's real introduction to stored charge. This is what the player does *during* the hold instead of watching. |
| 29–32 | **Diodes.** One-way, and they eat voltage headroom. A specific loop becomes the only legal loop, which recontextualises every earlier board. |
| 33–36 | **Capacitor bank.** Deliver a burst the supply cannot: charge slowly through high resistance, cut, dump. Build → wait → release. |
| 37–40 | **Shared return** — two circuits interfering through common return impedance. Ground bounce as spooky action at a distance. |
| 41–44 | **Inductive kick.** Cutting a live inductive branch spikes; you must route a flyback diode *first*. |
| 45–50 | **Field repair.** Boards arrive pre-routed, badly. The *cut* verb becomes primary. Fuse ratings shrink while demand climbs. |
| 50+ | **BURN-IN.** One board, demand ramps forever, heat never resets, scars accumulate. Score = seconds survived. No new content; your own board becomes the level. |

Per-board **par** on copper used and vias spent gives a 3-star rating and a reason to replay a board you already beat.

---

## 8. Failure, scoring, save

**Three failure states, all legible, none arbitrary:**

1. **FUSE BLOWN** — total draw exceeded budget. The element parts inside the glass and the glass fogs. Costs a life.
2. **TIMED OUT** — the bring-up window closed with a module below spec. The board de-energises. Costs a life.
3. **BURNED** — a segment exceeded ampacity. *Not* a loss: it destroys that segment and scars the board permanently. It only kills you if you cannot re-route in the time left.

Lives, the continue panel, the level banner and the result card all come free from `GC.arcade` (`lives: 3`, `levelBase: 1e9` + explicit `g.levelUp()` per board cleared, exactly as the current `circuit` does).

**Score.** Laddered mode = 1000 base + time bonus + fuse headroom − copper used − 40/via, reported via `GC.report`; copper and vias are also the **par** the three-star chase is measured against. Burn-in mode is scored in **coulombs delivered** *(grafted, §13.8)* — charge that actually reached a module while it was inside its window. It is the honest unit, it climbs smoothly, and it is the only number in the game the player is ever shown that is a real physical quantity.

**Save/resume** in `GC.stat`: level, per-board star ratings, and the burn-in high score. A board in progress is a small object (segments = `{a,b,w,layer,heat}`) so mid-board resume is genuinely cheap — unlike most of the pack.

---

## 9. Ship-complete checklist

- **GCHOW guide** — five panels, all diagrams, almost no prose: *drag to lay*, *linger **or draw over it again** to thicken*, *hold to cut*, *both bars full = pass*, *a gate you hold starves its branch and feeds the rest*. Plus one diagram that has to exist: **the same current entering a junction and leaving in two unequal streams**, because that is the one thing a player may never quite believe. The cut panel says it the way C's design argued it: **the iron adds where it moves and lifts where it lingers**.
- **GCFX sound palette.** The whole board is a power supply, so the palette is *electrical*, not chiptune:
  - `music` — a low 33 Hz-rooted drone bed with a slow filter sweep, tuned so its pitch **tracks total current**: the board hums harder as you load it. The music is a meter.
  - `coin` — a short plating tick per grid cell laid; pitch rises with the dwell width, so you *hear* the copper thicken.
  - `merge` — the PGOOD chirp when a module comes up.
  - `spawn` — the bring-up hold completing: a rising three-step.
  - `explode` — the burn: a filtered noise crack with a fast decay, not a boom.
  - `bad` — the fuse: a hard DC thump followed by silence in the drone for 400 ms. Losing the hum is the loudest thing in the game.
- **Achievements (GCPROG):** *First Light* (board 1) · *Derating* (pass a board with every segment under 70 % of ampacity) · *Ballast* (feed one module with two parallel traces) · *Scar Tissue* (pass a board after a burn) · *Under the Board* (pass a board using every via) · *Sixty Seconds* (60 s of burn-in) · *No Slack* (pass with the fuse over 95 %).
- **Reduced motion:** carriers drop to a slow dashed march; no glow pulses. The state is still fully readable because rate and speed are encoded in geometry, not brightness.
- **Colour-blindness:** the three signal hues are cyan / amber / green, which is the worst possible triple for deuteranopia. Mitigation is already structural — charge is *moving dots*, heat is a *bright wide trace with a tan halo*, PGOOD is a *dot inside a module outline*. They differ in form, position and motion, not only hue. A high-contrast option swaps green PGOOD for white. **This has been reasoned, not simulated** — run the frames through a deuteranope simulation before shipping, because the design has been measuring luminance and the risk here is hue.
- **The policy bench (§13.1) is a ship gate, not a nice-to-have.** No level past 8 gets tuned until `none` is proven to fail it and `lookahead` is proven to clear it. Physics measurements do not tell you whether a level is a game.
- **Build order, so the riskiest thing is answered first:** (1) the width verb — dwell *and* retrace, in front of the owner's thumb, before anything else is polished; (2) the policy bench; (3) levels 1–12 tuned against it; (4) gates and hold-up caps; (5) the back half.

---

## 10. Art direction

### 10.1 The palette, and why each colour

Real boards have a material identity. These are fabrication finishes, not accent colours.

| Colour | Hex | Measured L\* | Why |
|---|---|---|---|
| FR-4 substrate | `#8E7B4E` | (under mask) | Woven glass in epoxy. Baked once as a 128 px **over/under weave** at 8 px pitch — warp bundles with a specular centre, weft in a checker so it genuinely interlaces. Never a noise field. |
| Solder mask over substrate | `#0C3626` | **27.0** | A translucent semi-gloss film: the weave must survive *underneath* it (drawn at α 0.90, not opaque), and it takes one broad specular lobe, not a bloom. |
| Mask over copper | `#12452F` / pour `#164E36` | **26.9** (pour, same light) | Copper reflects back up through the film, so buried copper reads **lighter and warmer**. Almost nobody models this; it is what makes the pour and the inner layer look observed rather than invented. |
| Unenergised copper | `#5A3618` | **36.0** | Deliberately **oxidised**. Not taste — see §10.3. |
| Oxide flank | `#2E1B0B` | — | An etched edge is darker *and redder* than the face. |
| ENIG plating | `#C8A951` → `#F0DFA0` | **63.0** | Pale, slightly green-gold. Not yellow. |
| HASL tin | `#AEB4BE` | — | **Cold** against the warm copper. Two metals on one board is what stops everything metallic reading as "the same shiny thing". |
| Silkscreen | `#E4E1D6` | **78.8** | Never `#FFFFFF`. Matte, and *bled*. |
| Charge | `#39D7FF` core `#B8F4FF` | **91** | Saturated cyan, small core. |
| Heat | `#E0913A` → `#FFF3D2` | **73.6** | Amber → straw. Never red — see §10.4. |
| Blister | `#C4A86E` | **37.4** | Delaminated mask showing tan FR-4. |
| PGOOD | `#7CFFB0` | **91.2** | Green, because a PGOOD LED is green on every real board — and because it must not be the same colour as charge. |

### 10.2 One light, stated and obeyed
Key at **upper-left, azimuth 135°, elevation ~35°**, `L = normalize(−0.577, −0.577, 0.577)`. Everything derives from it:
- contact shadows offset **(+1.1, +1.1)** at `rgba(0,0,0,0.50)`, one width wider than the object;
- specular offset **(−0.45, −0.45)**;
- component bodies get a `rgba(255,240,215,0.18)` bevel on the **top edge only**;
- via barrels are lit on the side **away** from the key and black on the near side — that asymmetry is what makes a hole read as a hole rather than a dark circle;
- the mask gloss is **one** radial gradient centred at 16 %, 8 %, falling to `rgba(0,0,0,0.30)` at the far corner.

### 10.3 Material, in passes, not fills
A copper trace is **four passes plus grain**, and removing pass 1 or 3 is exactly what turns it back into an orange rectangle:

1. **Contact shadow**, width+1.4, offset along the light. Copper stands 35 µm proud of the board and it *occludes*.
2. **Oxide flank**, width+1.3, `#2E1B0B`.
3. **Face — a gradient ACROSS the trace, not along it.** Every segment is a straight line, so the correct anisotropy is a perpendicular linear gradient: lit flank → face → dark flank. *This is the second version.* The first build put the specular as a centred stripe at α 0.42; it measured **L\* 62.7 — a grey blade** — and made hot copper vs cold copper **1.04:1, invisible**. Rolled foil grains along its length; the highlight is a flank, never a blob.
4. **Grain**, the baked noise tile at α 0.26. At 0.5 it lifted the whole trace out of budget.

Occlusion is doing real work here: unenergised copper sits at only **1.37:1** against the mask and is still perfectly findable, because it casts a shadow. **Occlusion substitutes for luminance contrast** — that is what lets dead traces stay quiet without becoming invisible.

### 10.4 Motion that means something
- **Carriers are discrete and countable.** They are spawned at the rail at a rate proportional to total current, and at every junction routed into a branch with probability proportional to that branch's share. A 74/26 split is *literally countable* at the junction.
- **Speed is current density, not current.** `v ∝ J = I/w`, so the *same* current in *narrower* copper visibly moves **faster**. Two independent channels encoding two different quantities.
- **Magnitude is spacing, never brightness.** Above roughly one carrier per 6 px the dots merge into a continuous glowing line — which is precisely the "glowing line that pulses" failure mode this design exists to reject, arrived at from the other direction. Rate is tuned to ~1 per 9 px and hard-capped at 420 live carriers. *The first build was measured at 92 carriers on two traces and read as a bead chain; it is now 2.6/A.*
- **The plating meniscus.** While you drag, a molten bead sits at the tip and **swells as you dwell**. The width gauge *is* the material — there is no slider. See `scene-2-mid-drag.png`: the trace visibly thickens toward the tip.
- **Heat climbs monotonically in luminance.** The intuitive metal ramp (copper → dull red → orange → white) is **non-monotonic**: dull red is *darker* than copper, so a heating trace would go dark first and read as switched off. The ramp here never passes through red. Measured hot-vs-cold: **3.70:1, ΔL\* 38.0.**
- **A burn is readable by its BRIGHT halo.** Overheated mask blisters and delaminates, revealing tan FR-4. A dark char core on a dark mask is invisible (the prior research measured 1.02:1); the blister here reads **1.45:1** and carries real chroma, tan against green, which is a hue contrast as well as a luminance one.

### 10.5 Restraint — what is deliberately dull
Everything that is *board* lives between **L\* 19 and 37**: component bodies 19.2, ground pour 25.9, mask field 27.0, FR-4 rim 27.8, inner-layer ghosting ~28, unenergised copper 36.0. Nothing on that list ever changes.

**The rule, measured rather than asserted:** the pixels above L\* 70 on the board are `carrier`, `copper_hot`, `led_good` and `silk_text`, and together they cover **0.44 % of the board area**. Bright is a *signal*, not a surface. Silkscreen is the one static exception — it is the brightest non-emissive thing on a real board — and it was pulled from α 0.92 to **0.78** when it measured L\* 85.7 / 7.2:1, thirty characters of static ink shouting louder than a lit rail.

**Charge and heat are separated by HUE, not brightness**, because they are both bright and they mean opposite things. Measured separation: **173°** (cyan 213° vs amber 40°). PGOOD sits at 144°, a third meaning with a third hue. And carrier cores are drawn `source-over` while only their halo is `lighter`, because `lighter` over a warm base saturates toward white and arrives as an orange-white blob with no hue left.

### 10.6 Detail that rewards a second look
Tented vias (soft green domes, no hole — deliberately *quiet*) mixed 2:1 with open plated barrels, both on **half-pitch**, because scattered detail reads as dirt. Unpopulated 0805 land patterns, most of them with **no silk outline** — a box round every one read as confetti. Ground stitching vias with four-spoke thermal reliefs and a silk `G`. Thermal relief where a module leg meets the pour. Thieving copper in the pour. Fiducials. Test points TP1–TP4 with silk labels. A J3 through-hole connector footprint with a square pin-1 pad. Inner-layer copper ghosting with 45° doglegs, α 0.38 — the single largest "this is a real two-layer board" upgrade for the least work. Board legend `KAI-7 PWR DIST REV C` and the fuse rating `F1 11.6A`, so the board tells you its own budget before you route anything.

**45° mitring is the strongest "this is a PCB" tell and it is nearly free** — one pass over the point list, radius 7 px, `miterLimit 4`. Orthogonal-only reads as a wiring *diagram*; mitred reads as a routed *board*.

**No marking below ~5 px.** Illegible text reads as dirt and actively cheapens a render. Modules get their designator in silkscreen *outside* the outline, the way a fab places it, and a bare thermal slug instead of a fake part number.

**Silk outlines are corner ticks, not boxes** — four brackets say the same thing at a quarter of the ink.

### 10.7 Readability outranks beauty
Design targets, all met in the shipped prototype:

| What it decides | Target | Measured |
|---|---|---|
| Charge findable on the copper it rides | ≥ 4.5:1 | **5.49:1** (ΔL\* 51.8) |
| A heating trace is brighter, not darker | ≥ 3:1, ΔL\* > 0 | **3.70:1** (ΔL\* +38.0) |
| A burn is visible on a dark mask | ≥ 1.3:1 | **1.45:1** (ΔL\* +10.1) |
| Dead copper findable but quiet | 1.2–1.6:1 + occlusion | **1.37:1** |
| Silkscreen legible, not shouting | 4–6.5:1 | **5.91:1** |
| PGOOD unmistakable | ≥ 7:1 | **7.32:1** |
| Charge vs heat, hue | ≥ 120° | **173°** |

---

## 11. Feasibility — measured, not assumed

Desktop headless Chrome, 540×1080 buffer at 375 CSS px / dpr 2, level 10, four modules, 36 segments, 41 nodes:

- **Solve:** 5 CG iterations, **0.006 ms** warm-started. Cold solve happens once per topology change.
- **Frame (step + full draw):** median **0.50 ms**, p95 0.80 ms, p99 1.10 ms, max 1.10 ms.
- **× 6 phone penalty → 3.0 ms** against a 16.7 ms budget.

**One real performance bug was found by measuring, not by reading.** The static bake flag was being set by the network rebuild, so *every single segment the player laid* re-rendered the whole board — weave, ghosting, hatch, two blurred silkscreen passes. Max frame was **91.1 ms**. Separating "the board changed" from "the copper changed" took it to 1.10 ms. Bake procedural texture once at load; never inside the frame loop; and `ctx.filter='blur()'` belongs only on static content.

---

## 12. Weaknesses — honestly

1. **Dwell-for-width is still the single biggest risk in the design, and no thumb has touched it.** It could easily feel *laggy* ("why won't my trace get fat") or *accidental* ("I stopped to think and got copper I didn't want"). **Retrace-to-plate (§2) is now the answer to that**, and it downgrades this from "the design may not work" to "one of two width verbs may not survive" — but it does not remove the risk, it insures it. **This is still the first thing that goes in front of the owner**, and the question to ask him is narrow: *did the trace ever get fatter than you meant it to?*
2. **A partially-routed net silently does nothing.** Measured on a level-16 board: routes that ran out of vias mid-drag left dead copper that *looks* finished and carries 0 A. Unconnected copper must be drawn differently — unplated, no carriers, and the module's demand bar flat at zero — or players will stare at a route that looks right and is not. This is specified but not yet built.
3. **The physics is calibrated, not real.** Copper is ~100× too resistive. It is stated openly and it is the right call, but an electrical engineer will notice within a minute, and the game will be quietly wrong about a real quantity in a genre that attracts people who care.
4. **The mid-board period may be dead time.** Between "I have routed everything" and "the hold bar fills" there can be several seconds where the correct play is *to do nothing and watch*. That is thematically perfect (it is a bench burn-in test) and it may be boring. Rising demand and switched loads exist partly to paper over this, and they arrive at level 25.
5. **Levels 29–44 are asserted, not prototyped.** Diodes, capacitor banks, ground bounce and inductive kick are described here from first principles. Only routing, division, heating, burning, fusing, vias and the hold have actually been built and measured. The back half of the curve is a plan, and plans of this kind are usually 30 % wrong.
6. **The scar reads as a component.** A pale tan capsule on a green board is legible as *damage* in motion, but in a still frame it can be mistaken for a part. It wants to be more irregular and less symmetric than a stroked line.
7. **Deuteranopia gets the worst possible hue triple.** Structural mitigations are real (motion, form, position), but a colour-blind player will have a meaningfully harder time than a sighted one, and no amount of shape language fully fixes cyan-vs-green.
8. ~~**It needs a framework change.**~~ **Withdrawn — this was false, and it is worth recording why.** Both this document and the research brief asserted that no `GC.arcade` touch kind gives continuous free-form drag with capture, and therefore that shared code had to change. Reading `oracle-games.js:1841–1949` shows `spec.pointer: true` already provides exactly that, including capture and an off-canvas release. Two of the three competing designs checked the file and got it right; this one reasoned from the research summary and got it wrong. **The lesson generalises past this game: a claim about the framework that was never read out of the framework is a guess.**
9. **It is the slowest game in the pack.** A board takes 25–70 seconds. Every other game in the arcade is a 30-second reflex loop. This may simply be a different appetite than the one the Game Center currently serves, and the burn-in mode is the only part of it that behaves like an arcade game.
10. **Procedural boards may produce dull ones.** The generator here is a placeholder — column table plus a stitch list. A board grammar that reliably forces interesting collisions (rather than five parallel straight lines) is real design work that has not been done, and without it level 20 looks like level 5 with more modules.
11. **The difficulty curve has never been *played*, only reasoned about — and this is the largest methodological hole in the design.** Every number in `FINAL-PROBE.txt` is a physics measurement. Not one of them answers *is level 16 winnable*, *does doing nothing win*, or *is the correct move ever the obvious one*. The competing design measured exactly that and it changed its curve; this one has not. §13.1 is the fix and it should be built before any level past 8 is tuned.
12. **The grafted suppression verb collides with cut.** Both are press-and-hold. They must be disambiguated by **what is under the finger** (copper → cut; a gate body → suppress), never by duration — two different hold-lengths on one surface is precisely the mode-switch this design has otherwise refused. If that disambiguation feels ambiguous in the hand, suppression is the thing that goes, not cut.
13. **The bottom third of the board is empty and empty mask reads as unfinished, not as restraint.** Visible in every scene frame: below the last module there is silkscreen, four test points and a connector, and then nothing. Restraint means *dull*, not *absent*. That region needs inner-layer ghosting, thieving copper, a second footprint and legend — all of it below L\* 30 so it fills the frame without competing for attention.

---

## 13. Grafted from the two runners-up

Neither losing design was wrong; they were right about different things. Everything below is
adopted with attribution, and the two items that contradict this document's earlier claims are
marked as corrections rather than quietly patched.

### 13.1 The policy bench — the single most valuable thing either runner-up built  *(from B)*

B's `circuit-bench.js` does not measure physics. It measures **whether the game is a game**, by
running scripted players against every level and asking four questions:

1. **Is doing nothing fatal?** (If not, the level is dead time wearing a difficulty label.)
2. **Can a skilled policy clear it?** (If not, it is not tuned, it is broken.)
3. **Is the correct action ever non-local** — on a branch other than the one visibly in trouble?
4. **What does the sim cost per frame under real play**, not on an idle board?

Crucially it runs every policy with **human latency baked in**: re-decide every 200 ms, and the
decision lands 150 ms late. The numbers are therefore not a superhuman bot's.

This is the missing half of this design's evidence. Build the equivalent for routing, with three
policies — `none` (never touches the board), `greedy` (always widens the starved module), and
`lookahead` (widens whichever edit most raises the *minimum* margin across all modules). Then:

- **`none` must fail every level from 3 onward.** If it survives, that board has dead time in it,
  and weakness #4 is real and measurable rather than a worry.
- **`lookahead` must clear to ~50 and start failing beyond it.** If it clears 80, the curve is flat.
- **`greedy` must be measurably worse than `lookahead` from about level 8**, because that is the
  level where "help the one that is starving" stops being the right answer and "stop feeding the
  one that is fat" starts being it. B measured its equivalent crossover at level 12 and found the
  correct target was on a *different* branch than the one in trouble **100 % of the time** past it.
  If routing has no such crossover, the game is a search for one best path — i.e. a maze — and the
  whole strategic argument in §0 collapses.

B also recorded that **its first scoring function was wrong and produced a fake curve**: a policy
protecting the *average* margin played no better than doing nothing, and only protecting the
*minimum* produced a real skill gradient. Take that as given rather than rediscovering it.

### 13.2 The gate — hold to starve, and it fixes the dead time  *(from B)*

B's entire game is one verb this design does not have: **press and hold a MOSFET to choke its
branch, and every other branch gets stronger.** As a whole game it has a comprehension cliff; as a
**level-25 unlock inside this one it is close to perfect**, because:

- It is the same finger and the same physics. Closing a channel raises that branch's resistance;
  division does the rest. **No solver change** — it is one conductance being driven by input.
- It gives the player something to *do* during the hold — which is weakness #4, answered with a
  verb instead of with a rising number. This is a better fix than "switched loads", because a load
  that switches itself is content happening *to* the player; a gate is content the player operates.
- It is legible from the art already specified: a real gate necks shut. B's timings —
  **close τ = 90 ms, open τ = 260 ms** — are worth taking verbatim, because the asymmetry means you
  *commit* to a hold, and it makes a 180 ms tap already an 86 % pinch without needing a
  tap/hold distinction at all.
- Sliding the thumb hands over to the next gate, so one continuous drag chokes the bus in turn.

**Its leash is the hold-up capacitor, also from B:** a starved module runs on stored charge for a
few seconds before its PGOOD drops (B measured ~4.9 s at total outage). That is what stops the
player parking on one gate forever, and it is a far better introduction to capacitance than this
document's level-33 "charge a bank and dump it" — accumulation and depletion become something the
player *causes with their own thumb* rather than a thing they wait for.

Replaces the current level 25–28 band. The capacitor-bank band at 33–36 stays, and now lands on a
player who already understands stored charge.

### 13.3 Prove the game dies without the physics  *(from B)*

B's strongest single argument is a one-line bench: sweep the source stiffness. At 99.5 % ideal, a
gate closing gives its siblings **+0.17 %** — no game exists. At 66 %, **+9.88 %**. Set the source
impedance to zero and there is literally nothing to play.

Run the routing equivalent and put the number in this document: **hold the fuse and the loads
constant, drive rail impedance to zero, and confirm that widening one branch stops affecting the
others.** If it still affects them, the coupling is coming from something other than the physics
and needs finding. This is the cheapest possible answer to "could this be reskinned?", and it is an
answer with a number in it.

### 13.4 No decision may depend on a string  *(from B)*

B measured its silkscreen at **2.20:1** against the mask and, rather than brightening it, ruled that
no decision may depend on reading one. This document's silk measures better (5.91:1, dL\* 51.8,
`FINAL-PROBE.txt` §J) but **the rule is adopted anyway**: reference designators, the revision string
and the fuse rating are second-look detail. If the player ever has to *read* `U3` to know which
module is starving, the state is being carried by text and the render has failed. Modules are told
apart by position, by the colour of their PGOOD dot, and by the copper going into them.

### 13.5 The burn-in clock is a ramping supply, not a rising demand  *(from C)*

This document's endless mode says "demand ramps forever". C's formulation is better and should
replace it: **the supply itself ramps, `I(t) = I₀ · (1 + t/T)`, T falling from 50 s to 20 s across
the run.** One number, physically honest, and every other pressure falls out of it rather than
being added to it — more current means more I²R in every trace, which means ampacity crossings and
burns, which means the fuse budget tightens, all without a single new rule. It also means the
endless mode needs no new content at all, which is what makes it shippable in the first version.

### 13.6 The thermal knife-edge is a readability contract  *(from C)*

C measured the burn timing as a table rather than a threshold: **1.2× ampacity runs hot forever and
never fails; 1.5× burns at 6.6 s; 2× at 1.9 s; 3× at 0.7 s.** That shape is what makes "hot" a
*warning the player learns to read* instead of a countdown they cannot see. Re-measure it against
this design's constants (`HEAT_A = 0.396`, `HEAT_B = 0.55`, `AMP_EXP = 0.725`) and **treat the
1.2×-is-survivable rung as a requirement, not an outcome** — if every overheating trace eventually
burns, the amber state means "you have already lost" and the player has no reason to look at it.

### 13.7 A dump made of wire is a short  *(from C)*

The most valuable playtest datum either runner-up produced, and it was a *failure*: C's scripted
policy built a ballast path to dump surplus current and **killed its own run in 3.0 s**, because a
19-segment thin path to ground is ~0.20 Ω against 0.35 Ω load branches — so the dump took 46 % of
the budget and starved everything it was meant to protect.

This applies verbatim to the ballast and keep-out content at levels 13–20. **If a bot with perfect
information gets it exactly backwards, a first-time player certainly will.** So ballast is not
introduced as an affordance and explained; it is introduced as a **board that is already wrong in
that exact way**, where the player's first move is to *narrow* the dump and watch the modules come
back. Teach it as a repair, not as a tool.

### 13.8 Motion decides meaning  *(from C)*

C's justification for its cut gesture is better than this document's and costs nothing to adopt:
**the iron adds where it moves and lifts where it lingers.** That is why hold-to-cut is intuitive
rather than arbitrary — park a soldering iron on a trace and it comes up. State it that way in the
how-to guide. Take also C's **live ring on the stroke head**: whatever the width verb is doing, it
is shown at the fingertip, in the material, never in a corner of the HUD.

C's score unit is also the right one: **coulombs delivered** — charge that actually reached a
module inside its window. It is the honest quantity, it is the same thing B scores in
(amp-seconds), and it is what the burn-in mode should count. The laddered mode keeps board-pass
plus a copper/via par for the three-star chase.

### 13.9 What was deliberately *not* taken

- **B's instrument strip.** Seven aligned window meters in a bar across the board. B's own author
  called it "UI wearing a solder-mask coat" and it is — but it is *load-bearing* UI there, because
  comparison of seven branches needs a common baseline. This design has two to five modules, each
  with its own demand bar drawn *at the part*, and that is the more beautiful answer. It is only
  available because there are fewer things to compare. If module count ever passes five, this
  decision has to be revisited rather than defended.
- **C's stroke-speed gauge.** Speed as a semantic channel means the game demands slowness at
  exactly the moment it has manufactured panic. Dwell has the same failure mode in a milder form,
  which is why §2 now ships retrace alongside it.
- **B's fixed board.** Seven identical columns is a readable diagram and a repetitive one. Board
  authorship is the reason this design has a par chase and a level-50 that does not look like
  level 5.

---

## Prototype index

| File | What it is |
|---|---|
| `…\scratchpad\bld\circuit.js` | The game: solver, board model, input, renderer. 1,303 lines. |
| `…\scratchpad\bld\index.html` | Playable. Open it and drag on the board. |
| `…\scratchpad\bld\probe.js` | Physics tables + pixel measurement + performance. |
| `…\scratchpad\bld\FINAL-PROBE.txt` | The raw run every number in this document comes from. |
| `scene-1…10-*.png` | Level 1 untouched · mid-drag · starved · holding · current division · about to burn · scarred · a late board with back-layer routing · desktop · the short circuit. |

**The runner-up prototypes, kept because their benches are being grafted (§13):**

| File | What it is | Why it is still wanted |
|---|---|---|
| `…\scratchpad\circuit-core.js` · `circuit-bench.js` | Design B — the 7-gate board and its policy bench. | §13.1 — the `none` / `panic` / lookahead harness with human latency is the model for the routing bench, and `circuit-frame-02.png` is the densest copper render of the three. |
| `…\scratchpad\hotcopper.html` · `hc_probe.js` | Design C — the ramping-supply board. | §13.5–13.7 — the ramp law, the thermal knife-edge table, and the run its own ballast policy killed in 3.0 s. |
| `…\scratchpad\carrier-test.html` · `carrier-01.png` | Six carrier treatments, measured. | The finding that decided the palette: a cyan carrier on *bright* copper scores 2.43:1 and loses its hue entirely; on oxidised copper, 6.25:1 and it stays cyan. **The conductor must be darker than the charge.** |
| `…\scratchpad\pcb-01/02/03.png` | The three material-study frames. | The luminance-budget progression: naive → materials right / signalling wrong → budget corrected. |

---

# 14. Rendering a real PCB on Canvas2D — the technique layer

> **Scope.** §10 states *what the board should look like* and measures the result. This section
> states *how each material is made* and measures the cost. It is a second, independent
> prototype (`…\scratchpad\pcb\`) written to answer one question: can a board that reads as a
> photographed PCB — layer stack, plating, weave, vias, slivers — hold 60 fps on a phone with
> no image files? **Yes, with 3.3 ms of a 16.7 ms budget at 375×812 dpr2.** Four claims in the
> first draft of this section were falsified by measurement and are recorded as such.
>
> Environment: Electron, **software raster** (`disableHardwareAcceleration`), offscreen,
> muted, headless. Software raster is the pessimistic case and was chosen deliberately — a
> phone with a working GPU-composited canvas does better, not worse.

## 14.1 The layer stack is the whole trick

A PCB photograph reads as a PCB because of **four stacked physical layers**, not because of its
colour. Render them in order and the look arrives on its own; author the final colours directly
and it never does.

| Layer | Physically | Canvas2D technique |
|---|---|---|
| 1 · FR-4 substrate | woven glass cloth in epoxy | baked 64 px **plain-weave tile**, `createPattern(…,'repeat')` |
| 2 · Copper | 35 µm etched foil, stands proud | 4-pass stroked path (shadow / oxide / face / grain) on its own offscreen |
| 3 · Solder mask | a **translucent film** over the topography | `multiply` of a tint, then a low-alpha `source-over` scatter |
| 4 · Silkscreen | screen-printed epoxy ink | offscreen + `filter:'blur(0.85px)'`, bled copy under a crisp core |
| 4b · Surface finish | ENIG / HASL / hard gold, in the mask apertures | drawn **after** the film, never dimmed by it |

### The single most important technique in this document: the mask is a `multiply`, not a fill

Solder mask is a translucent film. Light goes down through it, reflects off whatever is
underneath, and comes back up through it. That is **multiplication**, and Canvas2D has the
operator:

```js
// mk = mask coverage, opaque tint, with pad apertures punched by destination-out
ctx.globalCompositeOperation = 'multiply';  ctx.drawImage(mk, 0, 0);
ctx.globalCompositeOperation = 'source-over';
ctx.globalAlpha = 0.34; ctx.drawImage(mkScatter, 0, 0);   // the film's own body
```

Because it is multiplicative, **buried copper stays brighter than buried substrate for free**.
You do not author "mask over copper" and "mask over substrate" as two greens — you author one
film, and the copper underneath decides. Three things fall out that authored flats cannot do:

- a *hot* trace glows **through** the mask correctly, because its own luminance is the input;
- the ground pour, inner-layer ghosting and thieving all differentiate themselves with no
  extra colours;
- apertures are free — where the coverage canvas is transparent, `multiply` is a no-op and
  bare plating shows through.

**Falsified claim, recorded.** The first draft asserted the film would also *win on contrast*.
It does not. Measured on the same region: film pipeline **1.14:1** buried-trace-vs-field, flat
authored greens **1.25:1**. The flat version is marginally more legible and looks like a
diagram; the film version looks like a board and responds to state. The film is chosen for
**material and for dynamic range**, not for contrast, and §14.4 buys the contrast back a
different way.

## 14.2 Per-material recipes

**FR-4 weave** — a real plain weave, never a noise field. Tile 64 px, pitch 8 px, bundle 6.4 px.
Three passes: all warp strips laid down, all weft over them, then warp redrawn **clipped to the
cells where `(i+j)` is even** so it genuinely interlaces. Each bundle is a cylinder — a
perpendicular linear gradient `weft → warp → warp → weft` with stops at 0/0.30/0.52/1.0 — plus
three wrapping hairline striations at α 0.13 for glass anisotropy. Then an epoxy flood
(`rgba(156,138,87,0.34)`) and one crossover-occlusion multiply. Seamless because the tile is an
exact multiple of the pitch.

**Copper trace** — four passes, per segment. Passes 1 and 3 are what stop it being an orange
rectangle:
1. contact shadow, `width+1.6`, offset **against** the light — copper occludes;
2. oxide flank, `width+1.3`, `#5A3115` (an etched edge is darker *and redder* than the face);
3. face — a linear gradient **across** the trace, not along it (rolled foil grains along its
   length, so the highlight is a *flank*, never a blob);
4. grain — the baked streak tile at α 0.26, `overlay`.

Round caps and joins hide the per-segment seams; 45° mitring (`mitre()`, radius 7 px) is one
pass over the point list and is the strongest single "this is a routed board" tell.

**Plating — the finishes differ by VARIANCE, not by hue.** This is the finding I would carry to
any other project:

| | mean L\* | **sd L\*** | max L\* |
|---|---|---|---|
| ENIG (flat satin) | 64.7 | **2.6** | 72.3 |
| HASL (frozen puddle) | 60.7 | **11.8** | **98.2** |

They are 4 L\* apart in mean and measure **1.07:1** against each other — they are *not*
distinguished by brightness or colour. ENIG is a shallow gradient over a flat fill plus one
thin nickel rim; HASL is a radial dome plus a **small sharp hot spot** offset toward the key and
jittered per pad. Two finishes on one board is what stops everything metallic reading as "the
same shiny thing".

**Falsified claim, recorded.** My first ENIG measured **sd 18.8 — higher than the HASL beside
it**, i.e. exactly backwards, and rendered as a gold ball. Two separate errors: a wide-range
gradient, and a measurement bug (a *square* sample window around a *round* pad is dominated by
the pad/background edge and reports ≈17 for every finish). Sampling inside `r×0.78` and
flattening the ramp gave the table above. **A material claim measured through a bad window is
not measured.**

**Via** — annular ring, then a black bore, then a radial gradient offset **away** from the key
(light falls into the tube and lands on the *far* wall), then a dark arc on the near rim. The
asymmetry is the entire effect: a symmetric dark circle reads as a dot, not a hole.
**Tented via** — a soft dome with a centre sag dimple, measured at **1.17:1** against the mask
field. Deliberately almost invisible: it is texture, not information.

**Teardrops** — the fab fillet where a trace enters a pad. Two quadratics from the trace edges
to tangent points at `0.72 r` on the pad. Absent from most procedural boards, present on every
real one, and it costs one filled path.

**Silkscreen bleed** — screen-printed ink spreads; it is never a crisp vector. Draw to an
offscreen, then compose a blurred copy at α 0.62 **under** a crisp copy at α 0.40. `ctx.filter`
is expensive (2.8 ms full-screen) and therefore belongs **only** on baked content.

**Mask slivers** — the fragile 1–1.5 px mask webs between fine-pitch pads, with a lit edge on
the key side and a dark edge opposite. One of the most recognisable close-up PCB details and
almost free.

**Edge fingers** — hard gold with a per-finger diagonal gradient, a dark trailing edge, a bright
leading edge, and a **routed chamfer** along the board edge (transparent → light → black over
4 px). The chamfer is the tell.

## 14.3 Cost, measured at both target resolutions

**What is cacheable: essentially everything.** The entire static board — substrate, copper,
film, plating, silkscreen, components — bakes to one offscreen and the frame loop draws it with
a single `drawImage`.

| | 375×812 dpr1 | **750×1624 (phone dpr2)** | 1920×1080 |
|---|---|---|---|
| Board bake (once per topology change) | 8.8 ms | **31.1 ms** | 65.7 ms |
| Board blit alone | 0.1 ms | **0.4 ms** | 0.8 ms |
| 240 carriers alone | 1.0 ms | **1.7 ms** | 3.6 ms |
| **Full frame, 240 carriers** | 1.2 ms | **2.1 ms** | 3.9 ms |
| **Full frame, 420 carriers** | 2.2 ms | **3.3 ms** (p95 5.4) | 5.9 ms (p95 7.6) |
| *Naive: rebake the board every frame* | *12.7 ms* | ***36.4 ms*** | *68.5 ms* |

**Caching the board is worth 17× at phone dpr2** (2.1 ms vs 36.4 ms) and is the difference
between shipping and not. Tile bakes are one-time: weave 90.9 ms, grain 2.0 ms.

Isolated costs at 750×1624, for budgeting: one full-screen `multiply` **3.7 ms**; 36 four-pass
trace segments **5.9 ms**; a full-screen `blur(0.85px)` silk bake **2.8 ms**; 60 ENIG pads
1.5 ms; 60 open vias 2.2 ms; 60 tented vias 0.6 ms; a full-screen weave pattern fill **0.3 ms**.

**Carrier sprites beat live gradients by 30–50 %.** Bake the halo once to a small canvas and
blit it: 420 carriers cost 3.3 ms as sprites vs 4.2 ms as per-carrier `createRadialGradient`
(1920×1080: 5.9 vs 8.9 ms). `createRadialGradient` inside a per-object loop is the most common
avoidable cost in this kind of renderer.

**A composite op inside a hot loop is catastrophic — measured.** The weave tile's crossover
occlusion was 64 per-cell `multiply` fills on a 64 px canvas and cost **601 ms**. Building the
occlusion map with plain `source-over` and applying it with **one** `multiply` gave identical
pixels at **90.9 ms** — 6.6× (I predicted 70× and was wrong; the remaining cost is the 64
radial gradients themselves, not the compositing). **Every `globalCompositeOperation` change
forces a surface read/modify/write. Count them; do not count draw calls.**

## 14.4 Two failures found by measuring, and their generalisable fixes

**Buried copper measured 1.055:1 against the mask field — invisible.** My hypothesis was the
scatter pass. I swept its alpha 0.00 → 0.80 and the contrast moved from 1.03 to 1.06. **The
hypothesis was wrong and the sweep is what said so.** Two real causes:

1. **Flank treatments must be a constant width in PIXELS, not a fraction of the stroke.** The
   face gradient used proportional stops (0.34 / 0.66), so an 8 px trace was *all flank and no
   face* — its centre sat 75 % of the way into the oxide colour. Deriving the flank fraction
   from an absolute 1.6 px (`fl = clamp(1.6/w, 0.06, 0.34)`) with a plateau between took it to
   **1.27:1** on its own. This generalises to every bevel, rim and edge highlight in the game.
2. **The substrate was too bright to leave copper anywhere to go.** Real FR-4 seen from the
   mask side is resin-rich and dull. Darkening the weave tile by 0.64 finished the job.

| substrate darken | mask field L\* | buried trace | vs field | pour vs field | silk vs field |
|---|---|---|---|---|---|
| 1.00 | 26.8 | 33.7 | 1.28 | 1.14 | 3.58 |
| 0.74 | 24.0 | 33.7 | 1.42 | 1.26 | 3.96 |
| **0.64** | **22.8** | **33.7** | **1.47** | **1.31** | **4.11** |
| 0.47 | 21.2 | 33.7 | 1.55 | 1.39 | 4.34 |

**Darken with a TINT, not with grey.** A neutral multiply drops chroma with luminance and the
tan cloth turns to concrete (visible in the first study sheet). Multiplying by
`rgb(255,238,198)×k` is the same darkening with the chroma intact.

**Final contrast table, all three resolutions:**

| What it decides | Target | 375×812 | 750×1624 | 1920×1080 |
|---|---|---|---|---|
| Charge vs the copper it rides | ≥ 4.5:1 | 5.97 | **7.09** | 7.49 |
| Charge vs mask field | ≥ 4.5:1 | 8.12 | **10.59** | 9.52 |
| Hot vs cold copper | ≥ 3:1, ΔL\* > 0 | 6.74 | **6.23** (ΔL\* +57.7) | 6.67 |
| Dead copper — findable but quiet | 1.2–1.6:1 | 1.36 | **1.49** | 1.27 |
| Silkscreen — legible, not shouting | 4–6.5:1 | 3.48 | **4.17** | 4.60 |
| ENIG / HASL pads vs field | ≥ 3:1 | 4.16 / 4.49 | **4.40 / 4.71** | 4.00 / 4.45 |
| ENIG vs HASL — *must be low* | ≤ 1.3:1 | 1.08 | **1.07** | 1.11 |
| Tented via — deliberately dull | ≤ 1.3:1 | 1.09 | **1.17** | 1.27 |
| Charge vs heat, hue separation | ≥ 120° | 139° | **147°** | 147° |
| **Board area above L\* 70** | ≤ 1 % | 0.89 % | **0.92 %** | 1.27 % |

The luminance budget holds: **under 1 % of the board is bright on a phone**. Bright is a
signal, not a surface. Silkscreen at 375×812 (3.48) sits just under its band and wants α 0.82
rather than 0.78 at dpr1 — the one open tuning item.

## 14.5 Animating current so it reads as current

**Direction, magnitude and rate are three separate channels and each gets its own encoding.**
Nothing here is brightness, because brightness is already spent on *heat* and on *importance*.

- **Direction** — carriers advance along the path and the comet tail trails **behind**, so a
  still frame still shows which way. Asymmetry, not an arrow.
- **Magnitude** — carrier **spacing**. Countable. A 74/26 split at a junction is literally
  countable, because carriers are routed into a branch with probability equal to its share.
- **Rate** — **speed = current density J = I/w**, so the *same* current in *narrower* copper
  visibly runs faster. Two quantities, two channels, no collision.
- **Surge vs trickle** — a surge is dense **and** fast **and** the carrier stretches into a
  capsule whose **length ∝ speed** (motion blur as a drawn primitive, not a filter), **and** the
  trace itself warms. Four cues agreeing. A trickle is four well-separated dots crawling on
  cold copper.
- **Voltage ≠ current.** On switch-on the *field* arrives in one frame — a wavefront sweeps the
  whole net and the mask relief brightens — and only then do carriers begin to crawl. That
  distinction is physically true, visually rich, and free.
- **Accumulation must be a state, not an animation.** A capacitor shows a **fill level** that
  persists when input stops; carriers arriving at the plate are *absorbed* and stop. A pulsing
  glow cannot represent a state variable. A column that stays where you left it can.

### The hard limit, measured: carriers fuse into exactly the bar we are avoiding

"Magnitude = spacing" is only a readout while the dots are separable. Past a density they fuse
into a lit bar — the "glowing line that pulses" failure, arrived at from the other direction.
Measured as peak L\* minus **mean trough** L\* along the trace centreline (the honest metric;
global max/min is pinned by the brightest core and the darkest gap and stays flat at 0.525
regardless of density, which is why the first attempt at this measurement said nothing):

| halo radius | spacing 16 | 12 | 10 | 8 | 7 | 6 | 5 |
|---|---|---|---|---|---|---|---|
| 0 (core only, r 2.6) | 65.1 | 65.1 | 65.1 | 65.1 | 63.5 | 35.9 | 5.7 |
| 5.2 px | 65.1 | 65.1 | 61.2 | 51.1 | 43.9 | — | 3.3 |
| 8.3 px | 62.0 | 47.1 | 39.4 | 31.9 | 27.6 | — | 1.7 |
| 11.7 px | 44.5 | 33.6 | 28.3 | 22.7 | — | — | 2.2 |

**It is the halo that fuses, not the core.** With an 11.7 px halo the troughs have risen to
L\* 50 *at 16 px spacing* — already a bar with bumps. Taking "trough stays below L\* 40" as the
criterion, the minimum workable spacing is 6 px at halo 0, 9 px at halo 5.2, 14 px at halo 8.3
and ~19 px at halo 11.7 — a constant ratio:

> **halo radius ≤ 0.58 × carrier spacing**, with an absolute floor of **spacing ≥ 6 px** set by
> the core diameter. The density ladder (`flowtest-ladder.png`) confirms it by eye: 6/12/24/48
> carriers over 800 px are countable, 96 is a fusing chain, 140 is a solid bar.

**This is a design constraint, not a rendering note.** It puts a hard cap on carriers per unit
length, and therefore **a surge can never be drawn by adding more dots**. Above the cap the
magnitude channel is saturated and the game must switch to speed, capsule length, heat and
trace state — which is precisely the behaviour that makes a surge read as a surge instead of as
a brighter line. The 200-carrier frame in `measure-phone_750x1624_dpr2.png` is the failure
preserved: at 2.8 px spacing it is a neon bar.

## 14.6 What the render still gets wrong

Honest, from looking at the frames rather than the tables:

1. **The ground pour and bare substrate are 0.4 L\* apart** (26.1 vs 25.7) — the pour boundary
   reads as a *texture* change, not a copper boundary, and the pour is the largest copper
   feature on the board. It needs its own separation (target ≥ 1.3:1); §14.4's substrate
   darkening gets it to 1.31 and that is the floor, not the goal.
2. **Pour keep-outs are axis-aligned rectangles and read as floating boxes.** A real pour is
   poured *around* the routing with a constant clearance. The keep-out must be generated by
   offsetting the trace paths, not by punching rectangles.
3. **Tented vias read as water droplets** — too glossy and too round. Flatten the dome and cut
   the specular.
4. **HASL is still too spherical.** The variance is now right; the silhouette is not. It wants
   an irregular rim, not a circle.
5. **The bottom third of the board is empty**, exactly as §12.13 says. Confirmed visually.
6. **The weave is convincing at 4× and near-invisible at 1×.** That is arguably correct, but it
   means 90.9 ms of bake is buying detail most players will never resolve on a phone; a cheaper
   2-pass weave should be measured against it before shipping.
7. **Everything here is measured under software raster on a desktop.** No phone has run it. The
   ×6 rule of thumb applied to 3.3 ms says 19.8 ms — *over* budget — and the reason I am not
   worried is that the phone case is GPU-composited while this measurement is not. **That is an
   argument, not a measurement, and it is the first thing to check on real hardware.**

## 14.7 Prototype index for this section

| File | What it is |
|---|---|
| `…\scratchpad\pcb\pcb.js` | The material library: tiles, trace passes, plating, vias, teardrops, film composite, board bake, carriers. Every technique above. |
| `…\scratchpad\pcb\scene.js` | One deterministic board with named probe points. |
| `…\scratchpad\pcb\measure.html` · `probe.js` | The bench: bake/frame cost at 3 resolutions, contrast, luminance budget. Electron, offscreen, muted, self-quitting. |
| `…\scratchpad\pcb\sweep.html` | The scatter-alpha, copper and substrate sweeps of §14.4. |
| `…\scratchpad\pcb\studies.html` | The 9-panel material sheet (`studies-main.png`). |
| `…\scratchpad\pcb\flowtest.html` | The carrier-fusion measurement and the density ladder. |
| `RESULT-*.json` | Every number in this section, raw. |
