/* ============================================================ 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'; 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/Proof2'; 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 }; // TIME-SPLIT into N SHELLS by b.life (arc-length fraction from the vertex). // Because the β-law drives colour BY proper time (magenta birth → violet // death), the shells step smoothly in hue: the event ages across depth. // Later time = nearer. Scatter spreads the dense vertex knot across shells. const SCATTER = 0.32; const NP = 7; // particle time-shells p1..p7 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 P = []; // P[i] = shell i (p(i+1)), birth→death for (let i = 0; i < NP; i++) P.push(evt([i / NP, i === NP - 1 ? 1.001 : (i + 1) / NP])); 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])); // PROOF2 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: 'Proof2 — the lit chamber (7 time-shells)', 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`, ` Proof2 · 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`);