13 Layers.

This commit is contained in:
2026-07-19 21:28:14 -04:00
parent 2a4fffbb2a
commit 9460943a26
46 changed files with 1323 additions and 100 deletions

View File

@@ -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",

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 206 KiB

After

Width:  |  Height:  |  Size: 412 KiB

View File

Before

Width:  |  Height:  |  Size: 4.5 MiB

After

Width:  |  Height:  |  Size: 4.5 MiB

View File

Before

Width:  |  Height:  |  Size: 396 KiB

After

Width:  |  Height:  |  Size: 396 KiB

View File

Before

Width:  |  Height:  |  Size: 396 KiB

After

Width:  |  Height:  |  Size: 396 KiB

View File

Before

Width:  |  Height:  |  Size: 396 KiB

After

Width:  |  Height:  |  Size: 396 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 1.4 MiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 1.4 MiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 1.7 MiB

View File

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@@ -1,14 +1,13 @@
<!DOCTYPE html><html><head><meta charset="utf-8">
<title>Proof1 · lit chamber — backlit parallax</title>
<style>
:root{--gap:6;--bright:1;}
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;}
.scene{perspective:2200px;perspective-origin:50% 45%;}
.scene{perspective:1400px;perspective-origin:50% 45%;}
.stack{position:relative;transform-style:preserve-3d;transition:transform .06s linear;
width:500.5px;height:810px;}
.panel{position:absolute;inset:-14%;transform:translateZ(-120px);
.panel{position:absolute;inset:-70%;
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;
@@ -27,7 +26,9 @@
<h1>Proof1 · lit chamber</h1>
<div>move mouse = parallax · drag sliders</div>
<label>air gap ×<span id="gv"></span></label>
<input id="gap" type="range" min="1" max="18" step="0.5" value="6">
<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">
<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>
@@ -37,7 +38,7 @@
<div class="panel" id="panel"></div>
</div></div></div>
<script>
const PLANES=[{"id":"sun","file":"plane-1-sun.svg","z":0,"blur":1.4,"dim":0.95,"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,"dim":0.85,"white":"none","glow":0.9},{"id":"event-core","file":"plane-6-event-core.svg","z":30,"blur":0,"dim":1,"white":"under","glow":0.5},{"id":"front-glass","file":"plane-7-front-glass.svg","z":30,"blur":0,"dim":1,"white":"none","glow":0,"blend":"multiply"}];
const PLANES=[{"id":"sun","file":"plane-1-sun.svg","z":0,"blur":1.4,"dim":1,"white":"none","glow":1},{"id":"event-glow","file":"plane-2-event-glow.svg","z":8,"blur":2.6,"dim":0.85,"white":"none","glow":0.9},{"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":"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-9-front-glass.svg","z":36,"blur":0,"dim":1,"white":"none","glow":0,"blend":"multiply"}];
const stack=document.getElementById('stack');
const legend=document.getElementById('legend');
const pills=document.getElementById('pills');
@@ -52,30 +53,38 @@ PLANES.forEach(p=>{
pill.onclick=()=>{d.dataset.off=d.dataset.off?'':'1'; pill.classList.toggle('off'); layout();};
pills.appendChild(pill);
});
let gap=6, bright=1, mx=0, my=0;
function layout(){
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)';
for(const p of PLANES){
const e=els[p.id];
const z=(p.z-maxZ)*gap; // front plane at 0, others recede
const blur=(p.blur||0)*(gap/6);
const off=e.dataset.off?' opacity:0;':'';
e.style.transform='translateZ('+z+'px)';
e.style.filter='blur('+blur.toFixed(2)+'px)';
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)';
e.style.opacity=e.dataset.off?0:(p.dim??1);
e.style.mixBlendMode=p.blend||'normal';
}
document.getElementById('panel').style.filter='blur(2px) brightness('+bright+')';
}
function parallax(){
stack.style.transform='rotateX('+(my*7)+'deg) rotateY('+(mx*9)+'deg)';
// 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+')';
}
addEventListener('mousemove',e=>{
mx=(e.clientX/innerWidth-0.5)*2; my=-(e.clientY/innerHeight-0.5)*2; parallax();
mx=(e.clientX/innerWidth-0.5)*2; my=-(e.clientY/innerHeight-0.5)*2; update();
});
const gapI=document.getElementById('gap'), brI=document.getElementById('bright');
gapI.oninput=()=>{gap=+gapI.value; document.getElementById('gv').textContent=' '+gap; layout();};
brI.oninput=()=>{bright=+brI.value; document.getElementById('bv').textContent=' '+bright.toFixed(2); layout();};
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();};
document.getElementById('gv').textContent=' '+gap;
document.getElementById('pv').textContent=' '+par;
document.getElementById('bv').textContent=' '+bright.toFixed(2);
layout(); parallax();
update();
</script></body></html>

137
fable/Proof2/manifest.json Normal file
View File

@@ -0,0 +1,137 @@
{
"piece": "Proof2 — the lit chamber (7 time-shells)",
"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"
}
]
}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 412 KiB

View File

Before

Width:  |  Height:  |  Size: 4.5 MiB

After

Width:  |  Height:  |  Size: 4.5 MiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 396 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 452 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 396 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 427 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 396 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 574 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 606 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 650 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 761 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 1.3 MiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1001" height="1620" viewBox="0 0 1001 1620"><image x="0" y="0" width="1001" height="1620" href="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMDAxIiBoZWlnaHQ9IjE2MjAiIHZpZXdCb3g9IjAgMCAxMDAxIDE2MjAiPgo8ZGVmcz4KIDxmaWx0ZXIgaWQ9ImR1c3QiIHg9IjAiIHk9IjAiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiPjxmZVR1cmJ1bGVuY2UgdHlwZT0idHVyYnVsZW5jZSIgYmFzZUZyZXF1ZW5jeT0iMC41IiBudW1PY3RhdmVzPSIyIiBzZWVkPSIxMyIgc3RpdGNoVGlsZXM9InN0aXRjaCIgcmVzdWx0PSJuIi8+PGZlQ29sb3JNYXRyaXggaW49Im4iIHR5cGU9Im1hdHJpeCIgdmFsdWVzPSIwIDAgMCAwIDAuMjM1IDAgMCAwIDAgMC4xODggMCAwIDAgMCAwLjEzMyAwIDAgMCAwLjMwMCAtMC4xOCIvPjwvZmlsdGVyPgogPGZpbHRlciBpZD0iZm94IiB4PSIwIiB5PSIwIiB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIj48ZmVUdXJidWxlbmNlIHR5cGU9ImZyYWN0YWxOb2lzZSIgYmFzZUZyZXF1ZW5jeT0iMC4wMDQiIG51bU9jdGF2ZXM9IjMiIHNlZWQ9IjE3IiBzdGl0Y2hUaWxlcz0ic3RpdGNoIiByZXN1bHQ9Im4iLz48ZmVDb2xvck1hdHJpeCBpbj0ibiIgdHlwZT0ibWF0cml4IiB2YWx1ZXM9IjAgMCAwIDAgMC40MiAwIDAgMCAwIDAuMzAgMCAwIDAgMCAwLjE2IDAgMCAwIDAuMTk4IC0wLjEyIi8+PC9maWx0ZXI+CjwvZGVmcz4KPHJlY3Qgd2lkdGg9IjEwMDEiIGhlaWdodD0iMTYyMCIgZmlsdGVyPSJ1cmwoI2ZveCkiLz4KPHJlY3Qgd2lkdGg9IjEwMDEiIGhlaWdodD0iMTYyMCIgZmlsdGVyPSJ1cmwoI2R1c3QpIi8+CjxsaW5lIHgxPSI0MTEiIHkxPSIzNDQiIHgyPSI0MzAiIHkyPSIxMjQ3IiBzdHJva2U9InJnYig2MCw0OCwzNCkiIHN0cm9rZS1vcGFjaXR5PSIwLjA5MCIgc3Ryb2tlLXdpZHRoPSIwLjc5Ii8+PGxpbmUgeDE9IjcwMiIgeTE9IjY4IiB4Mj0iNjg3IiB5Mj0iMTQzNyIgc3Ryb2tlPSJyZ2IoNjAsNDgsMzQpIiBzdHJva2Utb3BhY2l0eT0iMC4wNTEiIHN0cm9rZS13aWR0aD0iMC43NyIvPjxsaW5lIHgxPSI0MjIiIHkxPSI1MTQiIHgyPSI0MzciIHkyPSIxNzU3IiBzdHJva2U9InJnYig2MCw0OCwzNCkiIHN0cm9rZS1vcGFjaXR5PSIwLjE2NSIgc3Ryb2tlLXdpZHRoPSIxLjAzIi8+PGxpbmUgeDE9IjEwIiB5MT0iNTIzIiB4Mj0iMTkiIHkyPSIxODY5IiBzdHJva2U9InJnYig2MCw0OCwzNCkiIHN0cm9rZS1vcGFjaXR5PSIwLjE1MCIgc3Ryb2tlLXdpZHRoPSIwLjUzIi8+PGxpbmUgeDE9IjQzNCIgeTE9IjI4NSIgeDI9IjQyMSIgeTI9IjE0NDIiIHN0cm9rZT0icmdiKDYwLDQ4LDM0KSIgc3Ryb2tlLW9wYWNpdHk9IjAuMTU3IiBzdHJva2Utd2lkdGg9IjAuNzAiLz48bGluZSB4MT0iMTExIiB5MT0iMTgwIiB4Mj0iOTUiIHkyPSIxNTk0IiBzdHJva2U9InJnYig2MCw0OCwzNCkiIHN0cm9rZS1vcGFjaXR5PSIwLjA4NiIgc3Ryb2tlLXdpZHRoPSIwLjkxIi8+PGxpbmUgeDE9IjE0MCIgeTE9IjE2NCIgeDI9IjE1OCIgeTI9Ijc1OCIgc3Ryb2tlPSJyZ2IoNjAsNDgsMzQpIiBzdHJva2Utb3BhY2l0eT0iMC4xMDUiIHN0cm9rZS13aWR0aD0iMS4wNCIvPjxsaW5lIHgxPSIxOTIiIHkxPSIzNjMiIHgyPSIyMDciIHkyPSIxNDg5IiBzdHJva2U9InJnYig2MCw0OCwzNCkiIHN0cm9rZS1vcGFjaXR5PSIwLjA3NSIgc3Ryb2tlLXdpZHRoPSIxLjAzIi8+PC9zdmc+"/><g opacity="0.85"><image x="0" y="0" width="1001" height="1620" href="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMDAxIiBoZWlnaHQ9IjE2MjAiIHZpZXdCb3g9IjAgMCAxMDAxIDE2MjAiPgo8ZGVmcz48ZmlsdGVyIGlkPSJnIiB4PSIwIiB5PSIwIiB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIj4KPGZlVHVyYnVsZW5jZSB0eXBlPSJmcmFjdGFsTm9pc2UiIGJhc2VGcmVxdWVuY3k9IjAuOSIgbnVtT2N0YXZlcz0iMiIgc2VlZD0iMTkiIHN0aXRjaFRpbGVzPSJzdGl0Y2giIHJlc3VsdD0ibiIvPgo8ZmVDb2xvck1hdHJpeCBpbj0ibiIgdHlwZT0ibWF0cml4IiB2YWx1ZXM9IjAgMCAwIDAgMC4xNDkgMCAwIDAgMCAwLjEyNSAwIDAgMCAwIDAuMTAyIDAgMCAwIDAuNTIgMCIvPgo8L2ZpbHRlcj48L2RlZnM+PHJlY3Qgd2lkdGg9IjEwMDEiIGhlaWdodD0iMTYyMCIgZmlsdGVyPSJ1cmwoI2cpIi8+PC9zdmc+"/></g></svg>

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@@ -0,0 +1,90 @@
<!DOCTYPE html><html><head><meta charset="utf-8">
<title>Proof2 · 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;}
.scene{perspective:1400px;perspective-origin:50% 45%;}
.stack{position:relative;transform-style:preserve-3d;transition:transform .06s linear;
width:500.5px;height:810px;}
.panel{position:absolute;inset:-70%;
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>Proof2 · lit chamber</h1>
<div>move mouse = parallax · drag sliders</div>
<label>air gap ×<span id="gv"></span></label>
<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">
<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=[{"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"}];
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);
});
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)';
for(const p of PLANES){
const e=els[p.id];
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)';
e.style.opacity=e.dataset.off?0:(p.dim??1);
e.style.mixBlendMode=p.blend||'normal';
}
// 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+')';
}
addEventListener('mousemove',e=>{
mx=(e.clientX/innerWidth-0.5)*2; my=-(e.clientY/innerHeight-0.5)*2; update();
});
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();};
document.getElementById('gv').textContent=' '+gap;
document.getElementById('pv').textContent=' '+par;
document.getElementById('bv').textContent=' '+bright.toFixed(2);
update();
</script></body></html>

137
fable/Proof3/manifest.json Normal file
View File

@@ -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"
}
]
}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 412 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 4.5 MiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 396 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 606 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 396 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 705 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 396 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 721 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 700 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 693 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 684 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 680 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1001" height="1620" viewBox="0 0 1001 1620"><image x="0" y="0" width="1001" height="1620" href="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMDAxIiBoZWlnaHQ9IjE2MjAiIHZpZXdCb3g9IjAgMCAxMDAxIDE2MjAiPgo8ZGVmcz4KIDxmaWx0ZXIgaWQ9ImR1c3QiIHg9IjAiIHk9IjAiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiPjxmZVR1cmJ1bGVuY2UgdHlwZT0idHVyYnVsZW5jZSIgYmFzZUZyZXF1ZW5jeT0iMC41IiBudW1PY3RhdmVzPSIyIiBzZWVkPSIxMyIgc3RpdGNoVGlsZXM9InN0aXRjaCIgcmVzdWx0PSJuIi8+PGZlQ29sb3JNYXRyaXggaW49Im4iIHR5cGU9Im1hdHJpeCIgdmFsdWVzPSIwIDAgMCAwIDAuMjM1IDAgMCAwIDAgMC4xODggMCAwIDAgMCAwLjEzMyAwIDAgMCAwLjMwMCAtMC4xOCIvPjwvZmlsdGVyPgogPGZpbHRlciBpZD0iZm94IiB4PSIwIiB5PSIwIiB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIj48ZmVUdXJidWxlbmNlIHR5cGU9ImZyYWN0YWxOb2lzZSIgYmFzZUZyZXF1ZW5jeT0iMC4wMDQiIG51bU9jdGF2ZXM9IjMiIHNlZWQ9IjE3IiBzdGl0Y2hUaWxlcz0ic3RpdGNoIiByZXN1bHQ9Im4iLz48ZmVDb2xvck1hdHJpeCBpbj0ibiIgdHlwZT0ibWF0cml4IiB2YWx1ZXM9IjAgMCAwIDAgMC40MiAwIDAgMCAwIDAuMzAgMCAwIDAgMCAwLjE2IDAgMCAwIDAuMTk4IC0wLjEyIi8+PC9maWx0ZXI+CjwvZGVmcz4KPHJlY3Qgd2lkdGg9IjEwMDEiIGhlaWdodD0iMTYyMCIgZmlsdGVyPSJ1cmwoI2ZveCkiLz4KPHJlY3Qgd2lkdGg9IjEwMDEiIGhlaWdodD0iMTYyMCIgZmlsdGVyPSJ1cmwoI2R1c3QpIi8+CjxsaW5lIHgxPSI0MTEiIHkxPSIzNDQiIHgyPSI0MzAiIHkyPSIxMjQ3IiBzdHJva2U9InJnYig2MCw0OCwzNCkiIHN0cm9rZS1vcGFjaXR5PSIwLjA5MCIgc3Ryb2tlLXdpZHRoPSIwLjc5Ii8+PGxpbmUgeDE9IjcwMiIgeTE9IjY4IiB4Mj0iNjg3IiB5Mj0iMTQzNyIgc3Ryb2tlPSJyZ2IoNjAsNDgsMzQpIiBzdHJva2Utb3BhY2l0eT0iMC4wNTEiIHN0cm9rZS13aWR0aD0iMC43NyIvPjxsaW5lIHgxPSI0MjIiIHkxPSI1MTQiIHgyPSI0MzciIHkyPSIxNzU3IiBzdHJva2U9InJnYig2MCw0OCwzNCkiIHN0cm9rZS1vcGFjaXR5PSIwLjE2NSIgc3Ryb2tlLXdpZHRoPSIxLjAzIi8+PGxpbmUgeDE9IjEwIiB5MT0iNTIzIiB4Mj0iMTkiIHkyPSIxODY5IiBzdHJva2U9InJnYig2MCw0OCwzNCkiIHN0cm9rZS1vcGFjaXR5PSIwLjE1MCIgc3Ryb2tlLXdpZHRoPSIwLjUzIi8+PGxpbmUgeDE9IjQzNCIgeTE9IjI4NSIgeDI9IjQyMSIgeTI9IjE0NDIiIHN0cm9rZT0icmdiKDYwLDQ4LDM0KSIgc3Ryb2tlLW9wYWNpdHk9IjAuMTU3IiBzdHJva2Utd2lkdGg9IjAuNzAiLz48bGluZSB4MT0iMTExIiB5MT0iMTgwIiB4Mj0iOTUiIHkyPSIxNTk0IiBzdHJva2U9InJnYig2MCw0OCwzNCkiIHN0cm9rZS1vcGFjaXR5PSIwLjA4NiIgc3Ryb2tlLXdpZHRoPSIwLjkxIi8+PGxpbmUgeDE9IjE0MCIgeTE9IjE2NCIgeDI9IjE1OCIgeTI9Ijc1OCIgc3Ryb2tlPSJyZ2IoNjAsNDgsMzQpIiBzdHJva2Utb3BhY2l0eT0iMC4xMDUiIHN0cm9rZS13aWR0aD0iMS4wNCIvPjxsaW5lIHgxPSIxOTIiIHkxPSIzNjMiIHgyPSIyMDciIHkyPSIxNDg5IiBzdHJva2U9InJnYig2MCw0OCwzNCkiIHN0cm9rZS1vcGFjaXR5PSIwLjA3NSIgc3Ryb2tlLXdpZHRoPSIxLjAzIi8+PC9zdmc+"/><g opacity="0.85"><image x="0" y="0" width="1001" height="1620" href="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMDAxIiBoZWlnaHQ9IjE2MjAiIHZpZXdCb3g9IjAgMCAxMDAxIDE2MjAiPgo8ZGVmcz48ZmlsdGVyIGlkPSJnIiB4PSIwIiB5PSIwIiB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIj4KPGZlVHVyYnVsZW5jZSB0eXBlPSJmcmFjdGFsTm9pc2UiIGJhc2VGcmVxdWVuY3k9IjAuOSIgbnVtT2N0YXZlcz0iMiIgc2VlZD0iMTkiIHN0aXRjaFRpbGVzPSJzdGl0Y2giIHJlc3VsdD0ibiIvPgo8ZmVDb2xvck1hdHJpeCBpbj0ibiIgdHlwZT0ibWF0cml4IiB2YWx1ZXM9IjAgMCAwIDAgMC4xNDkgMCAwIDAgMCAwLjEyNSAwIDAgMCAwIDAuMTAyIDAgMCAwIDAuNTIgMCIvPgo8L2ZpbHRlcj48L2RlZnM+PHJlY3Qgd2lkdGg9IjEwMDEiIGhlaWdodD0iMTYyMCIgZmlsdGVyPSJ1cmwoI2cpIi8+PC9zdmc+"/></g></svg>

After

Width:  |  Height:  |  Size: 3.0 KiB

101
fable/Proof3/simulator.html Normal file
View File

@@ -0,0 +1,101 @@
<!DOCTYPE html><html><head><meta charset="utf-8">
<title>Proof3 · 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;}
.scene{perspective:1400px;perspective-origin:50% 45%;}
.stack{position:relative;transform-style:preserve-3d;transition:transform .06s linear;
width:500.5px;height:810px;}
.panel{position:absolute;inset:-70%;
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;}
#layers{margin-top:10px;max-height:44vh;overflow:auto;padding-right:4px;}
.lay{display:flex;align-items:center;gap:7px;margin:1px 0;}
.lay span{width:82px;color:#aab;font-size:11px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
.lay input[type=range]{flex:1;}
.lay b{width:26px;text-align:right;color:#778;font-size:10px;}
</style></head><body>
<div id="hud">
<h1>Proof3 · lit chamber</h1>
<div>move mouse = parallax · drag sliders</div>
<label>air gap ×<span id="gv"></span></label>
<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">
<label>backlight<span id="bv"></span></label>
<input id="bright" type="range" min="0.3" max="1.8" step="0.05" value="1">
<label>layer opacity (front → back)</label>
<div id="layers"></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=[{"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"}];
const stack=document.getElementById('stack');
const legend=document.getElementById('legend');
const layers=document.getElementById('layers');
const maxZ=Math.max(...PLANES.map(p=>p.z));
const els={}, op={}; // op[id] = per-layer opacity override (a slider each)
legend.innerHTML='<b>planes</b> (front → back)<br>'+[...PLANES].reverse().map(p=>p.id+' · '+p.z+'mm').join('<br>');
// build planes back→front for correct DOM paint order, sliders front→back for reading
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;
op[p.id]=(p.dim??1);
});
[...PLANES].reverse().forEach(p=>{
const row=document.createElement('div'); row.className='lay';
const lab=document.createElement('span'); lab.textContent=p.id;
const sl=document.createElement('input'); sl.type='range'; sl.min=0; sl.max=1; sl.step=0.05; sl.value=op[p.id];
const val=document.createElement('b'); val.textContent=op[p.id].toFixed(2);
sl.oninput=()=>{op[p.id]=+sl.value; val.textContent=op[p.id].toFixed(2); update();};
row.appendChild(lab); row.appendChild(sl); row.appendChild(val); layers.appendChild(row);
});
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)';
for(const p of PLANES){
const e=els[p.id];
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)';
e.style.opacity=op[p.id];
e.style.mixBlendMode=p.blend||'normal';
}
// 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+')';
}
addEventListener('mousemove',e=>{
mx=(e.clientX/innerWidth-0.5)*2; my=-(e.clientY/innerHeight-0.5)*2; update();
});
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();};
document.getElementById('gv').textContent=' '+gap;
document.getElementById('pv').textContent=' '+par;
document.getElementById('bv').textContent=' '+bright.toFixed(2);
update();
</script></body></html>

View File

@@ -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.70.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
];
}

View File

@@ -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) {

View File

@@ -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) => `<image x="0" y="0" width="${W}" height="${H}" href="${data
const frontGlass = planeSVG(
`${IMG(aging)}<g opacity="0.85">${IMG(grain)}</g>`);
// 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)}<g opacity="0.7">${IMG(diskImg)}</g>`);
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`, `<!DOCTYPE html><html><head><meta charset="utf-8">
<title>Proof1 · lit chamber — backlit parallax</title>
<style>
:root{--gap:6;--bright:1;}
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;}
.scene{perspective:2200px;perspective-origin:50% 45%;}
.scene{perspective:1400px;perspective-origin:50% 45%;}
.stack{position:relative;transform-style:preserve-3d;transition:transform .06s linear;
width:${W / 2}px;height:${H / 2}px;}
.panel{position:absolute;inset:-14%;transform:translateZ(-120px);
.panel{position:absolute;inset:-70%;
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;
@@ -207,7 +226,9 @@ writeFileSync(`${OUT}/simulator.html`, `<!DOCTYPE html><html><head><meta charset
<h1>Proof1 · lit chamber</h1>
<div>move mouse = parallax · drag sliders</div>
<label>air gap ×<span id="gv"></span></label>
<input id="gap" type="range" min="1" max="18" step="0.5" value="6">
<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">
<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>
@@ -232,32 +253,40 @@ PLANES.forEach(p=>{
pill.onclick=()=>{d.dataset.off=d.dataset.off?'':'1'; pill.classList.toggle('off'); layout();};
pills.appendChild(pill);
});
let gap=6, bright=1, mx=0, my=0;
function layout(){
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)';
for(const p of PLANES){
const e=els[p.id];
const z=(p.z-maxZ)*gap; // front plane at 0, others recede
const blur=(p.blur||0)*(gap/6);
const off=e.dataset.off?' opacity:0;':'';
e.style.transform='translateZ('+z+'px)';
e.style.filter='blur('+blur.toFixed(2)+'px)';
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)';
e.style.opacity=e.dataset.off?0:(p.dim??1);
e.style.mixBlendMode=p.blend||'normal';
}
document.getElementById('panel').style.filter='blur(2px) brightness('+bright+')';
}
function parallax(){
stack.style.transform='rotateX('+(my*7)+'deg) rotateY('+(mx*9)+'deg)';
// 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+')';
}
addEventListener('mousemove',e=>{
mx=(e.clientX/innerWidth-0.5)*2; my=-(e.clientY/innerHeight-0.5)*2; parallax();
mx=(e.clientX/innerWidth-0.5)*2; my=-(e.clientY/innerHeight-0.5)*2; update();
});
const gapI=document.getElementById('gap'), brI=document.getElementById('bright');
gapI.oninput=()=>{gap=+gapI.value; document.getElementById('gv').textContent=' '+gap; layout();};
brI.oninput=()=>{bright=+brI.value; document.getElementById('bv').textContent=' '+bright.toFixed(2); layout();};
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();};
document.getElementById('gv').textContent=' '+gap;
document.getElementById('pv').textContent=' '+par;
document.getElementById('bv').textContent=' '+bright.toFixed(2);
layout(); parallax();
update();
</script></body></html>`);
console.log(`PROOF1 — ${W}×${H} (φ) — ${PLANES.length} planes → ${OUT}/`);

305
tools/proof2-render.mjs Normal file
View File

@@ -0,0 +1,305 @@
/* ============================================================
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 `<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 };
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) =>
`<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>`);
// 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)}<g opacity="0.7">${IMG(diskImg)}</g>`);
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`, `<!DOCTYPE html><html><head><meta charset="utf-8">
<title>Proof2 · 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;}
.scene{perspective:1400px;perspective-origin:50% 45%;}
.stack{position:relative;transform-style:preserve-3d;transition:transform .06s linear;
width:${W / 2}px;height:${H / 2}px;}
.panel{position:absolute;inset:-70%;
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>Proof2 · lit chamber</h1>
<div>move mouse = parallax · drag sliders</div>
<label>air gap ×<span id="gv"></span></label>
<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">
<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);
});
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)';
for(const p of PLANES){
const e=els[p.id];
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)';
e.style.opacity=e.dataset.off?0:(p.dim??1);
e.style.mixBlendMode=p.blend||'normal';
}
// 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+')';
}
addEventListener('mousemove',e=>{
mx=(e.clientX/innerWidth-0.5)*2; my=-(e.clientY/innerHeight-0.5)*2; update();
});
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();};
document.getElementById('gv').textContent=' '+gap;
document.getElementById('pv').textContent=' '+par;
document.getElementById('bv').textContent=' '+bright.toFixed(2);
update();
</script></body></html>`);
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`);

349
tools/proof3-render.mjs Normal file
View File

@@ -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 `<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 };
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) =>
`<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>`);
// 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)}<g opacity="0.7">${IMG(diskImg)}</g>`);
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`, `<!DOCTYPE html><html><head><meta charset="utf-8">
<title>Proof3 · 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;}
.scene{perspective:1400px;perspective-origin:50% 45%;}
.stack{position:relative;transform-style:preserve-3d;transition:transform .06s linear;
width:${W / 2}px;height:${H / 2}px;}
.panel{position:absolute;inset:-70%;
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;}
#layers{margin-top:10px;max-height:44vh;overflow:auto;padding-right:4px;}
.lay{display:flex;align-items:center;gap:7px;margin:1px 0;}
.lay span{width:82px;color:#aab;font-size:11px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
.lay input[type=range]{flex:1;}
.lay b{width:26px;text-align:right;color:#778;font-size:10px;}
</style></head><body>
<div id="hud">
<h1>Proof3 · lit chamber</h1>
<div>move mouse = parallax · drag sliders</div>
<label>air gap ×<span id="gv"></span></label>
<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">
<label>backlight<span id="bv"></span></label>
<input id="bright" type="range" min="0.3" max="1.8" step="0.05" value="1">
<label>layer opacity (front → back)</label>
<div id="layers"></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 layers=document.getElementById('layers');
const maxZ=Math.max(...PLANES.map(p=>p.z));
const els={}, op={}; // op[id] = per-layer opacity override (a slider each)
legend.innerHTML='<b>planes</b> (front → back)<br>'+[...PLANES].reverse().map(p=>p.id+' · '+p.z+'mm').join('<br>');
// build planes back→front for correct DOM paint order, sliders front→back for reading
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;
op[p.id]=(p.dim??1);
});
[...PLANES].reverse().forEach(p=>{
const row=document.createElement('div'); row.className='lay';
const lab=document.createElement('span'); lab.textContent=p.id;
const sl=document.createElement('input'); sl.type='range'; sl.min=0; sl.max=1; sl.step=0.05; sl.value=op[p.id];
const val=document.createElement('b'); val.textContent=op[p.id].toFixed(2);
sl.oninput=()=>{op[p.id]=+sl.value; val.textContent=op[p.id].toFixed(2); update();};
row.appendChild(lab); row.appendChild(sl); row.appendChild(val); layers.appendChild(row);
});
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)';
for(const p of PLANES){
const e=els[p.id];
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)';
e.style.opacity=op[p.id];
e.style.mixBlendMode=p.blend||'normal';
}
// 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+')';
}
addEventListener('mousemove',e=>{
mx=(e.clientX/innerWidth-0.5)*2; my=-(e.clientY/innerHeight-0.5)*2; update();
});
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();};
document.getElementById('gv').textContent=' '+gap;
document.getElementById('pv').textContent=' '+par;
document.getElementById('bv').textContent=' '+bright.toFixed(2);
update();
</script></body></html>`);
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`);