/* ============================================================ 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/Proof1'; 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 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(); 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 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: 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); const manifest = { piece: 'Proof1 — the lit chamber', 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`, ` Proof1 · lit chamber — backlit parallax

Proof1 · lit chamber

move mouse = parallax · drag sliders
`); console.log(`PROOF1 — ${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`);