PRE-REFINEMENTS
99
fable/stack/SPEC.md
Normal file
@@ -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 <plan> per-plane SVGs + manifest.json
|
||||||
|
→ fable/stack/simulator.html?c=<id> 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
|
||||||
BIN
fable/stack/candidates/01-event-volume/composite-lit.png
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
60
fable/stack/candidates/01-event-volume/manifest.json
Normal file
@@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
38
fable/stack/candidates/01-event-volume/plane-00-A-back.svg
Normal file
|
After Width: | Height: | Size: 142 KiB |
42
fable/stack/candidates/01-event-volume/plane-01-A-back.svg
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
157
fable/stack/candidates/01-event-volume/plane-02-A-front.svg
Normal file
|
After Width: | Height: | Size: 641 KiB |
106
fable/stack/candidates/01-event-volume/plane-03-B-back.svg
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
105
fable/stack/candidates/01-event-volume/plane-04-B-front.svg
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
207
fable/stack/candidates/01-event-volume/plane-05-C-back.svg
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
41
fable/stack/candidates/01-event-volume/plane-06-C-front.svg
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
fable/stack/candidates/02-palimpsest/composite-lit.png
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
67
fable/stack/candidates/02-palimpsest/manifest.json
Normal file
@@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
49
fable/stack/candidates/02-palimpsest/plane-00-A-back.svg
Normal file
|
After Width: | Height: | Size: 2.7 MiB |
50
fable/stack/candidates/02-palimpsest/plane-01-A-front.svg
Normal file
|
After Width: | Height: | Size: 3.1 MiB |
38
fable/stack/candidates/02-palimpsest/plane-02-B-back.svg
Normal file
|
After Width: | Height: | Size: 474 KiB |
34
fable/stack/candidates/02-palimpsest/plane-03-B-back.svg
Normal file
|
After Width: | Height: | Size: 146 KiB |
42
fable/stack/candidates/02-palimpsest/plane-04-B-back.svg
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
40
fable/stack/candidates/02-palimpsest/plane-05-B-front.svg
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
36
fable/stack/candidates/02-palimpsest/plane-06-C-back.svg
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
41
fable/stack/candidates/02-palimpsest/plane-07-C-front.svg
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" width="1400" height="1400" viewBox="0 0 1400 1400">
|
||||||
|
<metadata>Bubble Chamber · seed=KAON-0088 · hash=1e2f9c933ac58 · palette=mono</metadata>
|
||||||
|
<defs>
|
||||||
|
<filter id="soften" x="-20%" y="-20%" width="140%" height="140%"><feGaussianBlur stdDeviation="1.40"/></filter>
|
||||||
|
<radialGradient id="paper" cx="50%" cy="42%" r="72%">
|
||||||
|
<stop offset="0%" stop-color="#e2dbc7"/><stop offset="100%" stop-color="#b3aa92"/>
|
||||||
|
</radialGradient>
|
||||||
|
<radialGradient id="bub-28_24_20" cx="50%" cy="50%" r="50%"><stop offset="0%" stop-color="#1c1814" stop-opacity="0.949"/><stop offset="48%" stop-color="#1c1814" stop-opacity="0.798"/><stop offset="100%" stop-color="#1c1814" stop-opacity="0.000"/></radialGradient>
|
||||||
|
<radialGradient id="shockcore" cx="50%" cy="50%" r="50%">
|
||||||
|
<stop offset="0%" stop-color="#16120f" stop-opacity="0.68"/>
|
||||||
|
<stop offset="28%" stop-color="#16120f" stop-opacity="0.45"/>
|
||||||
|
<stop offset="62%" stop-color="#1c1814" stop-opacity="0.28"/>
|
||||||
|
<stop offset="100%" stop-color="#1c1814" stop-opacity="0"/>
|
||||||
|
</radialGradient>
|
||||||
|
<radialGradient id="shockstain" cx="50%" cy="50%" r="50%">
|
||||||
|
<stop offset="0%" stop-color="#1c1814" stop-opacity="0.9"/>
|
||||||
|
<stop offset="100%" stop-color="#1c1814" stop-opacity="0"/>
|
||||||
|
</radialGradient>
|
||||||
|
<radialGradient id="shockclean" cx="50%" cy="50%" r="50%">
|
||||||
|
<stop offset="0%" stop-color="#cfc8b4" stop-opacity="0.9"/>
|
||||||
|
<stop offset="100%" stop-color="#cfc8b4" stop-opacity="0"/>
|
||||||
|
</radialGradient>
|
||||||
|
<radialGradient id="vign" cx="50%" cy="50%" r="72%">
|
||||||
|
<stop offset="30%" stop-color="#382e20" stop-opacity="0"/>
|
||||||
|
<stop offset="100%" stop-color="#382e20" stop-opacity="0"/>
|
||||||
|
</radialGradient>
|
||||||
|
</defs>
|
||||||
|
<g id="damage" inkscape:groupmode="layer" inkscape:label="Plate damage" style="display:inline">
|
||||||
|
<g stroke="#1c1814" fill="none" stroke-linecap="round"><circle cx="956.49" cy="1118.27" r="62.0" stroke-opacity="0.101" stroke-width="0.92"/><line x1="-165.08" y1="217.16" x2="759.53" y2="948.27" stroke-opacity="0.103" stroke-width="0.95"/><line x1="782.39" y1="691.43" x2="1321.41" y2="1717.56" stroke-opacity="0.123" stroke-width="0.73"/><line x1="1005.04" y1="814.99" x2="1239.92" y2="1410.61" stroke-opacity="0.221" stroke-width="0.43"/><line x1="1118.24" y1="1430.05" x2="968.95" y2="148.41" stroke-opacity="0.187" stroke-width="1.02"/><line x1="1454.37" y1="1651.97" x2="790.12" y2="913.80" stroke-opacity="0.115" stroke-width="0.66"/><path d="M 444.99 581.16 L 449.31 555.71 L 448.81 529.90 L 440.73 505.39 L 434.74 480.28 L 427.48 455.50 L 429.69 429.79 L 433.86 404.31 L 441.00 379.50 L 448.82 354.90 L 461.74 332.55 L 473.24 309.44 L 482.63 285.40 L 499.68 266.01 L 510.32 242.49 L 533.56 231.25 L 553.94 215.40 L 567.43 193.40 L 587.38 177.02 L 609.36 190.56 L 624.96 211.13 L 630.32 236.38 L 629.93 262.19 L 623.65 287.23 L 601.97 301.24 L 577.42 309.20 L 552.27 315.04 L 531.88 330.87 L 513.93 349.42" stroke-opacity="0.249" stroke-width="0.91"/><path d="M 242.23 622.29 L 233.10 602.86 L 226.14 582.55 L 230.53 561.54 L 239.13 541.86 L 251.87 524.58 L 268.17 510.62 L 286.60 499.61 L 304.38 487.57 L 323.24 477.31 L 340.81 464.98 L 362.20 463.17 L 382.47 456.11 L 402.11 447.45 L 407.85 426.76 L 415.83 406.83 L 434.65 396.52 L 456.12 396.32 L 477.31 392.85 L 494.31 405.95 L 514.34 413.69 L 535.57 416.86 L 555.33 425.25 L 575.21 433.35 L 595.70 439.77 L 616.79 443.78 L 637.13 450.64 L 646.53 469.94 L 660.80 485.97 L 672.51 503.97 L 685.32 521.19" stroke-opacity="0.225" stroke-width="1.05"/></g><g fill="#1c1814"><circle cx="467.63" cy="542.33" r="1.67" fill-opacity="0.310"/><circle cx="479.15" cy="544.76" r="0.88" fill-opacity="0.331"/><circle cx="486.58" cy="532.89" r="2.01" fill-opacity="0.234"/><circle cx="508.05" cy="536.46" r="0.66" fill-opacity="0.276"/><circle cx="463.94" cy="543.84" r="0.73" fill-opacity="0.300"/><circle cx="462.97" cy="557.88" r="0.94" fill-opacity="0.180"/><circle cx="568.21" cy="1197.08" r="1.48" fill-opacity="0.193"/><circle cx="424.16" cy="368.66" r="1.02" fill-opacity="0.383"/><circle cx="390.41" cy="326.62" r="1.78" fill-opacity="0.215"/><circle cx="460.65" cy="321.21" r="1.87" fill-opacity="0.156"/><circle cx="415.35" cy="339.99" r="1.80" fill-opacity="0.409"/><circle cx="481.74" cy="122.14" r="1.72" fill-opacity="0.250"/><circle cx="439.98" cy="124.51" r="0.81" fill-opacity="0.219"/><circle cx="447.47" cy="116.60" r="2.12" fill-opacity="0.403"/><circle cx="456.70" cy="94.46" r="1.44" fill-opacity="0.327"/><circle cx="437.56" cy="62.94" r="0.81" fill-opacity="0.365"/><circle cx="831.20" cy="888.38" r="1.27" fill-opacity="0.416"/><circle cx="874.34" cy="932.63" r="0.50" fill-opacity="0.307"/><circle cx="874.33" cy="932.27" r="1.70" fill-opacity="0.240"/><circle cx="873.22" cy="957.85" r="0.70" fill-opacity="0.203"/><circle cx="896.09" cy="931.92" r="1.38" fill-opacity="0.192"/><circle cx="892.40" cy="902.63" r="2.21" fill-opacity="0.230"/><circle cx="252.16" cy="868.61" r="1.79" fill-opacity="0.373"/><circle cx="265.02" cy="827.58" r="2.24" fill-opacity="0.399"/><circle cx="307.77" cy="870.71" r="1.17" fill-opacity="0.258"/><circle cx="1091.63" cy="1262.63" r="0.52" fill-opacity="0.306"/><circle cx="1136.10" cy="1271.88" r="1.46" fill-opacity="0.282"/><circle cx="1102.95" cy="1268.03" r="0.93" fill-opacity="0.363"/><circle cx="917.63" cy="303.65" r="1.85" fill-opacity="0.301"/><circle cx="921.65" cy="241.66" r="1.18" fill-opacity="0.192"/><circle cx="912.80" cy="326.36" r="0.54" fill-opacity="0.312"/><circle cx="908.94" cy="264.09" r="0.78" fill-opacity="0.440"/><circle cx="1154.45" cy="698.26" r="2.08" fill-opacity="0.429"/><circle cx="1183.48" cy="680.93" r="0.86" fill-opacity="0.436"/><circle cx="1210.26" cy="687.58" r="0.67" fill-opacity="0.418"/><circle cx="1235.14" cy="721.34" r="0.68" fill-opacity="0.152"/><circle cx="895.28" cy="85.22" r="0.75" fill-opacity="0.242"/><circle cx="916.78" cy="110.01" r="1.74" fill-opacity="0.385"/><circle cx="865.80" cy="120.73" r="1.15" fill-opacity="0.229"/><circle cx="916.25" cy="90.18" r="2.02" fill-opacity="0.359"/><circle cx="901.22" cy="70.57" r="1.27" fill-opacity="0.170"/><circle cx="928.37" cy="103.64" r="1.82" fill-opacity="0.235"/><circle cx="862.76" cy="104.59" r="0.50" fill-opacity="0.167"/></g>
|
||||||
|
</g>
|
||||||
|
<g id="fiducials" inkscape:groupmode="layer" inkscape:label="Fiducials" style="display:inline">
|
||||||
|
<g stroke="#1c1814" stroke-opacity="0.55" stroke-width="1.68"><line x1="104.30000000000001" y1="116.9" x2="129.5" y2="116.9"/><line x1="116.9" y1="104.30000000000001" x2="116.9" y2="129.5"/><line x1="1270.5" y1="116.9" x2="1295.6999999999998" y2="116.9"/><line x1="1283.1" y1="104.30000000000001" x2="1283.1" y2="129.5"/><line x1="104.30000000000001" y1="1283.1" x2="129.5" y2="1283.1"/><line x1="116.9" y1="1270.5" x2="116.9" y2="1295.6999999999998"/><line x1="1270.5" y1="1283.1" x2="1295.6999999999998" y2="1283.1"/><line x1="1283.1" y1="1270.5" x2="1283.1" y2="1295.6999999999998"/><line x1="687.4" y1="116.9" x2="712.6" y2="116.9"/><line x1="700" y1="104.30000000000001" x2="700" y2="129.5"/><line x1="104.30000000000001" y1="700" x2="129.5" y2="700"/><line x1="116.9" y1="687.4" x2="116.9" y2="712.6"/><line x1="1270.5" y1="700" x2="1295.6999999999998" y2="700"/><line x1="1283.1" y1="687.4" x2="1283.1" y2="712.6"/></g>
|
||||||
|
</g>
|
||||||
|
<g id="header" inkscape:groupmode="layer" inkscape:label="Archival header" style="display:inline">
|
||||||
|
<g fill="#1c1814" font-family="'JetBrains Mono', monospace"><text x="36" y="52" font-size="15" fill-opacity="0.62">GARGAMELLE · CERN</text><text x="36" y="74" font-size="13" fill-opacity="0.5">SEED KAON-0088</text><text x="1364" y="1345" font-size="14" text-anchor="end" fill-opacity="0.58">PLATE 163</text><text x="1364" y="1364" font-size="14" text-anchor="end" fill-opacity="0.58">EXPOSED 1971.06.13</text></g>
|
||||||
|
</g>
|
||||||
|
<g id="media" inkscape:groupmode="layer" inkscape:label="Media & hand" style="display:inline">
|
||||||
|
<rect x="38.0" y="38.0" width="1324.0" height="1324.0" fill="none" stroke="#1c1814" stroke-opacity="0.5" stroke-width="2.8"/><rect x="14.7" y="65.5" width="19.2" height="34.3" rx="5.6" fill="#faf8f0" fill-opacity="0.7" stroke="#1c1814" stroke-opacity="0.4" stroke-width="1.4"/><rect x="1366.1" y="65.5" width="19.2" height="34.3" rx="5.6" fill="#faf8f0" fill-opacity="0.7" stroke="#1c1814" stroke-opacity="0.4" stroke-width="1.4"/><rect x="14.7" y="219.8" width="19.2" height="34.3" rx="5.6" fill="#faf8f0" fill-opacity="0.7" stroke="#1c1814" stroke-opacity="0.4" stroke-width="1.4"/><rect x="1366.1" y="219.8" width="19.2" height="34.3" rx="5.6" fill="#faf8f0" fill-opacity="0.7" stroke="#1c1814" stroke-opacity="0.4" stroke-width="1.4"/><rect x="14.7" y="374.2" width="19.2" height="34.3" rx="5.6" fill="#faf8f0" fill-opacity="0.7" stroke="#1c1814" stroke-opacity="0.4" stroke-width="1.4"/><rect x="1366.1" y="374.2" width="19.2" height="34.3" rx="5.6" fill="#faf8f0" fill-opacity="0.7" stroke="#1c1814" stroke-opacity="0.4" stroke-width="1.4"/><rect x="14.7" y="528.5" width="19.2" height="34.3" rx="5.6" fill="#faf8f0" fill-opacity="0.7" stroke="#1c1814" stroke-opacity="0.4" stroke-width="1.4"/><rect x="1366.1" y="528.5" width="19.2" height="34.3" rx="5.6" fill="#faf8f0" fill-opacity="0.7" stroke="#1c1814" stroke-opacity="0.4" stroke-width="1.4"/><rect x="14.7" y="682.9" width="19.2" height="34.3" rx="5.6" fill="#faf8f0" fill-opacity="0.7" stroke="#1c1814" stroke-opacity="0.4" stroke-width="1.4"/><rect x="1366.1" y="682.9" width="19.2" height="34.3" rx="5.6" fill="#faf8f0" fill-opacity="0.7" stroke="#1c1814" stroke-opacity="0.4" stroke-width="1.4"/><rect x="14.7" y="837.2" width="19.2" height="34.3" rx="5.6" fill="#faf8f0" fill-opacity="0.7" stroke="#1c1814" stroke-opacity="0.4" stroke-width="1.4"/><rect x="1366.1" y="837.2" width="19.2" height="34.3" rx="5.6" fill="#faf8f0" fill-opacity="0.7" stroke="#1c1814" stroke-opacity="0.4" stroke-width="1.4"/><rect x="14.7" y="991.6" width="19.2" height="34.3" rx="5.6" fill="#faf8f0" fill-opacity="0.7" stroke="#1c1814" stroke-opacity="0.4" stroke-width="1.4"/><rect x="1366.1" y="991.6" width="19.2" height="34.3" rx="5.6" fill="#faf8f0" fill-opacity="0.7" stroke="#1c1814" stroke-opacity="0.4" stroke-width="1.4"/><rect x="14.7" y="1145.9" width="19.2" height="34.3" rx="5.6" fill="#faf8f0" fill-opacity="0.7" stroke="#1c1814" stroke-opacity="0.4" stroke-width="1.4"/><rect x="1366.1" y="1145.9" width="19.2" height="34.3" rx="5.6" fill="#faf8f0" fill-opacity="0.7" stroke="#1c1814" stroke-opacity="0.4" stroke-width="1.4"/><rect x="14.7" y="1300.3" width="19.2" height="34.3" rx="5.6" fill="#faf8f0" fill-opacity="0.7" stroke="#1c1814" stroke-opacity="0.4" stroke-width="1.4"/><rect x="1366.1" y="1300.3" width="19.2" height="34.3" rx="5.6" fill="#faf8f0" fill-opacity="0.7" stroke="#1c1814" stroke-opacity="0.4" stroke-width="1.4"/><g fill="#1c1814" font-family="'JetBrains Mono', monospace"><text x="58.6" y="700.0" font-size="10" fill-opacity="0.45" text-anchor="middle" transform="rotate(-90 58.6 700.0)">KODAK SAFETY FILM</text><text x="82.6" y="1248.8" font-size="13" fill-opacity="0.5">ROLL 627</text><text x="82.6" y="1265.6" font-size="13" fill-opacity="0.5">FRAME 163</text><text x="82.6" y="1282.4" font-size="13" fill-opacity="0.5">1971.06.13</text></g><g stroke="#9c1e1e" fill="#9c1e1e" stroke-linecap="round" stroke-linejoin="round"><path d="M 1142.9 886.3 L 1140.6 890.2 L 1136.5 891.9 L 1133.9 896.9 L 1130.0 900.7 L 1125.0 901.9 L 1120.0 898.3 L 1115.0 900.6 L 1111.0 896.4 L 1106.5 895.4 L 1101.5 894.9 L 1097.6 892.0 L 1094.0 888.7 L 1091.2 884.8 L 1089.5 880.4 L 1088.1 875.9 L 1089.5 870.9 L 1086.8 867.0 L 1085.3 862.5 L 1087.2 858.2 L 1082.1 852.1 L 1088.7 849.3 L 1088.1 843.6 L 1088.7 837.7 L 1092.5 834.0 L 1099.5 834.8 L 1104.2 833.8 L 1106.8 828.3 L 1110.8 823.4 L 1116.5 827.6 L 1121.2 827.1 L 1126.2 826.2 L 1131.6 825.5 L 1133.6 833.3 L 1137.2 835.6 L 1140.9 837.9 L 1145.8 839.3 L 1150.6 841.7 L 1151.3 846.9 L 1154.8 850.7 L 1155.0 855.7 L 1157.0 860.4 L 1154.1 865.2 L 1152.5 869.6 L 1151.5 874.0 L 1152.6 879.7 L 1144.9 880.4 L 1144.8 886.0 L 1143.6 891.7 L 1134.9 887.5 L 1134.5 896.2 L 1128.8 894.5 L 1124.9 898.1" fill="none" stroke-opacity="0.85" stroke-width="1.68"/><path d="M 875.2 624.7 L 899.7 640.5 L 933.8 663.3 L 965.0 691.7 L 995.0 710.9 L 1037.6 729.5 L 1071.6 748.7 L 1097.6 777.1 L 1133.8 799.8 L 1166.6 822.1" fill="none" stroke-opacity="0.85" stroke-width="1.40"/><path d="M 1165.1 822.7 L 1145.0 818.6 M 1165.1 822.7 L 1154.1 805.4" fill="none" stroke-opacity="0.85" stroke-width="1.40"/><text x="1157.9" y="826.3" font-size="31" font-family="'Bradley Hand','Segoe Script','Comic Sans MS',cursive" fill-opacity="0.9" transform="rotate(-5.6 1157.9 826.3)">Nº 163</text></g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 13 KiB |
BIN
fable/stack/candidates/03-interference/composite-lit.png
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
67
fable/stack/candidates/03-interference/manifest.json
Normal file
@@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
38
fable/stack/candidates/03-interference/plane-00-A-back.svg
Normal file
|
After Width: | Height: | Size: 147 KiB |
42
fable/stack/candidates/03-interference/plane-01-A-back.svg
Normal file
|
After Width: | Height: | Size: 2.1 MiB |
44
fable/stack/candidates/03-interference/plane-02-A-front.svg
Normal file
|
After Width: | Height: | Size: 531 KiB |
33
fable/stack/candidates/03-interference/plane-03-B-back.svg
Normal file
|
After Width: | Height: | Size: 12 KiB |
42
fable/stack/candidates/03-interference/plane-04-B-back.svg
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
41
fable/stack/candidates/03-interference/plane-05-B-front.svg
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
36
fable/stack/candidates/03-interference/plane-06-C-back.svg
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
41
fable/stack/candidates/03-interference/plane-07-C-front.svg
Normal file
|
After Width: | Height: | Size: 23 KiB |
48
fable/stack/plans/01-event-volume.mjs
Normal file
@@ -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 },
|
||||||
|
],
|
||||||
|
};
|
||||||
47
fable/stack/plans/02-palimpsest.mjs
Normal file
@@ -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 },
|
||||||
|
],
|
||||||
|
};
|
||||||
51
fable/stack/plans/03-interference.mjs
Normal file
@@ -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 },
|
||||||
|
],
|
||||||
|
};
|
||||||
129
fable/stack/simulator.html
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>stack simulator</title>
|
||||||
|
<style>
|
||||||
|
:root { color-scheme: dark; }
|
||||||
|
* { box-sizing: border-box; margin: 0; }
|
||||||
|
body { background:#0c0b09; color:#b8b0a0; font:12px/1.5 'JetBrains Mono',ui-monospace,monospace;
|
||||||
|
min-height:100vh; display:flex; }
|
||||||
|
#panel { width:250px; padding:16px; border-right:1px solid #26221c; flex-shrink:0; }
|
||||||
|
#panel h1 { font-size:13px; color:#e0d8c8; font-weight:600; margin-bottom:2px; }
|
||||||
|
#panel .sub { color:#6d6558; margin-bottom:14px; }
|
||||||
|
#panel label { display:block; margin:10px 0 2px; color:#8d8574; }
|
||||||
|
#panel input[type=range] { width:100%; accent-color:#c9803a; }
|
||||||
|
.plane-row { display:flex; gap:6px; align-items:baseline; margin:3px 0; cursor:pointer; }
|
||||||
|
.plane-row .z { color:#5d5548; width:44px; flex-shrink:0; }
|
||||||
|
.plane-row.off { opacity:0.35; }
|
||||||
|
button { background:#1c1914; color:#c9b98c; border:1px solid #3a342a; padding:5px 10px;
|
||||||
|
font:inherit; cursor:pointer; margin:2px 2px 0 0; }
|
||||||
|
button.on { background:#c9803a; color:#14100a; border-color:#c9803a; }
|
||||||
|
#stagewrap { flex:1; display:flex; align-items:center; justify-content:center;
|
||||||
|
perspective:1400px; overflow:hidden; }
|
||||||
|
#stage { position:relative; transform-style:preserve-3d; }
|
||||||
|
#stage img { position:absolute; left:0; top:0; width:100%; height:100%; user-select:none;
|
||||||
|
-webkit-user-drag:none; }
|
||||||
|
/* the light table — painted on the WRAP, not the stage: with preserve-3d a
|
||||||
|
stage background sits at z=0 and would occlude planes behind it */
|
||||||
|
#stagewrap.lit #light { background:radial-gradient(circle at 50% 46%, #fffdf4 0%, #f4ecd8 55%, #d8cdb2 100%); }
|
||||||
|
#stagewrap.unlit #light{ background:#b9b2a2; }
|
||||||
|
#stagewrap.unlit #stage img { filter:brightness(0.94) contrast(0.96); }
|
||||||
|
#light { position:absolute; inset:0; transform:translateZ(-400px) scale(1.6); }
|
||||||
|
#hint { position:fixed; right:14px; bottom:10px; color:#4d4639; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="panel">
|
||||||
|
<h1 id="title">—</h1>
|
||||||
|
<div class="sub" id="seed">—</div>
|
||||||
|
<div>
|
||||||
|
<button id="bl" class="on">backlit</button>
|
||||||
|
<button id="spin">auto-orbit</button>
|
||||||
|
</div>
|
||||||
|
<label>depth exaggeration <span id="zv"></span></label>
|
||||||
|
<input type="range" id="zscale" min="0" max="14" step="0.5" value="5">
|
||||||
|
<label>tilt range <span id="tv"></span></label>
|
||||||
|
<input type="range" id="tilt" min="0" max="24" step="1" value="10">
|
||||||
|
<label style="margin-top:14px">planes (click to toggle)</label>
|
||||||
|
<div id="planes"></div>
|
||||||
|
</div>
|
||||||
|
<div id="stagewrap" class="lit"><div id="stage"><div id="light"></div></div></div>
|
||||||
|
<div id="hint">move mouse to parallax · ?c=<candidate-id></div>
|
||||||
|
<script>
|
||||||
|
const q = new URLSearchParams(location.search);
|
||||||
|
const cid = q.get('c') || '01-event-volume';
|
||||||
|
const stage = document.getElementById('stage');
|
||||||
|
const SZ = Math.min(innerHeight, innerWidth - 250) - 40;
|
||||||
|
stage.style.width = stage.style.height = SZ + 'px';
|
||||||
|
|
||||||
|
let planes = [], rx = 0, ry = 0, zs = 5, spin = false, t0 = performance.now();
|
||||||
|
|
||||||
|
fetch(`candidates/${cid}/manifest.json`).then(r => r.json()).then(m => {
|
||||||
|
document.getElementById('title').textContent = m.title;
|
||||||
|
document.getElementById('seed').textContent = `seed ${m.seed} · ${m.planes.length} planes`;
|
||||||
|
const list = document.getElementById('planes');
|
||||||
|
m.planes.forEach((p, i) => {
|
||||||
|
const img = document.createElement('img');
|
||||||
|
img.src = `candidates/${cid}/${p.file}`;
|
||||||
|
stage.appendChild(img);
|
||||||
|
const row = document.createElement('div');
|
||||||
|
row.className = 'plane-row';
|
||||||
|
row.innerHTML = `<span class="z">${p.zmm}mm</span><span>${p.sheet}-${p.face} · ${p.label}</span>`;
|
||||||
|
row.onclick = () => { img.style.display = img.style.display === 'none' ? '' : 'none';
|
||||||
|
row.classList.toggle('off'); };
|
||||||
|
list.appendChild(row);
|
||||||
|
planes.push({ img, zmm: p.zmm });
|
||||||
|
});
|
||||||
|
layout();
|
||||||
|
});
|
||||||
|
|
||||||
|
function layout() {
|
||||||
|
const mid = planes.length ? planes.reduce((s, p) => s + p.zmm, 0) / planes.length : 0;
|
||||||
|
for (const p of planes)
|
||||||
|
p.img.style.transform = `translateZ(${(p.zmm - mid) * zs}px)`;
|
||||||
|
stage.style.transform = `rotateX(${rx}deg) rotateY(${ry}deg)`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const zEl = document.getElementById('zscale'), tEl = document.getElementById('tilt');
|
||||||
|
const zv = document.getElementById('zv'), tv = document.getElementById('tv');
|
||||||
|
function syncLabels(){ zv.textContent = `×${zEl.value}`; tv.textContent = `±${tEl.value}°`; }
|
||||||
|
syncLabels();
|
||||||
|
zEl.oninput = () => { zs = +zEl.value; syncLabels(); layout(); };
|
||||||
|
tEl.oninput = syncLabels;
|
||||||
|
|
||||||
|
const staticMode = q.get('static') !== null;
|
||||||
|
addEventListener('mousemove', e => {
|
||||||
|
if (spin || staticMode) return;
|
||||||
|
const T = +tEl.value;
|
||||||
|
ry = ((e.clientX / innerWidth) - 0.5) * 2 * T;
|
||||||
|
rx = -((e.clientY / innerHeight) - 0.5) * 2 * T;
|
||||||
|
layout();
|
||||||
|
});
|
||||||
|
document.getElementById('bl').onclick = function () {
|
||||||
|
const wrap = document.getElementById('stagewrap');
|
||||||
|
const lit = wrap.classList.toggle('lit');
|
||||||
|
wrap.classList.toggle('unlit', !lit);
|
||||||
|
this.classList.toggle('on', lit);
|
||||||
|
};
|
||||||
|
document.getElementById('spin').onclick = function () {
|
||||||
|
spin = !spin; this.classList.toggle('on', spin);
|
||||||
|
if (spin) requestAnimationFrame(orbit);
|
||||||
|
};
|
||||||
|
function orbit(t) {
|
||||||
|
if (!spin) return;
|
||||||
|
const T = +tEl.value;
|
||||||
|
ry = Math.sin((t - t0) / 1400) * T;
|
||||||
|
rx = Math.cos((t - t0) / 2300) * T * 0.4;
|
||||||
|
layout();
|
||||||
|
requestAnimationFrame(orbit);
|
||||||
|
}
|
||||||
|
// static pose for headless screenshots: ?static=ry,rx
|
||||||
|
if (staticMode) {
|
||||||
|
const [a, b] = (q.get('static') || '').split(',').map(Number);
|
||||||
|
ry = isFinite(a) ? a : 8; rx = isFinite(b) ? b : -4;
|
||||||
|
setTimeout(layout, 300);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
112
src/render/stack.js
Normal file
@@ -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
|
||||||
|
? `<filter id="planefx" x="-15%" y="-15%" width="130%" height="130%"><feGaussianBlur stdDeviation="${(blur * u).toFixed(2)}"/></filter>`
|
||||||
|
: '';
|
||||||
|
const g = `<defs>${filter}</defs><g${blur ? ' filter="url(#planefx)"' : ''}`
|
||||||
|
+ `${opacity !== 1 ? ` opacity="${opacity}"` : ''}`
|
||||||
|
+ `${scale !== 1 ? ` transform="translate(${c} ${c}) scale(${scale}) translate(${-c} ${-c})"` : ''}>`;
|
||||||
|
return svg.replace(/(<svg[^>]*>)/, `$1\n${g}`).replace(/<\/svg>\s*$/, '</g></svg>\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));
|
||||||
|
}
|
||||||
33
tools/stack-render.mjs
Normal file
@@ -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 <plan.mjs> [outDir]
|
||||||
|
Writes plane-NN-<sheet>-<face>.svg + manifest.json for the simulator
|
||||||
|
(fable/stack/simulator.html?c=<id>). */
|
||||||
|
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 <plan.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)`);
|
||||||