Files
bubblechambersimart/fable/Proof3/simulator.html
2026-07-19 21:28:14 -04:00

101 lines
6.7 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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>