diff --git a/fable/stack/SPEC.md b/fable/stack/SPEC.md new file mode 100644 index 0000000..709e72a --- /dev/null +++ b/fable/stack/SPEC.md @@ -0,0 +1,99 @@ +# The Chamber as a Volume — acrylic stack spec & depth design + +A backlit, six-plane physical piece for the eufyMake E1: three acrylic sheets, +printed front **and** back, spaced with air gaps over an LED panel. This is +roadmap Phase 3 made literal — the scene model's per-track `z` and `age` stop +being opacity tricks and become actual millimetres. + +## Physical stack (proposed — buy list at bottom) + +``` +viewer + ─ C-front the hand: grease pencil, réseau, fiducials, header, scratches + ═ sheet C 3 mm clear cast acrylic + ─ C-back near scatter: δ-ray curls, close cosmics + ~6 mm air gap + ─ B-front THE EVENT: sharp beaded track cores + ═ sheet B 3 mm + ─ B-back halation: the same tracks, blurred — real glow-behind-the-line + ~6 mm air gap + ─ A-front history: older background events, sinking + ═ sheet A 3 mm + ─ A-back the cause: pressure disk (CMYK-only → pure glow) + chamber optics + ~12 mm gap + ▤ diffuser frosted acrylic 2–3 mm (kills LED hotspots) + ▦ LED panel A3 slim light pad +``` + +Plane depths (A-back = 0): 0 / 3 / 9 / 12 / 18 / 21 mm — two depth scales: +3 mm *within* a sheet (fine parallax: halation, moiré) and 9 mm *between* +sheets (coarse parallax: cause → event → hand). + +**The depth story is causal, not just spatial**: deepest plane is the cause +(the pressure wave, pure transmitted light), then history, then the halation, +then the event itself, then the debris flying toward you, and finally — on the +front glass, where scanners actually wrote — the human hand measuring it all. +Parallax reads as *interpretation floating above evidence*. + +## Sheet & print geometry + +- Sheet: 330 × 420 mm (E1 full bed, A3+), portrait. 3 mm clear cast acrylic, + gloss both faces. +- Art square: 300 × 300 mm, centred horizontally (15 mm margins), top edge at + y = 20 mm. Zone below (y = 320–420) carries film furniture / data box / + caption on C-front only; other sheets stay clear there (light spills through + — the plate floats in a lit void). +- Corner mounting holes 5 mm ∅ at 8 mm insets, all sheets + diffuser, for M4 + nylon standoffs (3 mm + 6 mm + 6 mm spacer stacking, or threaded rod with + nuts as spacers). +- Registration: every plane file carries corner crosses at the same four + coordinates just outside the art square; back-face files are **mirrored** at + export so they read correctly through the sheet. + +## White-ink logic (backlit grammar) + +| zone | channels | lit result | +|---|---|---| +| clear acrylic | none | pure light | +| glow elements (disk, halation, deep tracks) | CMYK only | stained-glass — light pours through colour | +| focal ink (event cores, grease pencil, furniture) | CMYK + white underlay | holds density against the panel — reads as ink | +| blocking (none in v1) | white + black | opaque | + +Rule of thumb: **depth = transparency**. The deeper the plane, the less white +ink; the front glass is the most "solid" ink. The disk gets *zero* white — lit, +it becomes a light source (the laser-interference reference lives here, later). + +## Pipeline + +``` +plan (fable/stack/plans/*.mjs) + → node tools/stack-render.mjs per-plane SVGs + manifest.json + → fable/stack/simulator.html?c= parallax + backlight art-direction + → [next] tools/stack-export.mjs print masters: mirrored backs, + white-ink masks, A3 layout @ true scale +``` + +`src/render/stack.js` splits one generated scene into per-plane sub-scenes +(selectors: track kind / z band / age band; disk, optics, damage, media, +fiducials, header as flags) and renders each through the untouched `renderSVG` +with `transparentPaper`. Bloom/core pairs use the *same* track selector so +bubble positions are identical on both faces (same filtered list → same salted +draws) — keep `density`/`size` equal within a pair. + +Per-plane renders set `depth`/`aging` low: the physical stack now *is* the +depth model; double-encoding it as opacity would fight the medium. + +## Calibration coupon (print before the hero) + +One A5 sheet, double-sided: white-ink step wedge (0/25/50/75/100%, single + +double pass) · the same δ-ray at three white levels · CMYK-only vs CMYK+white +swatch pair · front/back registration cross · 6 pt / 8 pt mono type both faces. +Judge **lit on the panel**, not in hand. + +## Buy list (if starting from zero) + +- 3× cast clear acrylic 330 × 420 × 3 mm (+1 spare), protective film both sides +- 1× frosted/opal acrylic 330 × 420 × 2–3 mm (diffuser) +- A3 slim LED light panel (dimmable if possible) +- M4 nylon standoff kit (assorted 3/6/12 mm) or 4× M4 threaded rod + nuts +- IPA + lint-free cloths; adhesion promoter if the E1 profile recommends it diff --git a/fable/stack/candidates/01-event-volume/composite-lit.png b/fable/stack/candidates/01-event-volume/composite-lit.png new file mode 100644 index 0000000..2c0c62e Binary files /dev/null and b/fable/stack/candidates/01-event-volume/composite-lit.png differ diff --git a/fable/stack/candidates/01-event-volume/manifest.json b/fable/stack/candidates/01-event-volume/manifest.json new file mode 100644 index 0000000..60555c3 --- /dev/null +++ b/fable/stack/candidates/01-event-volume/manifest.json @@ -0,0 +1,60 @@ +{ + "id": "01-event-volume", + "title": "The Event, Suspended", + "seed": "LAMBDA-2648", + "geometry": { + "thickness": 3, + "gap": 6 + }, + "planes": [ + { + "file": "plane-00-A-back.svg", + "sheet": "A", + "face": "back", + "zmm": 0, + "label": "the cause — disk body + chamber optics" + }, + { + "file": "plane-01-A-back.svg", + "sheet": "A", + "face": "back", + "zmm": 0, + "label": "the cause — disk granular texture" + }, + { + "file": "plane-02-A-front.svg", + "sheet": "A", + "face": "front", + "zmm": 3, + "label": "history — older events sink" + }, + { + "file": "plane-03-B-back.svg", + "sheet": "B", + "face": "back", + "zmm": 9, + "label": "halation — the trigger blooms behind itself" + }, + { + "file": "plane-04-B-front.svg", + "sheet": "B", + "face": "front", + "zmm": 12, + "label": "the event — sharp beaded cores" + }, + { + "file": "plane-05-C-back.svg", + "sheet": "C", + "face": "back", + "zmm": 18, + "label": "near scatter — δ-curls lift toward the eye" + }, + { + "file": "plane-06-C-front.svg", + "sheet": "C", + "face": "front", + "zmm": 21, + "label": "the hand — grease pencil on the glass" + } + ] +} \ No newline at end of file diff --git a/fable/stack/candidates/01-event-volume/plane-00-A-back.svg b/fable/stack/candidates/01-event-volume/plane-00-A-back.svg new file mode 100644 index 0000000..0d054f5 --- /dev/null +++ b/fable/stack/candidates/01-event-volume/plane-00-A-back.svg @@ -0,0 +1,38 @@ + + +Bubble Chamber · seed=LAMBDA-2648 · hash=13c75926dbe348 · palette=magentarise + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/fable/stack/candidates/01-event-volume/plane-01-A-back.svg b/fable/stack/candidates/01-event-volume/plane-01-A-back.svg new file mode 100644 index 0000000..777e7b6 --- /dev/null +++ b/fable/stack/candidates/01-event-volume/plane-01-A-back.svg @@ -0,0 +1,42 @@ + + + +Bubble Chamber · seed=LAMBDA-2648 · hash=13c75926dbe348 · palette=magentarise + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/fable/stack/candidates/01-event-volume/plane-02-A-front.svg b/fable/stack/candidates/01-event-volume/plane-02-A-front.svg new file mode 100644 index 0000000..1d2393b --- /dev/null +++ b/fable/stack/candidates/01-event-volume/plane-02-A-front.svg @@ -0,0 +1,157 @@ + + + +Bubble Chamber · seed=LAMBDA-2648 · hash=13c75926dbe348 · palette=magentarise + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/fable/stack/candidates/01-event-volume/plane-03-B-back.svg b/fable/stack/candidates/01-event-volume/plane-03-B-back.svg new file mode 100644 index 0000000..4b7bf60 --- /dev/null +++ b/fable/stack/candidates/01-event-volume/plane-03-B-back.svg @@ -0,0 +1,106 @@ + + + +Bubble Chamber · seed=LAMBDA-2648 · hash=13c75926dbe348 · palette=magentarise + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/fable/stack/candidates/01-event-volume/plane-04-B-front.svg b/fable/stack/candidates/01-event-volume/plane-04-B-front.svg new file mode 100644 index 0000000..c71fc76 --- /dev/null +++ b/fable/stack/candidates/01-event-volume/plane-04-B-front.svg @@ -0,0 +1,105 @@ + + +Bubble Chamber · seed=LAMBDA-2648 · hash=13c75926dbe348 · palette=magentarise + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/fable/stack/candidates/01-event-volume/plane-05-C-back.svg b/fable/stack/candidates/01-event-volume/plane-05-C-back.svg new file mode 100644 index 0000000..689847a --- /dev/null +++ b/fable/stack/candidates/01-event-volume/plane-05-C-back.svg @@ -0,0 +1,207 @@ + + +Bubble Chamber · seed=LAMBDA-2648 · hash=13c75926dbe348 · palette=magentarise + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/fable/stack/candidates/01-event-volume/plane-06-C-front.svg b/fable/stack/candidates/01-event-volume/plane-06-C-front.svg new file mode 100644 index 0000000..9361dd1 --- /dev/null +++ b/fable/stack/candidates/01-event-volume/plane-06-C-front.svg @@ -0,0 +1,41 @@ + + +Bubble Chamber · seed=LAMBDA-2648 · hash=13c75926dbe348 · palette=magentarise + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +GARGAMELLE · CERNSEED LAMBDA-2648PLATE 840EXPOSED 1973.07.14 + + +KODAK SAFETY FILMROLL 663FRAME 8401973.07.14Nº 84033°V⁰ ? + + diff --git a/fable/stack/candidates/02-palimpsest/composite-lit.png b/fable/stack/candidates/02-palimpsest/composite-lit.png new file mode 100644 index 0000000..4aa76b9 Binary files /dev/null and b/fable/stack/candidates/02-palimpsest/composite-lit.png differ diff --git a/fable/stack/candidates/02-palimpsest/manifest.json b/fable/stack/candidates/02-palimpsest/manifest.json new file mode 100644 index 0000000..2f7c308 --- /dev/null +++ b/fable/stack/candidates/02-palimpsest/manifest.json @@ -0,0 +1,67 @@ +{ + "id": "02-palimpsest", + "title": "Palimpsest", + "seed": "KAON-0088", + "geometry": { + "thickness": 3, + "gap": 6 + }, + "planes": [ + { + "file": "plane-00-A-back.svg", + "sheet": "A", + "face": "back", + "zmm": 0, + "label": "deep time — oldest exposures + the chamber" + }, + { + "file": "plane-01-A-front.svg", + "sheet": "A", + "face": "front", + "zmm": 3, + "label": "earlier cycle" + }, + { + "file": "plane-02-B-back.svg", + "sheet": "B", + "face": "back", + "zmm": 9, + "label": "moments ago" + }, + { + "file": "plane-03-B-back.svg", + "sheet": "B", + "face": "back", + "zmm": 9, + "label": "the pressure wave — body + texture" + }, + { + "file": "plane-04-B-back.svg", + "sheet": "B", + "face": "back", + "zmm": 9, + "label": "pressure-wave grain" + }, + { + "file": "plane-05-B-front.svg", + "sheet": "B", + "face": "front", + "zmm": 12, + "label": "NOW — the trigger, all kinds, sharp" + }, + { + "file": "plane-06-C-back.svg", + "sheet": "C", + "face": "back", + "zmm": 18, + "label": "still in flight — δ-debris of now" + }, + { + "file": "plane-07-C-front.svg", + "sheet": "C", + "face": "front", + "zmm": 21, + "label": "after — the hand, the furniture, the wear" + } + ] +} \ No newline at end of file diff --git a/fable/stack/candidates/02-palimpsest/plane-00-A-back.svg b/fable/stack/candidates/02-palimpsest/plane-00-A-back.svg new file mode 100644 index 0000000..40b9397 --- /dev/null +++ b/fable/stack/candidates/02-palimpsest/plane-00-A-back.svg @@ -0,0 +1,49 @@ + + + +Bubble Chamber · seed=KAON-0088 · hash=1e2f9c933ac58 · palette=mono + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/fable/stack/candidates/02-palimpsest/plane-01-A-front.svg b/fable/stack/candidates/02-palimpsest/plane-01-A-front.svg new file mode 100644 index 0000000..873b838 --- /dev/null +++ b/fable/stack/candidates/02-palimpsest/plane-01-A-front.svg @@ -0,0 +1,50 @@ + + + +Bubble Chamber · seed=KAON-0088 · hash=1e2f9c933ac58 · palette=mono + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/fable/stack/candidates/02-palimpsest/plane-02-B-back.svg b/fable/stack/candidates/02-palimpsest/plane-02-B-back.svg new file mode 100644 index 0000000..ef19c79 --- /dev/null +++ b/fable/stack/candidates/02-palimpsest/plane-02-B-back.svg @@ -0,0 +1,38 @@ + + +Bubble Chamber · seed=KAON-0088 · hash=1e2f9c933ac58 · palette=mono + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/fable/stack/candidates/02-palimpsest/plane-03-B-back.svg b/fable/stack/candidates/02-palimpsest/plane-03-B-back.svg new file mode 100644 index 0000000..d0ca467 --- /dev/null +++ b/fable/stack/candidates/02-palimpsest/plane-03-B-back.svg @@ -0,0 +1,34 @@ + + +Bubble Chamber · seed=KAON-0088 · hash=1e2f9c933ac58 · palette=mono + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/fable/stack/candidates/02-palimpsest/plane-04-B-back.svg b/fable/stack/candidates/02-palimpsest/plane-04-B-back.svg new file mode 100644 index 0000000..cadf594 --- /dev/null +++ b/fable/stack/candidates/02-palimpsest/plane-04-B-back.svg @@ -0,0 +1,42 @@ + + + +Bubble Chamber · seed=KAON-0088 · hash=1e2f9c933ac58 · palette=mono + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/fable/stack/candidates/02-palimpsest/plane-05-B-front.svg b/fable/stack/candidates/02-palimpsest/plane-05-B-front.svg new file mode 100644 index 0000000..6bff06a --- /dev/null +++ b/fable/stack/candidates/02-palimpsest/plane-05-B-front.svg @@ -0,0 +1,40 @@ + + +Bubble Chamber · seed=KAON-0088 · hash=1e2f9c933ac58 · palette=mono + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/fable/stack/candidates/02-palimpsest/plane-06-C-back.svg b/fable/stack/candidates/02-palimpsest/plane-06-C-back.svg new file mode 100644 index 0000000..e8f1f1a --- /dev/null +++ b/fable/stack/candidates/02-palimpsest/plane-06-C-back.svg @@ -0,0 +1,36 @@ + + +Bubble Chamber · seed=KAON-0088 · hash=1e2f9c933ac58 · palette=mono + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/fable/stack/candidates/02-palimpsest/plane-07-C-front.svg b/fable/stack/candidates/02-palimpsest/plane-07-C-front.svg new file mode 100644 index 0000000..d5d59df --- /dev/null +++ b/fable/stack/candidates/02-palimpsest/plane-07-C-front.svg @@ -0,0 +1,41 @@ + + +Bubble Chamber · seed=KAON-0088 · hash=1e2f9c933ac58 · palette=mono + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +GARGAMELLE · CERNSEED KAON-0088PLATE 163EXPOSED 1971.06.13 + + +KODAK SAFETY FILMROLL 627FRAME 1631971.06.13Nº 163 + + diff --git a/fable/stack/candidates/03-interference/composite-lit.png b/fable/stack/candidates/03-interference/composite-lit.png new file mode 100644 index 0000000..8f65ded Binary files /dev/null and b/fable/stack/candidates/03-interference/composite-lit.png differ diff --git a/fable/stack/candidates/03-interference/manifest.json b/fable/stack/candidates/03-interference/manifest.json new file mode 100644 index 0000000..813064f --- /dev/null +++ b/fable/stack/candidates/03-interference/manifest.json @@ -0,0 +1,67 @@ +{ + "id": "03-interference", + "title": "The Measurement Interferes", + "seed": "NEUTRINO-1973", + "geometry": { + "thickness": 3, + "gap": 6 + }, + "planes": [ + { + "file": "plane-00-A-back.svg", + "sheet": "A", + "face": "back", + "zmm": 0, + "label": "the spectral disk — interference at the source" + }, + { + "file": "plane-01-A-back.svg", + "sheet": "A", + "face": "back", + "zmm": 0, + "label": "the disk — granular spray" + }, + { + "file": "plane-02-A-front.svg", + "sheet": "A", + "face": "front", + "zmm": 3, + "label": "the sea — sweepers, cosmics, history" + }, + { + "file": "plane-03-B-back.svg", + "sheet": "B", + "face": "back", + "zmm": 9, + "label": "deep réseau — scaled +1.5%, moirés against the front grid" + }, + { + "file": "plane-04-B-back.svg", + "sheet": "B", + "face": "back", + "zmm": 9, + "label": "halation — the trigger blooms" + }, + { + "file": "plane-05-B-front.svg", + "sheet": "B", + "face": "front", + "zmm": 12, + "label": "the event — sharp" + }, + { + "file": "plane-06-C-back.svg", + "sheet": "C", + "face": "back", + "zmm": 18, + "label": "δ-curls in flight" + }, + { + "file": "plane-07-C-front.svg", + "sheet": "C", + "face": "front", + "zmm": 21, + "label": "front réseau + the hand — measurement on glass" + } + ] +} \ No newline at end of file diff --git a/fable/stack/candidates/03-interference/plane-00-A-back.svg b/fable/stack/candidates/03-interference/plane-00-A-back.svg new file mode 100644 index 0000000..5226ca3 --- /dev/null +++ b/fable/stack/candidates/03-interference/plane-00-A-back.svg @@ -0,0 +1,38 @@ + + +Bubble Chamber · seed=NEUTRINO-1973 · hash=8fbd3625c7ebb · palette=mono + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/fable/stack/candidates/03-interference/plane-01-A-back.svg b/fable/stack/candidates/03-interference/plane-01-A-back.svg new file mode 100644 index 0000000..7861ec5 --- /dev/null +++ b/fable/stack/candidates/03-interference/plane-01-A-back.svg @@ -0,0 +1,42 @@ + + + +Bubble Chamber · seed=NEUTRINO-1973 · hash=8fbd3625c7ebb · palette=mono + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/fable/stack/candidates/03-interference/plane-02-A-front.svg b/fable/stack/candidates/03-interference/plane-02-A-front.svg new file mode 100644 index 0000000..23739c8 --- /dev/null +++ b/fable/stack/candidates/03-interference/plane-02-A-front.svg @@ -0,0 +1,44 @@ + + + +Bubble Chamber · seed=NEUTRINO-1973 · hash=8fbd3625c7ebb · palette=mono + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/fable/stack/candidates/03-interference/plane-03-B-back.svg b/fable/stack/candidates/03-interference/plane-03-B-back.svg new file mode 100644 index 0000000..9faec66 --- /dev/null +++ b/fable/stack/candidates/03-interference/plane-03-B-back.svg @@ -0,0 +1,33 @@ + + + +Bubble Chamber · seed=NEUTRINO-1973 · hash=8fbd3625c7ebb · palette=mono + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/fable/stack/candidates/03-interference/plane-04-B-back.svg b/fable/stack/candidates/03-interference/plane-04-B-back.svg new file mode 100644 index 0000000..7ad1292 --- /dev/null +++ b/fable/stack/candidates/03-interference/plane-04-B-back.svg @@ -0,0 +1,42 @@ + + + +Bubble Chamber · seed=NEUTRINO-1973 · hash=8fbd3625c7ebb · palette=mono + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/fable/stack/candidates/03-interference/plane-05-B-front.svg b/fable/stack/candidates/03-interference/plane-05-B-front.svg new file mode 100644 index 0000000..642e584 --- /dev/null +++ b/fable/stack/candidates/03-interference/plane-05-B-front.svg @@ -0,0 +1,41 @@ + + +Bubble Chamber · seed=NEUTRINO-1973 · hash=8fbd3625c7ebb · palette=mono + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/fable/stack/candidates/03-interference/plane-06-C-back.svg b/fable/stack/candidates/03-interference/plane-06-C-back.svg new file mode 100644 index 0000000..f12e9e0 --- /dev/null +++ b/fable/stack/candidates/03-interference/plane-06-C-back.svg @@ -0,0 +1,36 @@ + + +Bubble Chamber · seed=NEUTRINO-1973 · hash=8fbd3625c7ebb · palette=mono + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/fable/stack/candidates/03-interference/plane-07-C-front.svg b/fable/stack/candidates/03-interference/plane-07-C-front.svg new file mode 100644 index 0000000..72cfb87 --- /dev/null +++ b/fable/stack/candidates/03-interference/plane-07-C-front.svg @@ -0,0 +1,41 @@ + + +Bubble Chamber · seed=NEUTRINO-1973 · hash=8fbd3625c7ebb · palette=mono + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +BEBC · CERNSEED NEUTRINO-1973PLATE 774EXPOSED 1979.-5.-17 + + +KODAK SAFETY FILMROLL 897FRAME 7741979.-5.-17Nº 77432°re-scan + + diff --git a/fable/stack/plans/01-event-volume.mjs b/fable/stack/plans/01-event-volume.mjs new file mode 100644 index 0000000..cef7538 --- /dev/null +++ b/fable/stack/plans/01-event-volume.mjs @@ -0,0 +1,48 @@ +/* Candidate 01 — THE EVENT, SUSPENDED + The purest depth thesis: causality back-to-front. The pressure + disk glows deepest (CMYK-only when printed — it becomes a light + source), history sinks behind the trigger, the trigger blooms on + B-back and cuts sharp on B-front (real halation across 3 mm of + acrylic), the δ-curls lift off toward the eye, and the scanner's + grease pencil floats on the front glass where hands actually wrote. + Palette: near-mono traces + one earned ember accent on the disk + (magentarise with saturation crushed; disk hue is immune). */ +export default { + id: '01-event-volume', + title: 'The Event, Suspended', + seed: 'LAMBDA-2648', + size: 1400, + geometry: { thickness: 3, gap: 6 }, + params: { + palette: 'magentarise', saturation: 0.14, + diskHue: 0.055, diskSat: 0.88, // ember — the one colour + primaries: 18, burst: 0.8, vdecay: 4, + bgEvents: 7, bgIntensity: 0.5, cosmics: 8, sweepers: 5, + deltaRate: 0.9, deltaTight: 0.9, + shock: true, diskBubbles: true, diskSoften: 1.2, + shockIntensity: 0.9, diskPressure: 0.55, + shockSize: 0.34, shockY: 0.52, + annotate: 0.7, annoWidth: 0.5, reseau: 0.4, filmEdge: true, + artifacts: 0.5, instrument: 0.45, + }, + planes: [ + { sheet: 'A', face: 'back', label: 'the cause — disk body + chamber optics', + disk: true, boundary: true, optics: true, + params: { diskBubbles: false, diskPressure: 0.85, diskSoften: 1.6 } }, + { sheet: 'A', face: 'back', label: 'the cause — disk granular texture', + disk: true, + params: { diskBubbles: true }, fx: { opacity: 0.9 } }, + { sheet: 'A', face: 'front', label: 'history — older events sink', + tracks: { age: [0.25, 1] }, + fx: { blur: 1.6, opacity: 0.85 } }, + { sheet: 'B', face: 'back', label: 'halation — the trigger blooms behind itself', + tracks: { age: [0, 0.25], kinds: ['primary', 'vdecay'] }, + fx: { blur: 7, opacity: 0.5 } }, + { sheet: 'B', face: 'front', label: 'the event — sharp beaded cores', + tracks: { age: [0, 0.25], kinds: ['primary', 'vdecay'] } }, + { sheet: 'C', face: 'back', label: 'near scatter — δ-curls lift toward the eye', + tracks: { age: [0, 0.25], kinds: ['delta', 'cosmic', 'sweep'] } }, + { sheet: 'C', face: 'front', label: 'the hand — grease pencil on the glass', + media: true, fiducials: true, header: true, damage: true }, + ], +}; diff --git a/fable/stack/plans/02-palimpsest.mjs b/fable/stack/plans/02-palimpsest.mjs new file mode 100644 index 0000000..431bf21 --- /dev/null +++ b/fable/stack/plans/02-palimpsest.mjs @@ -0,0 +1,47 @@ +/* Candidate 02 — PALIMPSEST + Depth as pure time, full mono: the chamber ran all night and the + stack is the exposure record. Oldest events deepest; each plane + forward is a later moment; the current trigger sits on B-front; + its δ-debris is still in flight on C-back; the hand arrives last, + on the front glass — the only "now" that outlived the event. + No colour anywhere: transmitted light does all the work. */ +export default { + id: '02-palimpsest', + title: 'Palimpsest', + seed: 'KAON-0088', + size: 1400, + geometry: { thickness: 3, gap: 6 }, + params: { + palette: 'mono', + primaries: 16, burst: 0.75, vdecay: 3, + bgEvents: 11, bgIntensity: 0.75, cosmics: 10, sweepers: 6, + deltaRate: 0.85, + shock: true, diskBubbles: true, diskSoften: 1.0, + shockIntensity: 0.85, diskPressure: 0.4, + shockSize: 0.28, shockY: 0.55, + annotate: 0.5, annoWidth: 0.5, filmEdge: true, + artifacts: 0.6, instrument: 0.5, + }, + planes: [ + { sheet: 'A', face: 'back', label: 'deep time — oldest exposures + the chamber', + tracks: { age: [0.6, 1] }, boundary: true, optics: true, + fx: { blur: 1.4, opacity: 0.9 } }, + { sheet: 'A', face: 'front', label: 'earlier cycle', + tracks: { age: [0.35, 0.6] }, + fx: { blur: 0.8, opacity: 0.95 } }, + { sheet: 'B', face: 'back', label: 'moments ago', + tracks: { age: [0.12, 0.35] } }, + { sheet: 'B', face: 'back', label: 'the pressure wave — body + texture', + disk: true, + params: { diskBubbles: false, diskPressure: 0.7 } }, + { sheet: 'B', face: 'back', label: 'pressure-wave grain', + disk: true, + params: { diskBubbles: true }, fx: { opacity: 0.85 } }, + { sheet: 'B', face: 'front', label: 'NOW — the trigger, all kinds, sharp', + tracks: { age: [0, 0.12], kinds: ['primary', 'vdecay'] } }, + { sheet: 'C', face: 'back', label: 'still in flight — δ-debris of now', + tracks: { age: [0, 0.12], kinds: ['delta'] } }, + { sheet: 'C', face: 'front', label: 'after — the hand, the furniture, the wear', + media: true, fiducials: true, header: true, damage: true }, + ], +}; diff --git a/fable/stack/plans/03-interference.mjs b/fable/stack/plans/03-interference.mjs new file mode 100644 index 0000000..085c325 --- /dev/null +++ b/fable/stack/plans/03-interference.mjs @@ -0,0 +1,51 @@ +/* Candidate 03 — THE MEASUREMENT INTERFERES + The instrument becomes phenomenon: the réseau measuring grid is + printed TWICE — once deep (B-back, scaled 1.5%), once on the + front glass — so the act of measurement moirés against itself as + the viewer moves. The disk goes deepest with a full spectral + sunburst (iridescent striations, CMYK-only glow when printed): + the interference-pattern reference, arrived at from inside the + project's own vocabulary. Mono traces; the spectrum is earned by + the physics feature that IS interference. */ +export default { + id: '03-interference', + title: 'The Measurement Interferes', + seed: 'NEUTRINO-1973', + size: 1400, + geometry: { thickness: 3, gap: 6 }, + params: { + palette: 'mono', + primaries: 14, burst: 0.7, vdecay: 3, + bgEvents: 5, bgIntensity: 0.45, cosmics: 7, sweepers: 6, + deltaRate: 0.8, + shock: true, diskBubbles: false, diskSoften: 0.6, + shockIntensity: 0.95, diskPressure: 0.5, + diskSpectrum: 0.85, shockStriations: 1.0, + shockSize: 0.38, shockY: 0.5, shockX: 0, + annotate: 0.7, annoWidth: 0.5, reseau: 0.85, filmEdge: true, + artifacts: 0.45, instrument: 0.4, + }, + planes: [ + { sheet: 'A', face: 'back', label: 'the spectral disk — interference at the source', + disk: true, boundary: true, optics: true, + params: { diskSoften: 1.2, diskPressure: 0.8 } }, + { sheet: 'A', face: 'back', label: 'the disk — granular spray', + disk: true, + params: { diskBubbles: true }, fx: { opacity: 0.8 } }, + { sheet: 'A', face: 'front', label: 'the sea — sweepers, cosmics, history', + tracks: { age: [0.2, 1] }, + fx: { blur: 1.4, opacity: 0.85 } }, + { sheet: 'B', face: 'back', label: 'deep réseau — scaled +1.5%, moirés against the front grid', + media: { reseau: true }, + fx: { scale: 1.015 } }, + { sheet: 'B', face: 'back', label: 'halation — the trigger blooms', + tracks: { age: [0, 0.2], kinds: ['primary', 'vdecay'] }, + fx: { blur: 5, opacity: 0.55 } }, + { sheet: 'B', face: 'front', label: 'the event — sharp', + tracks: { age: [0, 0.2], kinds: ['primary', 'vdecay'] } }, + { sheet: 'C', face: 'back', label: 'δ-curls in flight', + tracks: { age: [0, 0.2], kinds: ['delta'] } }, + { sheet: 'C', face: 'front', label: 'front réseau + the hand — measurement on glass', + media: true, fiducials: true, header: true, damage: true }, + ], +}; diff --git a/fable/stack/simulator.html b/fable/stack/simulator.html new file mode 100644 index 0000000..e6dddff --- /dev/null +++ b/fable/stack/simulator.html @@ -0,0 +1,129 @@ + + + + +stack simulator + + + +
+

+
+
+ + +
+ + + + + +
+
+
+
move mouse to parallax · ?c=<candidate-id>
+ + + diff --git a/src/render/stack.js b/src/render/stack.js new file mode 100644 index 0000000..3f84dd6 --- /dev/null +++ b/src/render/stack.js @@ -0,0 +1,112 @@ +/* ============================================================ + stack.js — split one scene across the printable planes of a + physical acrylic stack (front + back of several sheets, air + gaps, backlit). The scene model's per-track z and age stop + being opacity tricks and become actual millimetres. + + A PLAN describes the piece: + { id, seed, size, geometry:{thickness,gap}, params:{...}, + planes: [ { sheet:'A', face:'back', label, + tracks: false | true | {kinds, z:[lo,hi], age:[lo,hi]}, + disk, boundary, optics, damage, // flags + media: false | true | {reseau,grease,film,splice}, + fiducials, header, // flags + params: {...per-plane overrides}, + fx: { blur, opacity, scale } }, ... ] } + + The scene is generated ONCE from the merged params; each plane + renders a filtered copy through the untouched renderSVG with + transparentPaper (the backlight is the paper). Bloom/core face + pairs must use IDENTICAL track selectors and equal density/size + so their salted bubble draws match — same filtered list, same + positions; only fx differ. + + Per-plane depth/aging default LOW: the physical stack now + carries depth; double-encoding it as opacity fights the medium. + ============================================================ */ +import { generateScene } from '../scene/scene.js'; +import { paramsFromSeed } from '../scene/params.js'; +import { renderSVG } from './svgVector.js'; + +const inRange = (v, r) => !r || (v >= r[0] && v <= r[1]); + +function filterTracks(tracks, sel) { + if (!sel) return []; + if (sel === true) return tracks; + return tracks.filter(t => + (!sel.kinds || sel.kinds.includes(t.kind)) && + inRange(t.z ?? 0, sel.z) && + inRange(t.age ?? 0, sel.age)); +} + +function filterMedia(media, sel) { + if (!media || !sel) return null; + if (sel === true) return media; + return { + reseau: sel.reseau ? media.reseau : null, + grease: sel.grease ? media.grease : [], + film: sel.film ? media.film : null, + splice: sel.splice ? media.splice : null, + }; +} + +/* wrap a rendered plane in optional blur / opacity / centre-scale + (blur is in u = px per 1000; scale is for deliberate moiré offsets) */ +function applyFx(svg, fx, size) { + const { blur = 0, opacity = 1, scale = 1 } = fx || {}; + if (!blur && opacity === 1 && scale === 1) return svg; + const u = size / 1000, c = size / 2; + const filter = blur + ? `` + : ''; + const g = `${filter}`; + return svg.replace(/(]*>)/, `$1\n${g}`).replace(/<\/svg>\s*$/, '\n'); +} + +export function renderStack(plan) { + const base = { ...paramsFromSeed(plan.seed), ...(plan.params || {}), invert: true }; + const scene = generateScene(base); + const size = plan.size || 1400; + + const planes = plan.planes.map((plane) => { + const emit = []; + if (plane.disk) emit.push('disk'); + if (plane.tracks || plane.optics || plane.damage || plane.boundary) emit.push('bubble'); + if (plane.media || plane.fiducials || plane.header) emit.push('fiduciaries'); + + const ps = { + ...base, + transparentPaper: true, vign: 0, + showBoundary: !!plane.boundary, + showFiducials: !!plane.fiducials, + showHeader: !!plane.header, + depth: 0.15, aging: 0.2, // the stack is the depth model now + emit, + ...(plane.params || {}), + }; + const sc = { + ...scene, + tracks: filterTracks(scene.tracks, plane.tracks ?? false), + shock: plane.disk ? scene.shock : null, + instrument: plane.optics ? scene.instrument : null, + artifacts: plane.damage ? scene.artifacts : null, + media: filterMedia(scene.media, plane.media), + }; + const svg = applyFx(renderSVG(sc, ps, size), plane.fx, size); + return { sheet: plane.sheet, face: plane.face, label: plane.label || '', svg }; + }); + + return { plan, scene, planes }; +} + +/* physical z (mm) of each plane from stack geometry; sheet order = first + appearance in the plan (back of the stack first) */ +export function planeDepths(plan) { + const t = plan.geometry?.thickness ?? 3; + const g = plan.geometry?.gap ?? 6; + const sheets = [...new Set(plan.planes.map(p => p.sheet))]; + return plan.planes.map(p => + sheets.indexOf(p.sheet) * (t + g) + (p.face === 'front' ? t : 0)); +} diff --git a/tools/stack-render.mjs b/tools/stack-render.mjs new file mode 100644 index 0000000..d5ee067 --- /dev/null +++ b/tools/stack-render.mjs @@ -0,0 +1,33 @@ +#!/usr/bin/env node +/* stack-render.mjs — render an acrylic-stack plan to per-plane SVGs. + usage: node tools/stack-render.mjs [outDir] + Writes plane-NN--.svg + manifest.json for the simulator + (fable/stack/simulator.html?c=). */ +import { mkdirSync, writeFileSync } from 'node:fs'; +import { resolve } from 'node:path'; +import { pathToFileURL } from 'node:url'; +import { renderStack, planeDepths } from '../src/render/stack.js'; + +const [planPath, outArg] = process.argv.slice(2); +if (!planPath) { console.error('usage: node tools/stack-render.mjs [outDir]'); process.exit(1); } + +const plan = (await import(pathToFileURL(resolve(planPath)).href)).default; +const outDir = resolve(outArg || `fable/stack/candidates/${plan.id}`); +mkdirSync(outDir, { recursive: true }); + +const { planes } = renderStack(plan); +const zmm = planeDepths(plan); + +const manifest = { + id: plan.id, title: plan.title || plan.id, seed: plan.seed, + geometry: { thickness: plan.geometry?.thickness ?? 3, gap: plan.geometry?.gap ?? 6 }, + planes: [], +}; +planes.forEach((p, i) => { + const file = `plane-${String(i).padStart(2, '0')}-${p.sheet}-${p.face}.svg`; + writeFileSync(`${outDir}/${file}`, p.svg); + manifest.planes.push({ file, sheet: p.sheet, face: p.face, zmm: zmm[i], label: p.label }); + console.log(` ${file} z=${zmm[i]}mm ${p.label}`); +}); +writeFileSync(`${outDir}/manifest.json`, JSON.stringify(manifest, null, 2)); +console.log(`→ ${outDir} (${planes.length} planes)`);