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