diff --git a/fable/Proof1/manifest.json b/fable/Proof1/manifest.json
index 64a9e52..03280f7 100644
--- a/fable/Proof1/manifest.json
+++ b/fable/Proof1/manifest.json
@@ -20,59 +20,77 @@
"file": "plane-1-sun.svg",
"z": 0,
"blur": 1.4,
- "dim": 0.95,
+ "dim": 1,
"white": "none",
"glow": 1
},
{
- "id": "sea-far",
- "file": "plane-2-sea-far.svg",
- "z": 9,
- "blur": 0.9,
- "dim": 1,
- "white": "none",
- "glow": 0.7
- },
- {
- "id": "sea-mid",
- "file": "plane-3-sea-mid.svg",
- "z": 15,
- "blur": 0.5,
- "dim": 1,
- "white": "trace",
- "glow": 0.7
- },
- {
- "id": "sea-near",
- "file": "plane-4-sea-near.svg",
- "z": 18,
- "blur": 0.2,
- "dim": 1,
- "white": "trace",
- "glow": 0.7
- },
- {
- "id": "event-halo",
- "file": "plane-5-event-halo.svg",
- "z": 27,
- "blur": 2.4,
+ "id": "event-glow",
+ "file": "plane-2-event-glow.svg",
+ "z": 8,
+ "blur": 2.6,
"dim": 0.85,
"white": "none",
"glow": 0.9
},
{
- "id": "event-core",
- "file": "plane-6-event-core.svg",
- "z": 30,
- "blur": 0,
+ "id": "sea-far",
+ "file": "plane-3-sea-far.svg",
+ "z": 14,
+ "blur": 0.9,
+ "dim": 0.7,
+ "white": "none",
+ "glow": 0.8
+ },
+ {
+ "id": "sea-mid",
+ "file": "plane-4-sea-mid.svg",
+ "z": 18,
+ "blur": 0.5,
+ "dim": 0.7,
+ "white": "trace",
+ "glow": 0.7
+ },
+ {
+ "id": "sea-near",
+ "file": "plane-5-sea-near.svg",
+ "z": 22,
+ "blur": 0.2,
+ "dim": 0.7,
+ "white": "trace",
+ "glow": 0.7
+ },
+ {
+ "id": "event-birth",
+ "file": "plane-6-event-birth.svg",
+ "z": 28,
+ "blur": 0.7,
"dim": 1,
- "white": "under",
+ "white": "none",
+ "glow": 0.8
+ },
+ {
+ "id": "event-life",
+ "file": "plane-7-event-life.svg",
+ "z": 32,
+ "blur": 0.3,
+ "dim": 1,
+ "white": "trace",
"glow": 0.5
},
+ {
+ "id": "event-death",
+ "file": "plane-8-event-death.svg",
+ "z": 36,
+ "blur": 0,
+ "dim": 1,
+ "white": "under",
+ "glow": 0.3
+ },
{
"id": "front-glass",
- "file": "plane-7-front-glass.svg",
- "z": 30,
+ "file": "plane-9-front-glass.svg",
+ "z": 36,
"blur": 0,
"dim": 1,
"white": "none",
diff --git a/fable/Proof1/plane-1-sun.svg b/fable/Proof1/plane-1-sun.svg
index 686cf46..96fb5e7 100644
--- a/fable/Proof1/plane-1-sun.svg
+++ b/fable/Proof1/plane-1-sun.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/fable/Proof1/plane-5-event-halo.svg b/fable/Proof1/plane-2-event-glow.svg
similarity index 100%
rename from fable/Proof1/plane-5-event-halo.svg
rename to fable/Proof1/plane-2-event-glow.svg
diff --git a/fable/Proof1/plane-2-sea-far.svg b/fable/Proof1/plane-3-sea-far.svg
similarity index 100%
rename from fable/Proof1/plane-2-sea-far.svg
rename to fable/Proof1/plane-3-sea-far.svg
diff --git a/fable/Proof1/plane-3-sea-mid.svg b/fable/Proof1/plane-4-sea-mid.svg
similarity index 100%
rename from fable/Proof1/plane-3-sea-mid.svg
rename to fable/Proof1/plane-4-sea-mid.svg
diff --git a/fable/Proof1/plane-4-sea-near.svg b/fable/Proof1/plane-5-sea-near.svg
similarity index 100%
rename from fable/Proof1/plane-4-sea-near.svg
rename to fable/Proof1/plane-5-sea-near.svg
diff --git a/fable/Proof1/plane-6-event-birth.svg b/fable/Proof1/plane-6-event-birth.svg
new file mode 100644
index 0000000..0ff82c6
--- /dev/null
+++ b/fable/Proof1/plane-6-event-birth.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/fable/Proof1/plane-7-event-life.svg b/fable/Proof1/plane-7-event-life.svg
new file mode 100644
index 0000000..a0aa0ff
--- /dev/null
+++ b/fable/Proof1/plane-7-event-life.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/fable/Proof1/plane-8-event-death.svg b/fable/Proof1/plane-8-event-death.svg
new file mode 100644
index 0000000..8f62bc6
--- /dev/null
+++ b/fable/Proof1/plane-8-event-death.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/fable/Proof1/plane-7-front-glass.svg b/fable/Proof1/plane-9-front-glass.svg
similarity index 100%
rename from fable/Proof1/plane-7-front-glass.svg
rename to fable/Proof1/plane-9-front-glass.svg
diff --git a/fable/Proof1/simulator.html b/fable/Proof1/simulator.html
index 8a16516..aabe76a 100644
--- a/fable/Proof1/simulator.html
+++ b/fable/Proof1/simulator.html
@@ -1,14 +1,13 @@
Proof1 · lit chamber — backlit parallax
+
+
Proof2 · lit chamber
+
move mouse = parallax · drag sliders
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/fable/Proof3/manifest.json b/fable/Proof3/manifest.json
new file mode 100644
index 0000000..4c941a8
--- /dev/null
+++ b/fable/Proof3/manifest.json
@@ -0,0 +1,137 @@
+{
+ "piece": "Proof3 — the lit chamber (equal-count shells, ring-pop)",
+ "source": "REFINED_Backlit7 merge (mirror event, uniform-lace)",
+ "seed": "MESON-5113#03",
+ "bubbleOpacity": 0.5,
+ "art": {
+ "w": 1001,
+ "h": 1620,
+ "ratio": "phi (golden portrait)"
+ },
+ "sheet": {
+ "w_mm": 330,
+ "h_mm": 420,
+ "note": "A3 — golden art square floated, clear margins = lit void"
+ },
+ "gaps_mm": "plane z is nominal; two scales: ~3mm within a sheet, ~9mm between",
+ "planes": [
+ {
+ "id": "sun",
+ "file": "plane-01-sun.svg",
+ "z": 0,
+ "blur": 1.4,
+ "dim": 1,
+ "white": "none",
+ "glow": 1
+ },
+ {
+ "id": "glow",
+ "file": "plane-02-glow.svg",
+ "z": 5,
+ "blur": 2.6,
+ "dim": 0.85,
+ "white": "none",
+ "glow": 0.9
+ },
+ {
+ "id": "wave1",
+ "file": "plane-03-wave1.svg",
+ "z": 10,
+ "blur": 0.9,
+ "dim": 0.7,
+ "white": "none",
+ "glow": 0.8
+ },
+ {
+ "id": "p1",
+ "file": "plane-04-p1.svg",
+ "z": 14,
+ "blur": 0.8,
+ "dim": 1,
+ "white": "none",
+ "glow": 0.8
+ },
+ {
+ "id": "wave2",
+ "file": "plane-05-wave2.svg",
+ "z": 18,
+ "blur": 0.5,
+ "dim": 0.7,
+ "white": "trace",
+ "glow": 0.7
+ },
+ {
+ "id": "p2",
+ "file": "plane-06-p2.svg",
+ "z": 22,
+ "blur": 0.7,
+ "dim": 1,
+ "white": "none",
+ "glow": 0.7
+ },
+ {
+ "id": "wave3",
+ "file": "plane-07-wave3.svg",
+ "z": 26,
+ "blur": 0.2,
+ "dim": 0.7,
+ "white": "trace",
+ "glow": 0.7
+ },
+ {
+ "id": "p3",
+ "file": "plane-08-p3.svg",
+ "z": 30,
+ "blur": 0.55,
+ "dim": 1,
+ "white": "trace",
+ "glow": 0.6
+ },
+ {
+ "id": "p4",
+ "file": "plane-09-p4.svg",
+ "z": 34,
+ "blur": 0.45,
+ "dim": 1,
+ "white": "trace",
+ "glow": 0.5
+ },
+ {
+ "id": "p5",
+ "file": "plane-10-p5.svg",
+ "z": 38,
+ "blur": 0.35,
+ "dim": 1,
+ "white": "trace",
+ "glow": 0.4
+ },
+ {
+ "id": "p6",
+ "file": "plane-11-p6.svg",
+ "z": 42,
+ "blur": 0.2,
+ "dim": 1,
+ "white": "under",
+ "glow": 0.35
+ },
+ {
+ "id": "p7",
+ "file": "plane-12-p7.svg",
+ "z": 46,
+ "blur": 0,
+ "dim": 1,
+ "white": "under",
+ "glow": 0.3
+ },
+ {
+ "id": "film-grain",
+ "file": "plane-13-film-grain.svg",
+ "z": 46,
+ "blur": 0,
+ "dim": 1,
+ "white": "none",
+ "glow": 0,
+ "blend": "multiply"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/fable/Proof3/plane-01-sun.svg b/fable/Proof3/plane-01-sun.svg
new file mode 100644
index 0000000..1bf132a
--- /dev/null
+++ b/fable/Proof3/plane-01-sun.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/fable/Proof3/plane-02-glow.svg b/fable/Proof3/plane-02-glow.svg
new file mode 100644
index 0000000..b27ca67
--- /dev/null
+++ b/fable/Proof3/plane-02-glow.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/fable/Proof3/plane-03-wave1.svg b/fable/Proof3/plane-03-wave1.svg
new file mode 100644
index 0000000..1b50feb
--- /dev/null
+++ b/fable/Proof3/plane-03-wave1.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/fable/Proof3/plane-04-p1.svg b/fable/Proof3/plane-04-p1.svg
new file mode 100644
index 0000000..cf302d8
--- /dev/null
+++ b/fable/Proof3/plane-04-p1.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/fable/Proof3/plane-05-wave2.svg b/fable/Proof3/plane-05-wave2.svg
new file mode 100644
index 0000000..0c698a9
--- /dev/null
+++ b/fable/Proof3/plane-05-wave2.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/fable/Proof3/plane-06-p2.svg b/fable/Proof3/plane-06-p2.svg
new file mode 100644
index 0000000..a819c98
--- /dev/null
+++ b/fable/Proof3/plane-06-p2.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/fable/Proof3/plane-07-wave3.svg b/fable/Proof3/plane-07-wave3.svg
new file mode 100644
index 0000000..962ab5a
--- /dev/null
+++ b/fable/Proof3/plane-07-wave3.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/fable/Proof3/plane-08-p3.svg b/fable/Proof3/plane-08-p3.svg
new file mode 100644
index 0000000..fb1a423
--- /dev/null
+++ b/fable/Proof3/plane-08-p3.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/fable/Proof3/plane-09-p4.svg b/fable/Proof3/plane-09-p4.svg
new file mode 100644
index 0000000..571e02b
--- /dev/null
+++ b/fable/Proof3/plane-09-p4.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/fable/Proof3/plane-10-p5.svg b/fable/Proof3/plane-10-p5.svg
new file mode 100644
index 0000000..d258d96
--- /dev/null
+++ b/fable/Proof3/plane-10-p5.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/fable/Proof3/plane-11-p6.svg b/fable/Proof3/plane-11-p6.svg
new file mode 100644
index 0000000..0ddf115
--- /dev/null
+++ b/fable/Proof3/plane-11-p6.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/fable/Proof3/plane-12-p7.svg b/fable/Proof3/plane-12-p7.svg
new file mode 100644
index 0000000..e26533d
--- /dev/null
+++ b/fable/Proof3/plane-12-p7.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/fable/Proof3/plane-13-film-grain.svg b/fable/Proof3/plane-13-film-grain.svg
new file mode 100644
index 0000000..2b80bc1
--- /dev/null
+++ b/fable/Proof3/plane-13-film-grain.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/fable/Proof3/simulator.html b/fable/Proof3/simulator.html
new file mode 100644
index 0000000..4ddf4ad
--- /dev/null
+++ b/fable/Proof3/simulator.html
@@ -0,0 +1,101 @@
+
+Proof3 · lit chamber — backlit parallax
+
+
+
Proof3 · lit chamber
+
move mouse = parallax · drag sliders
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/render/palette.js b/src/render/palette.js
index 498d92f..4c170df 100644
--- a/src/render/palette.js
+++ b/src/render/palette.js
@@ -118,16 +118,20 @@ export function bubbleStops(soft) {
// disc. Each stop carries [offset, alpha, paperMix]: paperMix 1 = fully toward
// paper (pale core), 0 = full family colour (jewel rim). `weight` (0..1) scales
// the rim's peak alpha for delicacy; `soft` widens the rim inward.
-export function bubbleHollowStops(weight = 0.7, soft = 0.3, peakOverride = null) {
+// core (0..1): how much of the pale centre veil is drawn. 1 = current filled
+// core; toward 0 the centre empties out (transparent) so the bubble reads as a
+// pure ring — used to "un-mud" dense shells (empty centre + crisp popping rim).
+export function bubbleHollowStops(weight = 0.7, soft = 0.3, peakOverride = null, core = 1) {
const s = clamp(soft, 0, 1.6);
+ const c = clamp(core, 0, 1);
const peak = peakOverride ?? (0.35 + 0.45 * clamp(weight, 0, 1)); // ~0.7 at weight 0.78
- const rimOff = clamp(0.80 - 0.14 * s, 0.6, 0.86); // rim sits ~0.7–0.8 of radius
+ const rimOff = clamp(0.80 - 0.14 * s, 0.6, 0.9); // lower soft → rim nearer edge = thinner/crisper
return [
- [0.00, peak * 0.36, 0.74], // pale veil at the very centre
- [Math.max(0.22, rimOff - 0.5), peak * 0.26, 0.5], // dip — the transparent belly
+ [0.00, peak * 0.36 * c, 0.74], // centre veil (fades with core)
+ [Math.max(0.22, rimOff - 0.5), peak * 0.26 * c, 0.5], // dip — the transparent belly
[rimOff - 0.16, peak * 0.72, 0.12], // rim rising, colour saturating
[rimOff, peak, 0.0], // bright saturated rim (peak)
- [Math.min(0.94, rimOff + 0.12), peak * 0.55, 0.04],
+ [Math.min(0.96, rimOff + 0.1), peak * 0.55, 0.04],
[1.00, 0.0, 0.0], // feather to nothing
];
}
diff --git a/src/render/svgVector.js b/src/render/svgVector.js
index 3c88df6..1e1490a 100644
--- a/src/render/svgVector.js
+++ b/src/render/svgVector.js
@@ -170,6 +170,8 @@ export function renderSVG(scene, params, sizePx = 4800) {
// (group opacity isolates → overlaps don't accumulate). Dense knots become
// even lace, not mud — even ink density = even backlit transmission.
const merge = !!params.bubbleMerge;
+ const band = params.bubbleBand || null; // [lo,hi] time-slice of the trail (life = arc-frac from vertex)
+ const scat = params.bubbleScatter ?? 0; // scatter the dense vertex knot across bands
const bubbleRng = makeRng(params.seed, 'bubbles'); // scene.tracks order → deterministic
for (const track of scene.tracks) {
if (track.pts.length < 2) continue;
@@ -191,6 +193,16 @@ export function renderSVG(scene, params, sizePx = 4800) {
const alpha = merge ? 1 : Math.round(Math.min(1, (0.36 + df.tone * 0.58) * bop) * 100) / 100;
const m = buckets.get(id);
for (const b of sampleBubbles({ ...track, sizeScale: df.sizeScale }, params, bubbleRng)) {
+ // bubbleBand [lo,hi]: emit only bubbles whose TIME score (life = arc-length
+ // fraction from the vertex) falls in the band → split the trail into
+ // depth planes by proper time. bubbleScatter blends a position-hashed
+ // jitter that is strongest near the vertex (life≈0), so the dense knot
+ // distributes across bands instead of piling on the earliest plane.
+ if (band) {
+ const h = ((Math.sin(b.x * 127.1 + b.y * 311.7) * 43758.5453) % 1 + 1) % 1;
+ const score = Math.max(0, Math.min(0.9999, b.life + scat * (1 - b.life) * (1 - b.life) * (h - 0.5) * 2));
+ if (score < band[0] || score >= band[1]) continue;
+ }
const bcol = pal.bubbleInk(track, b.life, b.beta), bkc = rgbKey(bcol);
if (!colorMap.has(bkc)) colorMap.set(bkc, bcol);
const bkey = bkc + '|' + alpha;
@@ -376,8 +388,12 @@ function defs({ paperC, paperFlat, ink, coreInk, press = 0, baseVign, params, u,
const hollow = params.bubbleHollow ?? 0;
let bubGrads = '';
if (hollow > 0) {
- // merge wants a near-opaque rim so union coverage is flat within the group
- const hy = bubbleHollowStops(params.bubbleWeight ?? 0.7, params.bubbleSoft ?? 0.3, params.bubbleMerge ? 0.97 : null);
+ // merge wants a near-opaque rim so union coverage is flat within the group;
+ // bubbleCore empties the centre and bubbleMergePeak tunes rim brightness/crisp.
+ const hy = bubbleHollowStops(
+ params.bubbleWeight ?? 0.7, params.bubbleSoft ?? 0.3,
+ params.bubbleMerge ? (params.bubbleMergePeak ?? 0.97) : null,
+ params.bubbleCore ?? 1);
for (const [key, col] of colorMap) {
let st = '';
for (const [off, a, pm] of hy) {
diff --git a/tools/proof1-render.mjs b/tools/proof1-render.mjs
index 85301df..38444b5 100644
--- a/tools/proof1-render.mjs
+++ b/tools/proof1-render.mjs
@@ -133,8 +133,19 @@ const p = bcAssembleParams();
const scene = generateScene(p);
const bare = { ...scene, instrument: null, artifacts: null, media: null };
-const tracksImg = renderSVG({ ...bare, tracks: scene.tracks, shock: null },
- { ...p, emit: ['bubble'], originX: VERTEX.x, originY: VERTEX.y, sceneZoom: TRACE_ZOOM }, W);
+const eventBase = { ...p, emit: ['bubble'], originX: VERTEX.x, originY: VERTEX.y, sceneZoom: TRACE_ZOOM };
+// TIME-SPLIT into BIRTH / LIFE / DEATH by b.life (arc-length fraction from the
+// vertex). Because the β-law drives colour BY proper time (magenta birth →
+// violet death), this splits the event by hue too: the event ages in depth.
+// Later time = nearer. Scatter spreads the dense vertex knot across bands.
+const SCATTER = 0.32;
+const BANDS = { birth: [0.0, 0.45], life: [0.45, 0.78], death: [0.78, 1.001] };
+const evt = (band) => renderSVG({ ...bare, tracks: scene.tracks, shock: null },
+ band ? { ...eventBase, bubbleBand: band, bubbleScatter: SCATTER } : eventBase, W);
+const tracksAll = evt(null); // glow = whole event, blurred deep
+const tracksBirth = evt(BANDS.birth);
+const tracksLife = evt(BANDS.life);
+const tracksDeath = evt(BANDS.death);
const diskImg = renderSVG({ ...bare, tracks: [], shock: scene.shock },
{ ...p, emit: ['disk'], originX: SUN.x, originY: SUN.y }, W);
const sheets = deck();
@@ -150,17 +161,26 @@ const IMG = (svg) => `${IMG(grain)}`);
-// DOF blur is tuned per MARK TYPE: thin wave-lines dissolve under blur far
-// faster than dense bubbles, so line-art planes get very little (parallax
-// offset carries their depth). Bubbles/glow tolerate more.
+// SEA on THREE individual plates (own internal parallax), FULL opacity so the
+// rearmost (sea-far) stays visible — real air gaps carry depth, not opacity
+// dimming. Thin wave-lines get minimal blur (they dissolve under it).
+//
+// ORDER (deep → near): sun · event-GLOW (submerged afterimage) · SEA ×3 ·
+// event BIRTH/LIFE/DEATH · glass. The sea is a VEIL between the event's ghost
+// (its light, held under the water) and its body (the fresh bubbles above it).
+// sun +50%: a 0.7-opacity second pass of the disk lifts the faint hollow sun
+// (~+50% across its range) without a hard opacity clip. sea −30%: dim 1.0→0.7.
+const sunPlane = planeSVG(`${IMG(diskImg)}${IMG(diskImg)}`);
const PLANES = [
- { id: 'sun', file: 'plane-1-sun.svg', z: 0, blur: 1.4, dim: 0.95, white: 'none', glow: 1.0, svg: planeSVG(IMG(diskImg)) },
- { id: 'sea-far', file: 'plane-2-sea-far.svg', z: 9, blur: 0.9, dim: 1.0, white: 'none', glow: 0.7, svg: planeSVG(IMG(sheets[0])) },
- { id: 'sea-mid', file: 'plane-3-sea-mid.svg', z: 15, blur: 0.5, dim: 1.0, white: 'trace', glow: 0.7, svg: planeSVG(IMG(sheets[1])) },
- { id: 'sea-near', file: 'plane-4-sea-near.svg', z: 18, blur: 0.2, dim: 1.0, white: 'trace', glow: 0.7, svg: planeSVG(IMG(sheets[2])) },
- { id: 'event-halo', file: 'plane-5-event-halo.svg', z: 27, blur: 2.4, dim: 0.85, white: 'none', glow: 0.9, svg: planeSVG(IMG(tracksImg)) },
- { id: 'event-core', file: 'plane-6-event-core.svg', z: 30, blur: 0.0, dim: 1.0, white: 'under', glow: 0.5, svg: planeSVG(IMG(tracksImg)) },
- { id: 'front-glass',file: 'plane-7-front-glass.svg',z: 30, blur: 0.0, dim: 1.0, white: 'none', glow: 0.0, blend: 'multiply', svg: frontGlass },
+ { id: 'sun', file: 'plane-1-sun.svg', z: 0, blur: 1.4, dim: 1.0, white: 'none', glow: 1.0, svg: sunPlane },
+ { id: 'event-glow', file: 'plane-2-event-glow.svg', z: 8, blur: 2.6, dim: 0.85, white: 'none', glow: 0.9, svg: planeSVG(IMG(tracksAll)) },
+ { id: 'sea-far', file: 'plane-3-sea-far.svg', z: 14, blur: 0.9, dim: 0.7, white: 'none', glow: 0.8, svg: planeSVG(IMG(sheets[0])) },
+ { id: 'sea-mid', file: 'plane-4-sea-mid.svg', z: 18, blur: 0.5, dim: 0.7, white: 'trace', glow: 0.7, svg: planeSVG(IMG(sheets[1])) },
+ { id: 'sea-near', file: 'plane-5-sea-near.svg', z: 22, blur: 0.2, dim: 0.7, white: 'trace', glow: 0.7, svg: planeSVG(IMG(sheets[2])) },
+ { id: 'event-birth', file: 'plane-6-event-birth.svg', z: 28, blur: 0.7, dim: 1.0, white: 'none', glow: 0.8, svg: planeSVG(IMG(tracksBirth)) },
+ { id: 'event-life', file: 'plane-7-event-life.svg', z: 32, blur: 0.3, dim: 1.0, white: 'trace', glow: 0.5, svg: planeSVG(IMG(tracksLife)) },
+ { id: 'event-death', file: 'plane-8-event-death.svg', z: 36, blur: 0.0, dim: 1.0, white: 'under', glow: 0.3, svg: planeSVG(IMG(tracksDeath)) },
+ { id: 'front-glass', file: 'plane-9-front-glass.svg', z: 36, blur: 0.0, dim: 1.0, white: 'none', glow: 0.0, blend: 'multiply', svg: frontGlass },
];
for (const pl of PLANES) writeFileSync(`${OUT}/${pl.file}`, pl.svg);
@@ -181,14 +201,13 @@ const planesJSON = JSON.stringify(PLANES.map(({ svg, ...r }) => r));
writeFileSync(`${OUT}/simulator.html`, `
Proof1 · lit chamber — backlit parallax
+
+
Proof2 · lit chamber
+
move mouse = parallax · drag sliders
+
+
+
+
+
+
+
+
+
+
+`);
+
+console.log(`PROOF2 — ${W}×${H} (φ) — ${PLANES.length} planes → ${OUT}/`);
+for (const pl of PLANES) console.log(` ${pl.file.padEnd(26)} z=${pl.z}mm blur=${pl.blur} white=${pl.white}`);
+console.log(`manifest -> ${OUT}/manifest.json`);
+console.log(`simulator -> ${OUT}/simulator.html`);
diff --git a/tools/proof3-render.mjs b/tools/proof3-render.mjs
new file mode 100644
index 0000000..b771bb9
--- /dev/null
+++ b/tools/proof3-render.mjs
@@ -0,0 +1,349 @@
+/* ============================================================
+ proof1-render.mjs — PROOF 1: the lit chamber, split into planes.
+ Takes the locked golden merge composition (REFINED_Backlit7,
+ the mirror event, uniform-lace merge) and emits it as SEPARATE
+ transparent planes for a backlit acrylic stack, plus a manifest
+ and a parallax/backlight simulator to art-direct the depth
+ BEFORE committing to print.
+
+ Plane map (deepest → nearest), causal depth cause→world→evidence:
+ 1 sun faded disk · pure transmitted colour · zero white
+ 2 sea-far sheet[0]
+ 3 sea-mid sheet[1]
+ 4 sea-near sheet[2]
+ 5 event-halo merged bubbles, blurred by the air gap (glow)
+ 6 event-core merged bubbles, sharp · white underlay
+ + front-glass grain + aging (the surface, multiply)
+
+ Golden frame (φ) is preserved and FLOATED in an A3 sheet later;
+ this stage renders the art square only.
+
+ Usage: node tools/proof1-render.mjs [width]
+ → fable/Proof1/
+ ============================================================ */
+import { writeFileSync, mkdirSync } from 'node:fs';
+import { carpetSVG } from '../src/qft/carpet.js';
+import { generateScene } from '../src/scene/scene.js';
+import { renderSVG } from '../src/render/svgVector.js';
+import { paramsFromSeed as bcParams } from '../src/scene/params.js';
+import { GROUPS, TOGGLES, FIXED } from '../src/ui/controls.js';
+import { sampleBubbles, depthFactors } from '../src/scene/bubbles.js';
+import { makeRng } from '../src/rng.js';
+
+const PHI = (1 + Math.sqrt(5)) / 2;
+const IP = 1 / PHI, IP2 = 1 / (PHI * PHI);
+
+const W = +(process.argv[2] || 1001);
+const H = Math.round(W * PHI); // golden portrait
+const OUT = 'fable/Proof3';
+mkdirSync(OUT, { recursive: true });
+const u = Math.min(W, H) / 1000;
+const dataUri = (svg) => 'data:image/svg+xml;base64,' + Buffer.from(svg).toString('base64');
+
+/* ---- golden armature (identical to REFINED_Backlit7) ---- */
+const SEED = 'MESON-5113';
+const SEED_EVENT = `${SEED}#03`; // the chosen mirror event
+const BUBOP = +(process.env.BUBOP ?? 0.5); // mid of the 45→55 band
+const HORIZON_F = IP2;
+const HY = HORIZON_F * H, SKY_H = HY;
+const S_SCALE = (Math.min(W, H) / 2) * (1 - 0.02);
+const XL = IP2 * W, XR = IP * W;
+const SUN = { x: XR, y: IP * SKY_H }; // mirror: sun on the RIGHT line
+const SUN_R = IP2 * SKY_H;
+const VERTEX = { x: XL, y: HY }; // mirror: vertex on the LEFT line
+const TRACE_ZOOM = 2.1;
+
+/* ---- textures ---- */
+function grainSVG(o = {}) {
+ const { seed = 19, tone = [38, 32, 26], amount = 0.5 } = o;
+ const t = tone.map(v => (v / 255).toFixed(3));
+ return ``;
+}
+function agingSVG(o = {}) {
+ const { seed = 5, scratches = 8, dust = 0.6, foxing = 0.62, tone = [60, 48, 34] } = o;
+ const t = tone.map(v => (v / 255).toFixed(3));
+ let lines = '';
+ // deterministic scratch scatter without Math.random (seeded LCG)
+ let s = seed * 2654435761 >>> 0;
+ const rnd = () => ((s = (s * 1664525 + 1013904223) >>> 0) / 4294967296);
+ for (let i = 0; i < scratches; i++) {
+ const x = rnd() * W, y0 = rnd() * H * 0.4, len = (0.3 + rnd() * 0.6) * H;
+ const x2 = x + (rnd() - 0.5) * 40, y2 = y0 + len;
+ lines += ``;
+ }
+ return ``;
+}
+
+/* ---- golden sea deck (equal-line, φ waves) ---- */
+function deck() {
+ const base = {
+ mode: 'plate', width: W, height: H, horizon: HORIZON_F,
+ wFar: 0.5, wNear: 0.5 * PHI, overlap: PHI,
+ rows: 48, mound: 0.4, sat: 0.58, lightNear: 0.33, lightFar: 0.56,
+ chaos: 0.82, blips: 1.5,
+ };
+ const lerp = (a, b, t) => a + (b - a) * t;
+ const strokes = [{ near: 2.6, far: 1.0 }, { near: 1.6, far: 0.6 }, { near: 1.0, far: 0.38 }];
+ return [0, 1, 2].map(i => {
+ const t = i / 2;
+ return carpetSVG(H, { ...base, salt: 'mfield' + i,
+ hue: lerp(0.55, 0.47, t), hue2: lerp(0.55, 0.47, t) + 0.035,
+ chaos: lerp(0.82 * 0.8, 0.82, t),
+ strokeNear: strokes[i].near * 1.35, strokeFar: strokes[i].far * 1.35 });
+ });
+}
+
+/* ---- event params (merge, delicate, golden-placed) ---- */
+function bcAssembleParams() {
+ const p = { ...FIXED, ...bcParams(SEED) };
+ for (const g of GROUPS) for (const c of g.controls) if (!(c.id in p)) p[c.id] = c.value;
+ for (const t of TOGGLES) if (!(t.id in p)) p[t.id] = t.value;
+ Object.assign(p, {
+ invert: true, transparentPaper: true, vign: 0,
+ showHeader: false, showBoundary: false, showFiducials: false,
+ annotate: 0, reseau: 0, filmEdge: false, splice: false, artifacts: 0,
+ palette: 'magbeta', saturation: 1.0,
+ diskBubbles: false, diskPressure: 0.9, diskSoften: 1.4,
+ shockStriations: 1.0, diskHollow: 1.0,
+ depth: 0.3, aging: 0.28,
+ eventX: 0, eventY: 0, shockX: 0, shockY: 0,
+ sweepers: 5, primaries: 17, eloss: 0.34, deltaRate: 0.8,
+ bgEvents: 2,
+ shockSize: SUN_R / S_SCALE, shockIntensity: 0.8,
+ size: 1.6,
+ bubbleHollow: 1, bubbleWeight: 0.7, bubbleSoft: 0.7, bubbleMerge: 1,
+ bubbleOpacity: BUBOP,
+ canvasW: W, canvasH: H,
+ seed: SEED_EVENT,
+ });
+ return p;
+}
+
+/* ---- render the source images ---- */
+const p = bcAssembleParams();
+const scene = generateScene(p);
+const bare = { ...scene, instrument: null, artifacts: null, media: null };
+
+const eventBase = { ...p, emit: ['bubble'], originX: VERTEX.x, originY: VERTEX.y, sceneZoom: TRACE_ZOOM };
+const SCATTER = 0.32;
+const NP = 7; // particle time-shells p1..p7
+
+// EQUAL-COUNT bands: sample the exact bubble stream renderSVG will use (same
+// seed/track order/params) to get the life distribution, then split it at
+// count-quantiles so every shell carries ~the same number of bubbles (dense
+// late betas no longer overweight the front shells).
+function equalCountEdges(n) {
+ const rng = makeRng(p.seed, 'bubbles');
+ const lives = [];
+ for (const track of scene.tracks) {
+ if (track.pts.length < 2) continue;
+ const df = depthFactors(track, p);
+ for (const b of sampleBubbles({ ...track, sizeScale: df.sizeScale }, p, rng)) lives.push(b.life);
+ }
+ lives.sort((a, b) => a - b);
+ const edges = [0];
+ for (let i = 1; i < n; i++) edges.push(lives[Math.floor(i / n * lives.length)]);
+ edges.push(1.001);
+ return edges;
+}
+const EDGES = equalCountEdges(NP);
+
+// UN-MUD: as a shell ages toward the FRONT (t→1), draw the bubble CENTRE down
+// (core→0, transparent) and CRISP the rim (soft→thin, peak→bright) so dense
+// violet deaths read as a lace of popping rings, not filled mud. Back/birth
+// shells stay soft and full.
+const lerp = (a, b, t) => a + (b - a) * t;
+const shellParams = (i) => {
+ const t = NP > 1 ? i / (NP - 1) : 0;
+ return {
+ bubbleCore: lerp(0.72, 0.05, t), // centre empties toward the front
+ bubbleSoft: lerp(0.75, 0.14, t), // rim thins/crisps toward the front
+ bubbleMergePeak: lerp(0.86, 0.99, t), // rim brightens/pops toward the front
+ bubbleScatter: SCATTER,
+ };
+};
+const evt = (band, extra = {}) => renderSVG({ ...bare, tracks: scene.tracks, shock: null },
+ band ? { ...eventBase, bubbleBand: band, ...extra } : eventBase, W);
+const tracksAll = evt(null); // glow = whole event, blurred deep
+const P = []; // P[i] = shell i (p(i+1)), birth→death
+for (let i = 0; i < NP; i++) P.push(evt([EDGES[i], EDGES[i + 1]], shellParams(i)));
+const diskImg = renderSVG({ ...bare, tracks: [], shock: scene.shock },
+ { ...p, emit: ['disk'], originX: SUN.x, originY: SUN.y }, W);
+const sheets = deck();
+const grain = grainSVG({ amount: 0.52 });
+const aging = agingSVG({ seed: 8 });
+
+/* ---- write each plane as a transparent SVG (art square only) ---- */
+const planeSVG = (inner) =>
+ ``;
+const IMG = (svg) => ``;
+
+// grain+aging combined onto one front-glass texture (multiply handled in sim)
+const frontGlass = planeSVG(
+ `${IMG(aging)}${IMG(grain)}`);
+
+// SEA on THREE individual plates (own internal parallax), FULL opacity so the
+// rearmost (sea-far) stays visible — real air gaps carry depth, not opacity
+// dimming. Thin wave-lines get minimal blur (they dissolve under it).
+//
+// ORDER (deep → near): sun · event-GLOW (submerged afterimage) · SEA ×3 ·
+// event BIRTH/LIFE/DEATH · glass. The sea is a VEIL between the event's ghost
+// (its light, held under the water) and its body (the fresh bubbles above it).
+// sun +50%: a 0.7-opacity second pass lifts the faint hollow sun. sea −30% (0.7).
+const sunPlane = planeSVG(`${IMG(diskImg)}${IMG(diskImg)}`);
+const pPlane = (i) => planeSVG(IMG(P[i]));
+
+// PROOF3 layer order (deep → near), thin stacked sheets:
+// sun · glow · wave1 · p1 · wave2 · p2 · wave3 · p3 · p4 · p5 · p6 · p7 · grain
+// The sea threads through the EARLY (birth) shells; the later (death) shells
+// stack in front of the water. Each particle shell is one step older in time.
+const zBubBlur = [0.8, 0.7, 0.55, 0.45, 0.35, 0.2, 0.0]; // p1..p7: deeper = softer
+const pWhite = ['none', 'none', 'trace', 'trace', 'trace', 'under', 'under'];
+const PLANES = [
+ { id: 'sun', file: 'plane-01-sun.svg', z: 0, blur: 1.4, dim: 1.0, white: 'none', glow: 1.0, svg: sunPlane },
+ { id: 'glow', file: 'plane-02-glow.svg', z: 5, blur: 2.6, dim: 0.85, white: 'none', glow: 0.9, svg: planeSVG(IMG(tracksAll)) },
+ { id: 'wave1', file: 'plane-03-wave1.svg', z: 10, blur: 0.9, dim: 0.7, white: 'none', glow: 0.8, svg: planeSVG(IMG(sheets[0])) },
+ { id: 'p1', file: 'plane-04-p1.svg', z: 14, blur: zBubBlur[0], dim: 1.0, white: pWhite[0], glow: 0.8, svg: pPlane(0) },
+ { id: 'wave2', file: 'plane-05-wave2.svg', z: 18, blur: 0.5, dim: 0.7, white: 'trace', glow: 0.7, svg: planeSVG(IMG(sheets[1])) },
+ { id: 'p2', file: 'plane-06-p2.svg', z: 22, blur: zBubBlur[1], dim: 1.0, white: pWhite[1], glow: 0.7, svg: pPlane(1) },
+ { id: 'wave3', file: 'plane-07-wave3.svg', z: 26, blur: 0.2, dim: 0.7, white: 'trace', glow: 0.7, svg: planeSVG(IMG(sheets[2])) },
+ { id: 'p3', file: 'plane-08-p3.svg', z: 30, blur: zBubBlur[2], dim: 1.0, white: pWhite[2], glow: 0.6, svg: pPlane(2) },
+ { id: 'p4', file: 'plane-09-p4.svg', z: 34, blur: zBubBlur[3], dim: 1.0, white: pWhite[3], glow: 0.5, svg: pPlane(3) },
+ { id: 'p5', file: 'plane-10-p5.svg', z: 38, blur: zBubBlur[4], dim: 1.0, white: pWhite[4], glow: 0.4, svg: pPlane(4) },
+ { id: 'p6', file: 'plane-11-p6.svg', z: 42, blur: zBubBlur[5], dim: 1.0, white: pWhite[5], glow: 0.35, svg: pPlane(5) },
+ { id: 'p7', file: 'plane-12-p7.svg', z: 46, blur: zBubBlur[6], dim: 1.0, white: pWhite[6], glow: 0.3, svg: pPlane(6) },
+ { id: 'film-grain', file: 'plane-13-film-grain.svg', z: 46, blur: 0.0, dim: 1.0, white: 'none', glow: 0.0, blend: 'multiply', svg: frontGlass },
+];
+
+for (const pl of PLANES) writeFileSync(`${OUT}/${pl.file}`, pl.svg);
+
+const manifest = {
+ piece: 'Proof3 — the lit chamber (equal-count shells, ring-pop)',
+ source: 'REFINED_Backlit7 merge (mirror event, uniform-lace)',
+ seed: SEED_EVENT, bubbleOpacity: BUBOP,
+ art: { w: W, h: H, ratio: 'phi (golden portrait)' },
+ sheet: { w_mm: 330, h_mm: 420, note: 'A3 — golden art square floated, clear margins = lit void' },
+ gaps_mm: 'plane z is nominal; two scales: ~3mm within a sheet, ~9mm between',
+ planes: PLANES.map(({ svg, ...rest }) => rest),
+};
+writeFileSync(`${OUT}/manifest.json`, JSON.stringify(manifest, null, 2));
+
+/* ---- the backlit parallax simulator ---- */
+const planesJSON = JSON.stringify(PLANES.map(({ svg, ...r }) => r));
+writeFileSync(`${OUT}/simulator.html`, `
+Proof3 · lit chamber — backlit parallax
+
+
+
Proof3 · lit chamber
+
move mouse = parallax · drag sliders
+
+
+
+
+
+
+
+
+
+
+
+`);
+
+console.log(`PROOF3 — ${W}×${H} (φ) — ${PLANES.length} planes → ${OUT}/`);
+for (const pl of PLANES) console.log(` ${pl.file.padEnd(26)} z=${pl.z}mm blur=${pl.blur} white=${pl.white}`);
+console.log(`manifest -> ${OUT}/manifest.json`);
+console.log(`simulator -> ${OUT}/simulator.html`);