2026-07-19 13:37:10 -04:00
|
|
|
|
/* ============================================================
|
|
|
|
|
|
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 `<svg xmlns="http://www.w3.org/2000/svg" width="${W}" height="${H}" viewBox="0 0 ${W} ${H}">
|
|
|
|
|
|
<defs><filter id="g" x="0" y="0" width="100%" height="100%">
|
|
|
|
|
|
<feTurbulence type="fractalNoise" baseFrequency="0.9" numOctaves="2" seed="${seed}" stitchTiles="stitch" result="n"/>
|
|
|
|
|
|
<feColorMatrix in="n" type="matrix" values="0 0 0 0 ${t[0]} 0 0 0 0 ${t[1]} 0 0 0 0 ${t[2]} 0 0 0 ${amount} 0"/>
|
|
|
|
|
|
</filter></defs><rect width="${W}" height="${H}" filter="url(#g)"/></svg>`;
|
|
|
|
|
|
}
|
|
|
|
|
|
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 += `<line x1="${x.toFixed(0)}" y1="${y0.toFixed(0)}" x2="${x2.toFixed(0)}" y2="${y2.toFixed(0)}" stroke="rgb(${tone.join(',')})" stroke-opacity="${(0.05 + rnd() * 0.12).toFixed(3)}" stroke-width="${(0.4 + rnd() * 0.8).toFixed(2)}"/>`;
|
|
|
|
|
|
}
|
|
|
|
|
|
return `<svg xmlns="http://www.w3.org/2000/svg" width="${W}" height="${H}" viewBox="0 0 ${W} ${H}">
|
|
|
|
|
|
<defs>
|
|
|
|
|
|
<filter id="dust" x="0" y="0" width="100%" height="100%"><feTurbulence type="turbulence" baseFrequency="0.5" numOctaves="2" seed="${seed + 5}" stitchTiles="stitch" result="n"/><feColorMatrix in="n" type="matrix" values="0 0 0 0 ${t[0]} 0 0 0 0 ${t[1]} 0 0 0 0 ${t[2]} 0 0 0 ${(dust * 0.5).toFixed(3)} -0.18"/></filter>
|
|
|
|
|
|
<filter id="fox" x="0" y="0" width="100%" height="100%"><feTurbulence type="fractalNoise" baseFrequency="0.004" numOctaves="3" seed="${seed + 9}" stitchTiles="stitch" result="n"/><feColorMatrix in="n" type="matrix" values="0 0 0 0 0.42 0 0 0 0 0.30 0 0 0 0 0.16 0 0 0 ${(foxing * 0.32).toFixed(3)} -0.12"/></filter>
|
|
|
|
|
|
</defs>
|
|
|
|
|
|
<rect width="${W}" height="${H}" filter="url(#fox)"/>
|
|
|
|
|
|
<rect width="${W}" height="${H}" filter="url(#dust)"/>
|
|
|
|
|
|
${lines}</svg>`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* ---- 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 };
|
|
|
|
|
|
|
2026-07-19 21:28:14 -04:00
|
|
|
|
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);
|
2026-07-19 13:37:10 -04:00
|
|
|
|
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) =>
|
|
|
|
|
|
`<svg xmlns="http://www.w3.org/2000/svg" width="${W}" height="${H}" viewBox="0 0 ${W} ${H}">${inner}</svg>`;
|
|
|
|
|
|
const IMG = (svg) => `<image x="0" y="0" width="${W}" height="${H}" href="${dataUri(svg)}"/>`;
|
|
|
|
|
|
|
|
|
|
|
|
// grain+aging combined onto one front-glass texture (multiply handled in sim)
|
|
|
|
|
|
const frontGlass = planeSVG(
|
|
|
|
|
|
`${IMG(aging)}<g opacity="0.85">${IMG(grain)}</g>`);
|
|
|
|
|
|
|
2026-07-19 21:28:14 -04:00
|
|
|
|
// 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)}<g opacity="0.7">${IMG(diskImg)}</g>`);
|
2026-07-19 13:37:10 -04:00
|
|
|
|
const PLANES = [
|
2026-07-19 21:28:14 -04:00
|
|
|
|
{ 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 },
|
2026-07-19 13:37:10 -04:00
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
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`, `<!DOCTYPE html><html><head><meta charset="utf-8">
|
|
|
|
|
|
<title>Proof1 · lit chamber — backlit parallax</title>
|
|
|
|
|
|
<style>
|
|
|
|
|
|
html,body{margin:0;height:100%;background:#0a0a0b;overflow:hidden;
|
|
|
|
|
|
font:12px/1.5 ui-monospace,monospace;color:#9aa;}
|
|
|
|
|
|
#wrap{position:fixed;inset:0;display:grid;place-items:center;}
|
2026-07-19 21:28:14 -04:00
|
|
|
|
.scene{perspective:1400px;perspective-origin:50% 45%;}
|
2026-07-19 13:37:10 -04:00
|
|
|
|
.stack{position:relative;transform-style:preserve-3d;transition:transform .06s linear;
|
|
|
|
|
|
width:${W / 2}px;height:${H / 2}px;}
|
2026-07-19 21:28:14 -04:00
|
|
|
|
.panel{position:absolute;inset:-70%;
|
2026-07-19 13:37:10 -04:00
|
|
|
|
background:radial-gradient(120% 90% at 50% 42%,#fff6ec, #ffe7c8 42%, #f0c48c 78%, #caa06a 100%);
|
|
|
|
|
|
filter:blur(2px);}
|
|
|
|
|
|
.plane{position:absolute;inset:0;width:100%;height:100%;background-size:cover;
|
|
|
|
|
|
background-repeat:no-repeat;will-change:transform,filter;}
|
|
|
|
|
|
#hud{position:fixed;left:14px;top:12px;z-index:9;background:#141416cc;padding:12px 14px;
|
|
|
|
|
|
border-left:3px solid #d9b48a;max-width:300px;}
|
|
|
|
|
|
#hud h1{font-size:12px;letter-spacing:.18em;text-transform:uppercase;color:#d9b48a;font-weight:400;margin:0 0 8px;}
|
|
|
|
|
|
#hud label{display:block;margin:7px 0 2px;color:#aab;}
|
|
|
|
|
|
#hud input[type=range]{width:100%;}
|
|
|
|
|
|
#legend{position:fixed;right:14px;top:12px;z-index:9;background:#141416cc;padding:10px 12px;color:#889;}
|
|
|
|
|
|
#legend b{color:#d9b48a;font-weight:400;}
|
|
|
|
|
|
.pill{display:inline-block;margin:2px 4px 2px 0;padding:1px 6px;border:1px solid #333;border-radius:9px;cursor:pointer;color:#aab;}
|
|
|
|
|
|
.pill.off{opacity:.35;text-decoration:line-through;}
|
|
|
|
|
|
</style></head><body>
|
|
|
|
|
|
<div id="hud">
|
|
|
|
|
|
<h1>Proof1 · lit chamber</h1>
|
|
|
|
|
|
<div>move mouse = parallax · drag sliders</div>
|
|
|
|
|
|
<label>air gap ×<span id="gv"></span></label>
|
2026-07-19 21:28:14 -04:00
|
|
|
|
<input id="gap" type="range" min="1" max="28" step="0.5" value="12">
|
|
|
|
|
|
<label>parallax ×<span id="pv"></span></label>
|
|
|
|
|
|
<input id="par" type="range" min="0" max="220" step="5" value="110">
|
2026-07-19 13:37:10 -04:00
|
|
|
|
<label>backlight<span id="bv"></span></label>
|
|
|
|
|
|
<input id="bright" type="range" min="0.3" max="1.8" step="0.05" value="1">
|
|
|
|
|
|
<div id="pills"></div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div id="legend"></div>
|
|
|
|
|
|
<div id="wrap"><div class="scene"><div class="stack" id="stack">
|
|
|
|
|
|
<div class="panel" id="panel"></div>
|
|
|
|
|
|
</div></div></div>
|
|
|
|
|
|
<script>
|
|
|
|
|
|
const PLANES=${planesJSON};
|
|
|
|
|
|
const stack=document.getElementById('stack');
|
|
|
|
|
|
const legend=document.getElementById('legend');
|
|
|
|
|
|
const pills=document.getElementById('pills');
|
|
|
|
|
|
const maxZ=Math.max(...PLANES.map(p=>p.z));
|
|
|
|
|
|
const els={};
|
|
|
|
|
|
legend.innerHTML='<b>planes</b> (front → back)<br>'+[...PLANES].reverse().map(p=>p.id+' · '+p.z+'mm').join('<br>');
|
|
|
|
|
|
PLANES.forEach(p=>{
|
|
|
|
|
|
const d=document.createElement('div');
|
|
|
|
|
|
d.className='plane'; d.style.backgroundImage='url('+p.file+')';
|
|
|
|
|
|
d.dataset.id=p.id; stack.appendChild(d); els[p.id]=d;
|
|
|
|
|
|
const pill=document.createElement('span'); pill.className='pill'; pill.textContent=p.id;
|
|
|
|
|
|
pill.onclick=()=>{d.dataset.off=d.dataset.off?'':'1'; pill.classList.toggle('off'); layout();};
|
|
|
|
|
|
pills.appendChild(pill);
|
|
|
|
|
|
});
|
2026-07-19 21:28:14 -04:00
|
|
|
|
let gap=12, par=110, bright=1, mx=0, my=0;
|
|
|
|
|
|
const panel=document.getElementById('panel');
|
|
|
|
|
|
// One transform per plane = translateZ (real depth) + a depth-scaled screen
|
|
|
|
|
|
// shift (EXAGGERATED parallax) + the stack's 3D tilt. Deeper planes shift more,
|
|
|
|
|
|
// so the event's birth→death timeline slides apart as you move.
|
|
|
|
|
|
function update(){
|
|
|
|
|
|
stack.style.transform='rotateX('+(my*11).toFixed(2)+'deg) rotateY('+(mx*15).toFixed(2)+'deg)';
|
2026-07-19 13:37:10 -04:00
|
|
|
|
for(const p of PLANES){
|
|
|
|
|
|
const e=els[p.id];
|
2026-07-19 21:28:14 -04:00
|
|
|
|
const z=(p.z-maxZ)*gap; // front plane at 0, others recede
|
|
|
|
|
|
const depth=(maxZ-p.z)/(maxZ||1); // 0 = front, 1 = deepest
|
|
|
|
|
|
const ox=(-mx*depth*par).toFixed(1), oy=(my*depth*par).toFixed(1);
|
|
|
|
|
|
e.style.transform='translate3d('+ox+'px,'+oy+'px,'+z.toFixed(1)+'px)';
|
|
|
|
|
|
e.style.filter='blur('+((p.blur||0)*(gap/8)).toFixed(2)+'px)';
|
2026-07-19 13:37:10 -04:00
|
|
|
|
e.style.opacity=e.dataset.off?0:(p.dim??1);
|
|
|
|
|
|
e.style.mixBlendMode=p.blend||'normal';
|
|
|
|
|
|
}
|
2026-07-19 21:28:14 -04:00
|
|
|
|
// keep the light panel ALWAYS behind the deepest plane, scaled up so it still
|
|
|
|
|
|
// fills the backdrop as it recedes (else deep planes pop behind it at high gap).
|
|
|
|
|
|
const pz=(0-maxZ)*gap-90;
|
|
|
|
|
|
panel.style.transform='translateZ('+pz.toFixed(0)+'px) scale('+(1+(-pz)/1400).toFixed(2)+')';
|
|
|
|
|
|
panel.style.filter='blur(2px) brightness('+bright+')';
|
2026-07-19 13:37:10 -04:00
|
|
|
|
}
|
|
|
|
|
|
addEventListener('mousemove',e=>{
|
2026-07-19 21:28:14 -04:00
|
|
|
|
mx=(e.clientX/innerWidth-0.5)*2; my=-(e.clientY/innerHeight-0.5)*2; update();
|
2026-07-19 13:37:10 -04:00
|
|
|
|
});
|
2026-07-19 21:28:14 -04:00
|
|
|
|
const gapI=document.getElementById('gap'), parI=document.getElementById('par'), brI=document.getElementById('bright');
|
|
|
|
|
|
gapI.oninput=()=>{gap=+gapI.value; document.getElementById('gv').textContent=' '+gap; update();};
|
|
|
|
|
|
parI.oninput=()=>{par=+parI.value; document.getElementById('pv').textContent=' '+par; update();};
|
|
|
|
|
|
brI.oninput=()=>{bright=+brI.value; document.getElementById('bv').textContent=' '+bright.toFixed(2); update();};
|
2026-07-19 13:37:10 -04:00
|
|
|
|
document.getElementById('gv').textContent=' '+gap;
|
2026-07-19 21:28:14 -04:00
|
|
|
|
document.getElementById('pv').textContent=' '+par;
|
2026-07-19 13:37:10 -04:00
|
|
|
|
document.getElementById('bv').textContent=' '+bright.toFixed(2);
|
2026-07-19 21:28:14 -04:00
|
|
|
|
update();
|
2026-07-19 13:37:10 -04:00
|
|
|
|
</script></body></html>`);
|
|
|
|
|
|
|
|
|
|
|
|
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`);
|