← all documents · raw markdown · 71 KB

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:

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.

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.

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:

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:

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

---

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:

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

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:

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:

and weakness #4 is real and measurable rather than a worry.

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:

division does the rest. No solver change โ€” it is one conductance being driven by input.

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.

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.

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

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.

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.

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:

// 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:

extra colours;

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*.

still frame still shows which way. Asymmetry, not an arrow.

countable, because carriers are routed into a branch with probability equal to its share.

visibly runs faster. Two quantities, two channels, no collision.

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.

whole net and the mask relief brightens โ€” and only then do carriers begin to crawl. That

distinction is physically true, visually rich, and free.

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. |