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 ` + + + + + + +${lines}`; +} + +/* ---- 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) => + `${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`);