Spaces:
Running
Running
| <meta charset="utf-8"> | |
| <meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no"> | |
| <title>ANTTI'S BRAIN — DESCENT · infinite procedural key-hunt</title> | |
| <style> | |
| :root{ | |
| --void:#04060b; --ink:#c9d4e0; --amber:#e8b13a; --amber-dim:#8a6d2c; | |
| --cyan:#54d2e2; --line:#1b2436; --dim:#67788d; --green:#4ec98f; --red:#e06060; | |
| --gold:#ffd76a; | |
| } | |
| *{box-sizing:border-box} | |
| html,body{margin:0;height:100%;background:var(--void);overflow:hidden; | |
| font-family:ui-monospace,"SF Mono",Menlo,Consolas,monospace;color:var(--ink)} | |
| #gl{position:fixed;inset:0;width:100%;height:100%;display:block;cursor:grab} | |
| #gl.drag{cursor:grabbing} | |
| .panel{position:fixed;background:rgba(6,9,16,.74);border:1px solid var(--line); | |
| backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px)} | |
| #hud{top:14px;left:14px;padding:12px 14px;min-width:270px;font-size:11px;line-height:1.7} | |
| #hud .ttl{color:var(--amber);letter-spacing:.16em;font-size:10px; | |
| text-transform:uppercase;margin-bottom:8px;display:flex;justify-content:space-between;gap:18px} | |
| #hud .ttl b{color:var(--amber-dim);font-weight:400} | |
| #hud .row{display:flex;justify-content:space-between;color:#8494a8;gap:14px} | |
| #hud .row span{color:var(--ink)} | |
| #hud .row .k{color:var(--dim)} | |
| #hud .row .room{color:var(--cyan);letter-spacing:.08em} | |
| #hud .row .lvl{color:var(--gold);letter-spacing:.12em} | |
| #hud .row .keyok{color:var(--green)} | |
| #hud .row .keyno{color:var(--gold)} | |
| #resWrap{margin-top:6px} | |
| #resWrap label{display:flex;justify-content:space-between;color:var(--dim);font-size:10px;margin-bottom:3px} | |
| #resWrap label span{color:var(--gold)} | |
| #resBar{height:4px;background:var(--line);position:relative;overflow:hidden} | |
| #resFill{position:absolute;inset:0;width:0%;background:linear-gradient(90deg,#8a6d2c,var(--gold)); | |
| transition:width .25s} | |
| #hud .note{margin-top:7px;border-top:1px solid var(--line);padding-top:6px; | |
| color:var(--dim);font-size:10px;line-height:1.6;max-width:300px} | |
| #ctl{bottom:14px;left:14px;padding:12px 14px;width:288px;font-size:11px} | |
| #ctl h2{margin:0 0 9px;font-size:10px;letter-spacing:.16em;text-transform:uppercase; | |
| color:var(--cyan);font-weight:600} | |
| .modes{display:flex;gap:6px;margin-bottom:11px} | |
| .modes button{flex:1} | |
| .field{margin:8px 0} | |
| .field label{display:flex;justify-content:space-between;color:#8494a8;margin-bottom:4px} | |
| .field label var{color:var(--ink);font-style:normal} | |
| input[type=range]{width:100%;height:2px;-webkit-appearance:none;appearance:none; | |
| background:var(--line);outline:none;margin:6px 0} | |
| input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;width:12px;height:12px; | |
| border-radius:50%;background:var(--amber);cursor:pointer;border:2px solid var(--void)} | |
| input[type=range]::-moz-range-thumb{width:12px;height:12px;border-radius:50%; | |
| background:var(--amber);cursor:pointer;border:2px solid var(--void)} | |
| .btns{display:flex;gap:6px;margin-top:9px;flex-wrap:wrap} | |
| button{flex:1;min-width:56px;padding:7px 4px;background:transparent;color:var(--ink); | |
| border:1px solid var(--line);font:inherit;font-size:10px;letter-spacing:.05em; | |
| cursor:pointer;transition:.15s;text-transform:uppercase} | |
| button:hover{border-color:var(--amber-dim);color:var(--amber)} | |
| button.on{border-color:var(--cyan);color:var(--cyan)} | |
| button:focus-visible{outline:2px solid var(--cyan);outline-offset:2px} | |
| #stat{margin-top:7px;font-size:10px;color:var(--dim);min-height:13px;word-break:break-all} | |
| #help{bottom:14px;right:14px;padding:11px 13px;font-size:10.5px;line-height:1.85; | |
| color:#73859a;text-align:right} | |
| #help b{color:var(--amber)} | |
| #banner{position:fixed;top:38%;left:0;right:0;text-align:center;pointer-events:none; | |
| opacity:0;transition:opacity .5s;z-index:8} | |
| #banner .b1{font-size:22px;letter-spacing:.4em;color:var(--gold);text-transform:uppercase; | |
| text-shadow:0 0 24px rgba(255,215,106,.55)} | |
| #banner .b2{font-size:11px;color:var(--dim);margin-top:8px;letter-spacing:.14em} | |
| #banner.show{opacity:1} | |
| #boot{position:fixed;inset:0;display:flex;flex-direction:column;gap:10px; | |
| align-items:center;justify-content:center;background:rgba(4,6,11,.92);z-index:9; | |
| transition:opacity .8s;text-align:center;padding:20px} | |
| #boot.gone{opacity:0;pointer-events:none} | |
| #boot .big{font-size:15px;letter-spacing:.28em;color:var(--amber)} | |
| #boot .sub{font-size:11px;color:var(--dim);line-height:1.9;max-width:520px} | |
| #boot .sub em{color:var(--gold);font-style:normal} | |
| #bootStat{font-size:11px;color:var(--cyan);letter-spacing:.12em;min-height:14px} | |
| #boot.err .big{color:var(--red)} | |
| #boot.err .sub{color:#c99;word-break:break-all;text-align:left} | |
| </style> | |
| <canvas id="gl"></canvas> | |
| <div id="boot"> | |
| <div class="big">ANTTI'S BRAIN — DESCENT</div> | |
| <div class="sub">an endless chain of chambers, hallucinated from one seed.<br> | |
| each level is a run of rooms. somewhere in it, one crystal is not like the others:<br> | |
| it pulses <em>gold</em>. touch it, and the red seal on the far gate dissolves.<br> | |
| pass the gate: next level. rooms grow heavier, more twisted, stranger in shape.<br><br> | |
| the <em>resonance</em> meter runs hot as you near the key. no enemies, no clocks —<br> | |
| only geometry, depth, and the hash. nothing is stored; walk back and every<br> | |
| room is re-dreamed identically. click / drag to enter · mouse look is inverted.<br><br> | |
| <em>first launch:</em> the GPU driver compiles the shader once, then caches it.<br> | |
| the world is now ONE branchless shape kernel — this should take seconds, not<br> | |
| minutes. the counter below is honest elapsed time either way.</div> | |
| <div id="bootStat">waking up…</div> | |
| </div> | |
| <div id="banner"><div class="b1" id="bn1"></div><div class="b2" id="bn2"></div></div> | |
| <div id="hud" class="panel"> | |
| <div class="ttl">Brain — Descent <b id="fps">— fps</b></div> | |
| <div class="row"><span class="k">level</span><span class="lvl" id="lvlV">1</span></div> | |
| <div class="row"><span class="k">room in level</span><span id="progV">—</span></div> | |
| <div class="row"><span class="k">chamber</span><span class="room" id="roomV">—</span></div> | |
| <div class="row"><span class="k">index / shape</span><span id="roomI">—</span></div> | |
| <div class="row"><span class="k">mass · twist</span><span id="massV">—</span></div> | |
| <div class="row"><span class="k">key crystal</span><span id="keyV" class="keyno">not found</span></div> | |
| <div id="resWrap"><label>resonance <span id="resT">—</span></label> | |
| <div id="resBar"><div id="resFill"></div></div></div> | |
| <div class="row" style="margin-top:6px"><span class="k">mode</span><span id="modeV">Boundary</span></div> | |
| <div class="row"><span class="k">boundary</span><span id="src">—</span></div> | |
| <div class="row"><span class="k">music</span><span id="musV">—</span></div> | |
| <div class="row"><span class="k">position</span><span id="pos">—</span></div> | |
| <div class="note" id="modeNote">—</div> | |
| </div> | |
| <div id="ctl" class="panel"> | |
| <h2>The descent</h2> | |
| <div class="modes"> | |
| <button id="m0" class="on">Boundary</button> | |
| <button id="m1">Projection</button> | |
| <button id="m2">Cascade</button> | |
| <button id="m3">Glass</button> | |
| </div> | |
| <div class="field"><label>Mass gain <var id="m_l">1.0</var></label> | |
| <input id="m" type="range" min="0.2" max="2.2" step="0.05" value="1.0"></div> | |
| <div class="field"><label>Lensing <var id="g_l">1.0</var></label> | |
| <input id="g" type="range" min="0" max="3" step="0.05" value="1.0"></div> | |
| <div class="field"><label>Music volume <var id="v_l">0.5</var></label> | |
| <input id="v" type="range" min="0" max="1" step="0.05" value="0.5"></div> | |
| <div class="btns"> | |
| <button id="bSave">Save</button> | |
| <button id="bLoad">Load</button> | |
| <button id="bCode">Save code</button> | |
| </div> | |
| <div class="btns"> | |
| <button id="bMus" class="on">Music: on</button> | |
| <button id="bSrc">Test pattern</button> | |
| </div> | |
| <div class="btns"> | |
| <button id="bSeed">New brain</button> | |
| <button id="bIdle" class="on">Idle spin: on</button> | |
| </div> | |
| <div id="stat"></div> | |
| </div> | |
| <div id="help" class="panel"> | |
| <b>W A S D</b> move · <b>Q E</b> down/up<br> | |
| drag to look (inverted) · <b>shift</b> boost<br> | |
| gold pulse = the key · red membrane = the seal<br> | |
| the amber thread lives in the gates | |
| </div> | |
| <video id="vid" style="display:none" playsinline muted></video> | |
| <script> | |
| ; | |
| /* Do not hype. Do not lie. Just show. */ | |
| const $=id=>document.getElementById(id); | |
| const cvs=$('gl'), boot=$('boot'); | |
| /* any failure paints itself on the boot screen instead of dying silently */ | |
| function bootErr(t){ | |
| if(window.dreamTick){clearInterval(window.dreamTick);window.dreamTick=null;} | |
| boot.classList.add('err');boot.classList.remove('gone'); | |
| boot.innerHTML='<div class="big">It broke — here is why</div><div class="sub">' | |
| +String(t).replace(/[<>&]/g,c=>({'<':'<','>':'>','&':'&'}[c])).slice(0,600)+'</div>'; | |
| } | |
| window.onerror=(m,s,l,c,e)=>{bootErr((e&&e.stack)||m+' @ line '+l);}; | |
| window.addEventListener('unhandledrejection',e=>bootErr(e.reason)); | |
| let gl=cvs.getContext('webgl2',{antialias:false,powerPreference:'high-performance'}); | |
| if(!gl)gl=cvs.getContext('webgl2'); | |
| if(!gl){bootErr('No WebGL2 — this brain needs a WebGL2 browser.');throw new Error('no webgl2');} | |
| const MAXW=15; /* up to 5 moons x 3 live rooms */ | |
| const VERT=`#version 300 es | |
| void main(){vec2 p=vec2((gl_VertexID<<1)&2,gl_VertexID&2);gl_Position=vec4(p*2.0-1.0,0.0,1.0);}`; | |
| const FRAG=`#version 300 es | |
| precision highp float; | |
| out vec4 O; | |
| uniform vec2 iRes; | |
| uniform float iTime; | |
| uniform vec3 camP,camR,camU,camF; | |
| uniform int nWell,mode,keyWell; | |
| /* loop bounds as UNIFORMS: the D3D/ANGLE compiler cannot know their value, | |
| so it cannot unroll the raymarch 180-wide (x26 for the glass chord) — | |
| it must compile each loop body ONCE. This is the first-launch fix. */ | |
| uniform int marchN,glassN,escN; | |
| uniform vec4 wells[${MAXW}]; /* xyz pos · w size */ | |
| uniform vec3 wellCol[${MAXW}]; | |
| uniform vec4 wellPar[${MAXW}]; /* x spin-xz · y spin-xy · z room index · w free */ | |
| uniform vec3 roomPos[3]; /* prev · current · next */ | |
| uniform vec4 roomPar[3]; /* x size · y p-xz · z p-vert · w squash */ | |
| uniform vec4 roomMod[3]; /* x twist · y breath amp · z breath freq · w mass */ | |
| uniform vec4 roomShp[3]; /* x ripple amp · y lobes · z ring blend · w free */ | |
| uniform vec3 roomTintU[3]; | |
| uniform vec4 seal; /* xyz pos · w radius (<0 = open) */ | |
| uniform float mGain,lensK; | |
| uniform sampler2D camTex; | |
| /* ---------- primitives ---------- */ | |
| vec2 rot2(vec2 v,float a){float c=cos(a),s=sin(a);return vec2(c*v.x-s*v.y,s*v.x+c*v.y);} | |
| float sdSeg(vec3 p,vec3 a,vec3 b){vec3 pa=p-a,ba=b-a; | |
| float h=clamp(dot(pa,ba)/dot(ba,ba),0.0,1.0);return length(pa-ba*h);} | |
| /* ---------- THE ONE SHAPE — the Aeon Forge move applied to geometry. | |
| Shape is a PARAMETER VECTOR drawn from the hash, not a branch: | |
| px cross-section exponent (2 = circle -> 8 = square) | |
| pv profile exponent (2 = ball -> 8 = prism) | |
| hy squash ratio (height / size) | |
| aa · an angular ripple amp · lobes (gears, stars) | |
| tw torus blend (0 = solid -> 1 = ring) | |
| One branchless formula: compiles in a blink, and rooms MORPH | |
| continuously with depth instead of unlocking discrete shapes. | |
| Conservative distance: p-norms with p>=2 are 1-Lipschitz, mix of | |
| 1-Lipschitz fields is 1-Lipschitz, ripple guarded by its own slope. */ | |
| float shapeSD(vec3 q,float s,float px,float pv,float hy, | |
| float aa,float an,float tw){ | |
| float cx=pow(pow(abs(q.x)+1e-5,px)+pow(abs(q.z)+1e-5,px),1.0/px); | |
| float lp=pow(pow(cx,pv)+pow(abs(q.y/hy)+1e-5,pv),1.0/pv); | |
| float d=(lp-s)*min(hy,1.0); | |
| if(tw>0.001){ | |
| float dR=length(vec2(length(q.xz)-s*0.72,q.y))-s*0.42; | |
| d=mix(d,dR,tw); | |
| } | |
| if(aa>0.001){ | |
| float rr=length(q.xz); | |
| float w=clamp(rr/(0.35*s),0.0,1.0); /* fade lobes near the axis: | |
| kills the 1/r slope blow-up */ | |
| d-=s*aa*w*cos(an*atan(q.z,q.x+1e-6)); | |
| } | |
| return d/(1.0+aa*(an+1.0)*2.9); /* true slope bound of the faded ripple */ | |
| } | |
| float roomSD(vec3 p,int r){ | |
| vec3 q=p-roomPos[r]; | |
| float tw=roomMod[r].x; | |
| if(abs(tw)>0.0005) q.xz=rot2(q.xz,q.y*tw); | |
| float d=shapeSD(q,roomPar[r].x,roomPar[r].y,roomPar[r].z,roomPar[r].w, | |
| roomShp[r].x,roomShp[r].y,roomShp[r].z); | |
| float ra=roomMod[r].y; | |
| if(ra>0.001) d+=ra*sin(q.x*roomMod[r].z)*sin(q.y*roomMod[r].z)*sin(q.z*roomMod[r].z); | |
| return d/(1.0+abs(tw)*roomPar[r].x*0.6+ra*roomMod[r].z); /* Lipschitz guard */ | |
| } | |
| float worldSD(vec3 p){ | |
| float d=min(roomSD(p,0),min(roomSD(p,1),roomSD(p,2))); | |
| d=min(d,sdSeg(p,roomPos[0],roomPos[1])-1.7); | |
| d=min(d,sdSeg(p,roomPos[1],roomPos[2])-1.7); | |
| return d;} | |
| int nearestRoom(vec3 p){ | |
| float d0=roomSD(p,0),d1=roomSD(p,1),d2=roomSD(p,2); | |
| return d0<d1?(d0<d2?0:2):(d1<d2?1:2);} | |
| vec3 wallNormal(vec3 p){ /* inward-facing */ | |
| const vec2 e=vec2(0.02,-0.02); | |
| return -normalize( | |
| e.xyy*worldSD(p+e.xyy)+e.yyx*worldSD(p+e.yyx)+ | |
| e.yxy*worldSD(p+e.yxy)+e.xxx*worldSD(p+e.xxx));} | |
| /* ---------- moons wear their room's shape — the SAME kernel, | |
| shrunk and rounded into optical glass. no tables, no branches. ---------- */ | |
| float moonSD(vec3 p,int k){ | |
| vec3 q=p-wells[k].xyz; | |
| float s=wells[k].w; | |
| float bs=length(q)-s*2.2; | |
| if(bs>0.6) return bs; | |
| q.xz=rot2(q.xz,wellPar[k].x); | |
| q.xy=rot2(q.xy,wellPar[k].y); | |
| int ri=int(wellPar[k].z+0.5); | |
| return shapeSD(q,s*0.62,roomPar[ri].y,roomPar[ri].z,roomPar[ri].w, | |
| roomShp[ri].x,roomShp[ri].y,roomShp[ri].z)-s*0.10; | |
| } | |
| vec3 moonNormal(vec3 p,int k){ | |
| const vec2 e=vec2(0.012,-0.012); | |
| return normalize( | |
| e.xyy*moonSD(p+e.xyy,k)+e.yyx*moonSD(p+e.yyx,k)+ | |
| e.yxy*moonSD(p+e.yxy,k)+e.xxx*moonSD(p+e.xxx,k));} | |
| /* ---------- boundary images ---------- */ | |
| vec3 eqSample(vec3 n){ | |
| float u=0.5+atan(n.z,n.x)*0.159155; | |
| float v=0.5-asin(clamp(n.y,-1.0,1.0))*0.318310; | |
| return texture(camTex,vec2(1.0-u,v)).rgb;} | |
| vec3 wallImg(vec3 p){ | |
| int r=nearestRoom(p); | |
| return eqSample(normalize(p-roomPos[r]))*roomTintU[r];} | |
| void main(){ | |
| vec2 uv0=(gl_FragCoord.xy-0.5*iRes)/iRes.y; | |
| vec3 x=camP; | |
| vec3 rd=normalize(uv0.x*camR+uv0.y*camU+1.5*camF); | |
| vec3 glow=vec3(0.0), tint=vec3(1.0), accum=vec3(0.0), col=vec3(0.0); | |
| float trav=0.0, stp=0.05; | |
| int bnc=0; bool done=false; | |
| const int MAXB=12; | |
| /* the amber thread lives only in the corridors, not in the rooms */ | |
| vec3 gA0=mix(roomPos[0],roomPos[1],0.28), gB0=mix(roomPos[0],roomPos[1],0.72); | |
| vec3 gA1=mix(roomPos[1],roomPos[2],0.28), gB1=mix(roomPos[1],roomPos[2],0.72); | |
| for(int i=0;i<marchN;i++){ | |
| /* lensing: deflection per path length, dtheta ~ gradPhi * ds */ | |
| if(lensK>0.001){ | |
| vec3 pull=vec3(0.0); | |
| for(int k=0;k<nWell;k++){ | |
| vec3 dv=wells[k].xyz-x; float r2=dot(dv,dv)+0.4; | |
| pull+=dv*inversesqrt(r2)*(wells[k].w*mGain/r2); } | |
| rd=normalize(rd+pull*stp*0.055*lensK); | |
| } | |
| float dm=1e9; int hb=-1; | |
| for(int k=0;k<nWell;k++){ | |
| float sd=moonSD(x,k); | |
| float r=length(x-wells[k].xyz); | |
| float e=wells[k].w*mGain/(r+1.0); | |
| glow+=wellCol[k]*e*e*0.03*stp; | |
| if(k==keyWell) /* the key crystal breathes gold */ | |
| glow+=vec3(1.0,0.80,0.30)*e*e*(0.05+0.045*sin(iTime*4.0))*stp; | |
| if(sd<dm){dm=sd;hb=k;} } | |
| float dw=-worldSD(x); | |
| { | |
| float dg=min(sdSeg(x,gA0,gB0),sdSeg(x,gA1,gB1)); | |
| glow+=vec3(0.91,0.69,0.23)*(0.010*stp/(dg*dg+0.15))*smoothstep(4.0,1.0,dg); | |
| } | |
| float dsl=1e9; | |
| if(seal.w>0.0){ | |
| dsl=length(x-seal.xyz)-seal.w; | |
| glow+=vec3(0.95,0.22,0.18)*(0.012*stp/(dsl*dsl+0.22))*smoothstep(6.0,1.5,dsl); | |
| if(dsl<0.04){ /* the red membrane: sealed exit */ | |
| float pl=0.5+0.5*sin(iTime*3.0+x.y*2.2+x.x*1.3); | |
| col=(vec3(0.72,0.10,0.09)+vec3(0.35,0.06,0.05)*pl)*tint; | |
| done=true; break; | |
| } | |
| } | |
| if(dm<0.03 && hb>=0){ | |
| vec3 n=moonNormal(x,hb); | |
| /* THE OPTICAL HACK: decouple shape from optics. | |
| Flat faces have a constant n — eqSample(n) smears ONE webcam pixel | |
| across the whole face, and refract(rd,n) acts like a window pane. | |
| 'radial' is the crystal-ball normal the shape WOULD have if it were | |
| a sphere; texture wraps with it, and light bends through a blend of | |
| the two, so a sharp cube refracts like a lens. The SDF march below | |
| is untouched: silhouettes stay crisp, the ray still flies on. */ | |
| vec3 radial=normalize(x-wells[hb].xyz); | |
| vec3 opticN=normalize(mix(n,radial,0.75)); | |
| if(dot(-rd,opticN)<=0.0) opticN=n; /* silhouette guard: never face away from the ray */ | |
| float F=0.04+0.96*pow(1.0-max(dot(-rd,opticN),0.0),5.0); /* Schlick, glass F0 */ | |
| if(mode==1){ /* PROJECTION: moons carry the image (opaque) */ | |
| float L=0.0; for(int k=0;k<nWell;k++){ | |
| L+=wells[k].w*mGain/(length(x-wells[k].xyz)+1.0);} | |
| col=tint*eqSample(radial)*(0.55+0.7*min(L,1.4)); done=true; break; | |
| } | |
| if(mode==2){ | |
| /* CASCADE: the inspiration.html magic, verbatim in spirit. | |
| The ray NEVER enters the crystal. It deposits a translucent | |
| layer of the boundary image (hologram accumulation), then | |
| bounces off the sphere-blended normal and keeps flying — | |
| moon to wall to moon. Recursion is capped at 4 like the | |
| original: it LOOKS endless because the lensing fisheye | |
| compounds every bounce. Cheap, so it can never TDR. */ | |
| accum+=tint*eqSample(radial)*0.5; | |
| rd=reflect(rd,opticN); | |
| tint*=0.62; bnc++; | |
| if(bnc>4){col=accum;done=true;break;} | |
| for(int j=0;j<escN;j++){ if(moonSD(x,hb)>0.05)break; x+=n*0.05; } | |
| x+=rd*0.04; trav+=0.08; stp=0.05; continue; | |
| } | |
| if(mode==0){ | |
| /* BOUNDARY: the rajapinta_box move — moons are curved mirrors. | |
| gravity already bent the ray on the way in; the surface just | |
| reflects the boundary. your face drags where the moon passes. */ | |
| rd=reflect(rd,opticN); | |
| tint*=0.82; bnc++; | |
| if(bnc>4){col=vec3(0.0);done=true;break;} | |
| for(int j=0;j<escN;j++){ if(moonSD(x,hb)>0.05)break; x+=n*0.05; } | |
| x+=rd*0.04; trav+=0.08; stp=0.05; continue; | |
| } | |
| /* GLASS (mode 3): heavy optical crystal — the one bulk-transport | |
| mode, kept as the honest-physics exhibit. Snell in, marched | |
| chord, Beer-Lambert tint, Snell out. Affordable ONLY because | |
| Glass rays terminate at the first (image) wall; in the | |
| mirror-wall modes this march is what caused the TDR. */ | |
| glow+=tint*eqSample(reflect(rd,opticN))*F*0.55; /* mirror glint, Fresnel-weighted */ | |
| bnc++; if(bnc>MAXB){col=vec3(0.0);done=true;break;} | |
| const float IOR=1.45; | |
| vec3 d=refract(rd,opticN,1.0/IOR); | |
| if(dot(d,d)<1e-4) d=reflect(rd,opticN); /* entry-grazing guard */ | |
| x-=n*0.04; /* step inside the glass */ | |
| float chord=0.0; | |
| for(int j=0;j<glassN;j++){ | |
| float sd=moonSD(x,hb); | |
| if(sd>-0.006){ /* reached the far surface */ | |
| vec3 ne=moonNormal(x,hb); | |
| /* same optical hack on the way out: the far side of a flat pane | |
| must also curve like the back of a lens, or the compression | |
| cancels and the world-in-a-world never forms */ | |
| vec3 radE=normalize(x-wells[hb].xyz); | |
| vec3 opticNE=normalize(mix(ne,radE,0.75)); | |
| if(dot(d,opticNE)<=0.0) opticNE=ne; /* exit guard: normal must face along the ray */ | |
| vec3 ro=refract(d,-opticNE,IOR); | |
| if(dot(ro,ro)<1e-4){ d=reflect(d,-opticNE); x+=d*0.03; chord+=0.03; continue; } /* TIR: bounce inside */ | |
| d=ro; break; /* refract out */ | |
| } | |
| float st=max(-sd*0.8,0.025); | |
| x+=d*st; chord+=st; | |
| } | |
| /* Beer-Lambert: the crystal's colour tints what passes through, by chord length */ | |
| tint*=exp(-(vec3(1.0)-wellCol[hb])*chord*0.35)*0.96; | |
| rd=d; | |
| vec3 ne2=moonNormal(x,hb); | |
| for(int j=0;j<escN;j++){ if(moonSD(x,hb)>0.05)break; x+=ne2*0.05; } | |
| x+=rd*0.04; trav+=chord+0.08; stp=0.05; continue; | |
| } | |
| if(dw<0.03){ | |
| if(mode==0||mode==3){ /* BOUNDARY & GLASS: walls are the image */ | |
| vec3 wc=wallImg(x); | |
| float L=0.0; for(int k=0;k<nWell;k++){ | |
| L+=wells[k].w*mGain/(length(x-wells[k].xyz)+1.0);} | |
| float fog=exp(-trav*0.010); | |
| col=wc*tint*(0.30+1.1*min(L,1.6))*fog; done=true; break; | |
| } else { /* PROJECTION / CASCADE: walls mirror */ | |
| vec3 n=wallNormal(x); | |
| rd=reflect(rd,n); tint*=0.85; bnc++; | |
| if(bnc>4){ col=(mode==2)?accum:vec3(0.0); done=true; break; } /* inspiration's maxBnc=4 */ | |
| for(int j=0;j<escN;j++){ if(-worldSD(x)>0.05)break; x+=n*0.05; } | |
| x+=rd*0.04; stp=0.05; continue; | |
| } | |
| } | |
| stp=clamp(min(min(dm,dw),dsl)*0.9,0.03,3.0); | |
| x+=rd*stp; trav+=stp; | |
| if(trav>260.0)break; | |
| } | |
| if(!done) col=(mode==2)?accum:vec3(0.0); | |
| col+=glow; | |
| col=col/(col+1.0); | |
| col=pow(col,vec3(0.4545)); | |
| float vig=1.0-0.35*dot(uv0,uv0); | |
| O=vec4(col*vig,1.0); | |
| }`; | |
| /* ---------------- boundary texture (webcam + fallback) ---------------- */ | |
| const vid=$('vid'); | |
| let camOK=false, useCam=true; | |
| const camTex=gl.createTexture(); | |
| gl.activeTexture(gl.TEXTURE0); | |
| gl.bindTexture(gl.TEXTURE_2D,camTex); | |
| gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_MIN_FILTER,gl.LINEAR); | |
| gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_MAG_FILTER,gl.LINEAR); | |
| gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_WRAP_S,gl.REPEAT); | |
| gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_WRAP_T,gl.CLAMP_TO_EDGE); | |
| if(navigator.mediaDevices&&navigator.mediaDevices.getUserMedia){ | |
| navigator.mediaDevices.getUserMedia({video:{width:640,height:480,facingMode:'user'}}) | |
| .then(st=>{vid.srcObject=st;vid.play();camOK=true;setSrcLabel();}) | |
| .catch(()=>{useCam=false;camOK=false;setSrcLabel();}); | |
| }else{useCam=false;setSrcLabel();} | |
| const pat=document.createElement('canvas');pat.width=256;pat.height=256; | |
| const pctx=pat.getContext('2d'); | |
| function drawPattern(t){ | |
| const g=pctx.createLinearGradient(0,0,0,256); | |
| g.addColorStop(0,'#2a3550');g.addColorStop(1,'#6a4a70'); | |
| pctx.fillStyle=g;pctx.fillRect(0,0,256,256); | |
| const cx=128+64*Math.cos(0.6*t),cy=128+48*Math.sin(0.9*t); | |
| for(let s=0;s<24;s++){ | |
| pctx.fillStyle=s%2?'#d8c890':'#705838'; | |
| pctx.beginPath();pctx.arc(cx,cy,46-s*1.9,0,7);pctx.fill(); | |
| } | |
| pctx.fillStyle='#101018';pctx.fillRect(176,56,40,40); | |
| pctx.fillStyle='#c9d4e0';pctx.font='14px monospace'; | |
| pctx.fillText('no camera — pattern',56,240); | |
| } | |
| function setSrcLabel(){ | |
| $('src').textContent=useCam&&camOK?'webcam live':'test pattern'; | |
| $('bSrc').textContent=useCam?'Test pattern':'Use camera'; | |
| } | |
| function uploadBoundary(t){ | |
| gl.activeTexture(gl.TEXTURE0); | |
| gl.bindTexture(gl.TEXTURE_2D,camTex); | |
| gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL,true); | |
| if(useCam&&camOK&&vid.readyState>=2){ | |
| gl.texImage2D(gl.TEXTURE_2D,0,gl.RGBA,gl.RGBA,gl.UNSIGNED_BYTE,vid); | |
| }else{ | |
| drawPattern(t); | |
| gl.texImage2D(gl.TEXTURE_2D,0,gl.RGBA,gl.RGBA,gl.UNSIGNED_BYTE,pat); | |
| } | |
| gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL,false); | |
| } | |
| /* ===================================================================== | |
| THE PROCEDURAL BRAIN — now with LEVELS | |
| Every chamber is a pure function of (SEED, index). Nothing is stored. | |
| Level L is a run of depth(L) rooms. One room hides the KEY CRYSTAL | |
| (moon 0, gold, pulsing). The far gate of the level is SEALED until | |
| the key is touched. Room mass, twist, ripple and the shape pool all | |
| deepen with the level. Save = 6 numbers. Everything else is hash. | |
| ===================================================================== */ | |
| let SEED=(Math.random()*4294967296)>>>0; | |
| function rng(i,salt){ | |
| let t=(SEED ^ Math.imul(i|0,2654435761) ^ Math.imul(salt|0,40503))>>>0; | |
| t=Math.imul(t^(t>>>15),t|1); | |
| t^=t+Math.imul(t^(t>>>7),t|61); | |
| return ((t^(t>>>14))>>>0)/4294967296; | |
| } | |
| /* shape is continuous now — names are nearest archetypes, for the HUD */ | |
| function shapeName(d){ | |
| if(d.tw>0.30)return 'RING'; | |
| if(d.aa>0.05)return (d.an>=8?'GEAR-':'STAR-')+d.an; | |
| const bx=d.px>4.5, pr=d.pv>4.5; | |
| if(bx&&pr)return 'CUBE'; | |
| if(!bx&&pr)return 'CYLINDER'; | |
| if(bx&&!pr)return 'CUSHION'; | |
| if(d.hy<0.62)return 'DOME'; | |
| return 'ORB'; | |
| } | |
| const SYL=['KA','RA','JA','PIN','TA','VUO','LU','MI','SA','TÄH','TI','KI','VI','HEL','ME','NU','SO','LO']; | |
| /* ------- level geometry: depth(L) rooms per level, cumulative starts ------- */ | |
| function depthOf(L){return Math.min(3+L,10);} /* L1: 4 rooms … caps at 10 */ | |
| function levelStartOf(L){let s=0;for(let k=1;k<L;k++)s+=depthOf(k);return s;} | |
| function levelOfRoom(i){ | |
| if(i<0)return 1; /* the ante-brain: level 1 rules */ | |
| let L=1,s=0; | |
| while(s+depthOf(L)<=i){s+=depthOf(L);L++;} | |
| return L; | |
| } | |
| function rngL(L,salt){return rng(0x5EED+Math.imul(L,131),7000+salt);} | |
| function keyRoomOf(L){const d=depthOf(L); | |
| return levelStartOf(L)+1+Math.floor(rngL(L,1)*(d-1));} | |
| function exitRoomOf(L){return levelStartOf(L)+depthOf(L)-1;} | |
| function nMoonsOf(L){return Math.min(3+((L-1)>>1),5);} | |
| function roomDef(i){ | |
| const L=levelOfRoom(i); | |
| /* the Aeon Forge move: depth widens the PARAMETER RANGES of one kernel — | |
| rooms morph continuously with the descent instead of unlocking shapes */ | |
| const spread=Math.min(1.2+L*0.8,6.0); | |
| const px=2.0+rng(i,1)*spread; /* 2 circle -> 8 square */ | |
| const pv=2.0+rng(i,4)*spread; /* 2 ball -> 8 prism */ | |
| const size=7.0+rng(i,2)*4.0+Math.min(L*0.15,3.0); | |
| const height=4.5+rng(i,3)*2.5; | |
| const hy=Math.min(Math.max(height/size,0.45),1.0); | |
| const an=4+Math.floor(rng(i,13)*9); /* 4..12 lobes */ | |
| let aa=L>=2?rng(i,12)*rng(i,12)*Math.min(0.02+L*0.013,0.12):0.0; /* lobes amp */ | |
| aa=Math.min(aa,0.5/an); /* slope budget: aa*an <= 0.5 */ | |
| let tw=L>=4?Math.pow(rng(i,14),1.5)*Math.min((L-3)*0.10,0.55):0.0; /* ring blend */ | |
| const hue=rng(i,5); | |
| const turn=(rng(i,6)-0.5)*2.4; /* gate heading vs. entry */ | |
| let twist=(rng(i,8)-0.5)*2.0*Math.min(0.015+L*0.011,0.13); | |
| let ripA=Math.min(Math.max(L-2,0)*0.06,0.45)*rng(i,9); | |
| const ripF=0.5+rng(i,10)*1.2; | |
| /* GUARANTEE the room centre is deep interior (transition fires at -1.0). | |
| Pure function of the drawn values, so still deterministic per (SEED,i). | |
| Lobes are zero at the axis (faded), so they don't enter the formula. */ | |
| const cDepth=()=>((-(size*hy)*(1-tw)+size*0.30*tw) | |
| /(1.0+aa*(an+1.0)*2.9) | |
| /(1.0+Math.abs(twist)*size*0.6+ripA*ripF)); | |
| for(let g=0;g<10 && cDepth()>-1.3;g++){ | |
| tw*=0.7; aa*=0.7; | |
| if(g>2){twist*=0.75; ripA*=0.75;} | |
| } | |
| const massK=Math.min(0.7+rng(i,11)*0.9+L*0.04,2.8); | |
| let name='';const nc=2+Math.floor(rng(i,7)*2); | |
| for(let k=0;k<nc;k++)name+=SYL[Math.floor(rng(i,20+k)*SYL.length)]; | |
| const tint=[0.86+0.14*Math.cos(6.283*hue), | |
| 0.86+0.14*Math.cos(6.283*(hue+0.33)), | |
| 0.86+0.14*Math.cos(6.283*(hue+0.66))]; | |
| return {px,pv,hy,aa,an,tw,size,height,hue,turn,name,tint,twist,ripA,ripF,massK,level:L}; | |
| } | |
| function moonDef(i,m){ /* moon m of room i — re-dreamed identically every visit */ | |
| const s=100+m*13; | |
| const mass=-Math.log(1e-6+rng(i,s+1)); | |
| return {mass, | |
| fx:0.05+rng(i,s+2)*0.20, fy:0.05+rng(i,s+3)*0.20, fz:0.05+rng(i,s+4)*0.20, | |
| px:rng(i,s+5)*6.283, py:rng(i,s+6)*6.283, pz:rng(i,s+7)*6.283, | |
| ax:0.28+rng(i,s+8)*0.22, ay:0.25+rng(i,s+9)*0.22, az:0.28+rng(i,s+10)*0.22, | |
| ra:(rng(i,s+11)-0.5)*1.2, rb:(rng(i,s+12)-0.5)*1.2, | |
| pa:rng(i,s+13)*6.283, pb:rng(i,s+14)*6.283, | |
| hue:rng(i,s+15)}; | |
| } | |
| /* ------- game state ------- */ | |
| let LEVEL=1, keyFound=false, maxReached=1; | |
| let CI=0, dirIn=[Math.sin(rng(0,99)*6.283),Math.cos(rng(0,99)*6.283)]; | |
| let R={}; /* the three live rooms: def, center, index */ | |
| function rotXZ(v,a){const c=Math.cos(a),s=Math.sin(a);return [c*v[0]-s*v[1],s*v[0]+c*v[1]];} | |
| function computeDirIn(N){ /* dirIn is a pure function of SEED and N */ | |
| let d=[Math.sin(rng(0,99)*6.283),Math.cos(rng(0,99)*6.283)]; | |
| if(N>0){for(let j=0;j<N;j++)d=rotXZ(d,roomDef(j).turn);} | |
| else if(N<0){for(let j=-1;j>=N;j--)d=rotXZ(d,-roomDef(j).turn);} | |
| return d; | |
| } | |
| function rebuildChain(){ | |
| const dPrev=roomDef(CI-1), dCur=roomDef(CI), dNext=roomDef(CI+1); | |
| const dirOut=rotXZ(dirIn,dCur.turn); | |
| const Dn=dCur.size+dNext.size+12.0, Dp=dPrev.size+dCur.size+12.0; | |
| R={ | |
| idx:[CI-1,CI,CI+1], | |
| def:[dPrev,dCur,dNext], | |
| pos:[[-dirIn[0]*Dp,0,-dirIn[1]*Dp],[0,0,0],[dirOut[0]*Dn,0,dirOut[1]*Dn]], | |
| dirOut | |
| }; | |
| $('roomV').textContent=dCur.name; | |
| $('roomI').textContent='#'+CI+' · '+shapeName(dCur) | |
| +' · p'+dCur.px.toFixed(1)+'/'+dCur.pv.toFixed(1) | |
| +(dCur.tw>0.01?' · ring '+dCur.tw.toFixed(2):''); | |
| $('massV').textContent=dCur.massK.toFixed(2)+' · '+(dCur.twist>=0?'+':'')+dCur.twist.toFixed(3); | |
| updateLevelHud(); | |
| } | |
| function banner(a,b){ | |
| $('bn1').textContent=a;$('bn2').textContent=b||''; | |
| const el=$('banner');el.classList.add('show'); | |
| clearTimeout(banner.t);banner.t=setTimeout(()=>el.classList.remove('show'),2600); | |
| } | |
| function updateLevelHud(){ | |
| $('lvlV').textContent=LEVEL; | |
| const rIn=CI-levelStartOf(LEVEL); | |
| $('progV').textContent=(CI<0?'—':(rIn+1)+' / '+depthOf(LEVEL)); | |
| $('keyV').textContent=keyFound?'found — seal open':'not found'; | |
| $('keyV').className=keyFound?'keyok':'keyno'; | |
| } | |
| function onRoomChanged(){ | |
| const newL=levelOfRoom(CI); | |
| if(newL>LEVEL){ | |
| LEVEL=newL; | |
| keyFound=(LEVEL+1<=maxReached); /* re-visiting a beaten level? seal stays open */ | |
| if(LEVEL>maxReached){maxReached=LEVEL;keyFound=false; | |
| banner('LEVEL '+LEVEL,depthOf(LEVEL)+' rooms · find the gold crystal'); | |
| autoSave();} | |
| }else if(newL<LEVEL){ | |
| LEVEL=newL; | |
| keyFound=(LEVEL+1<=maxReached); /* walked back: that seal was already opened */ | |
| } | |
| updateLevelHud(); | |
| } | |
| function advance(){ /* stepped through the forward gate */ | |
| const shift=R.pos[2]; | |
| camP=[camP[0]-shift[0],camP[1]-shift[1],camP[2]-shift[2]]; | |
| dirIn=R.dirOut; CI++; rebuildChain(); onRoomChanged(); | |
| } | |
| function retreat(){ /* stepped back — re-dream the past */ | |
| const shift=R.pos[0]; | |
| camP=[camP[0]-shift[0],camP[1]-shift[1],camP[2]-shift[2]]; | |
| const dPrev=roomDef(CI-1); | |
| dirIn=rotXZ(dirIn,-dPrev.turn); CI--; rebuildChain(); onRoomChanged(); | |
| } | |
| /* ---------------- JS twins of the SDFs (collision + transitions) ---------- */ | |
| /* corridor segment distance — the twin of GLSL sdSeg */ | |
| function sdSegJ(px,py,pz,a,b){ | |
| const bax=b[0]-a[0],bay=b[1]-a[1],baz=b[2]-a[2]; | |
| const pax=px-a[0],pay=py-a[1],paz=pz-a[2]; | |
| const h=Math.min(Math.max((pax*bax+pay*bay+paz*baz)/(bax*bax+bay*bay+baz*baz),0),1); | |
| return Math.hypot(pax-bax*h,pay-bay*h,paz-baz*h);} | |
| /* the SAME kernel, mirrored exactly for collision and gate transitions */ | |
| function shapeSDJ(x,y,z,d){ | |
| const s=d.size; | |
| const cx=Math.pow(Math.pow(Math.abs(x)+1e-5,d.px)+Math.pow(Math.abs(z)+1e-5,d.px),1/d.px); | |
| const lp=Math.pow(Math.pow(cx,d.pv)+Math.pow(Math.abs(y/d.hy)+1e-5,d.pv),1/d.pv); | |
| let sd=(lp-s)*Math.min(d.hy,1.0); | |
| if(d.tw>0.001){ | |
| const dR=Math.hypot(Math.hypot(x,z)-s*0.72,y)-s*0.42; | |
| sd=sd+(dR-sd)*d.tw; | |
| } | |
| if(d.aa>0.001){ | |
| const rr=Math.hypot(x,z); | |
| const w=Math.min(Math.max(rr/(0.35*s),0),1); | |
| sd-=s*d.aa*w*Math.cos(d.an*Math.atan2(z,x+1e-6)); | |
| } | |
| return sd/(1.0+d.aa*(d.an+1.0)*2.9); | |
| } | |
| function roomSDJ(px,py,pz,r){ | |
| const d=R.def[r],c=R.pos[r]; | |
| let x=px-c[0],y=py-c[1],z=pz-c[2]; | |
| if(Math.abs(d.twist)>0.0005){ | |
| const ca=Math.cos(y*d.twist),sa=Math.sin(y*d.twist); | |
| const nx=ca*x-sa*z, nz=sa*x+ca*z; x=nx; z=nz; | |
| } | |
| let sd=shapeSDJ(x,y,z,d); | |
| if(d.ripA>0.001) | |
| sd+=d.ripA*Math.sin(x*d.ripF)*Math.sin(y*d.ripF)*Math.sin(z*d.ripF); | |
| return sd/(1.0+Math.abs(d.twist)*d.size*0.6+d.ripA*d.ripF); | |
| } | |
| function worldSDJ(px,py,pz){ | |
| let d=Math.min(roomSDJ(px,py,pz,0),Math.min(roomSDJ(px,py,pz,1),roomSDJ(px,py,pz,2))); | |
| d=Math.min(d,sdSegJ(px,py,pz,R.pos[0],R.pos[1])-1.7); | |
| d=Math.min(d,sdSegJ(px,py,pz,R.pos[1],R.pos[2])-1.7); | |
| return d;} | |
| /* ---------------- moons + key + seal upload ---------------- */ | |
| let P={mGain:1.0,lensK:1.0},mode=0,idle=1; | |
| const wellsF=new Float32Array(MAXW*4), colsF=new Float32Array(MAXW*3), | |
| parF=new Float32Array(MAXW*4), | |
| roomPosF=new Float32Array(9), roomParF=new Float32Array(12), | |
| roomModF=new Float32Array(12), roomShpF=new Float32Array(12), | |
| roomTintF=new Float32Array(9); | |
| function moonCol(h){ | |
| return[0.55+0.45*Math.cos(6.283*h),0.55+0.45*Math.cos(6.283*(h+0.33)),0.55+0.45*Math.cos(6.283*(h+0.66))];} | |
| let keyWell=-1, keyPos=null, keyR=0; | |
| let sealV=[0,0,0,-1]; | |
| function packWorld(t){ | |
| let w=0; keyWell=-1; keyPos=null; | |
| const nM=nMoonsOf(LEVEL), kRoom=keyRoomOf(LEVEL); | |
| for(let r=0;r<3;r++){ | |
| const d=R.def[r],c=R.pos[r]; | |
| roomPosF[r*3]=c[0];roomPosF[r*3+1]=c[1];roomPosF[r*3+2]=c[2]; | |
| roomParF[r*4]=d.size;roomParF[r*4+1]=d.px;roomParF[r*4+2]=d.pv;roomParF[r*4+3]=d.hy; | |
| roomModF[r*4]=d.twist;roomModF[r*4+1]=d.ripA;roomModF[r*4+2]=d.ripF;roomModF[r*4+3]=d.massK; | |
| roomShpF[r*4]=d.aa;roomShpF[r*4+1]=d.an;roomShpF[r*4+2]=d.tw;roomShpF[r*4+3]=0; | |
| roomTintF[r*3]=d.tint[0];roomTintF[r*3+1]=d.tint[1];roomTintF[r*3+2]=d.tint[2]; | |
| const A=d.size*0.62, Ay=d.height*0.55; | |
| for(let m=0;m<nM && w<MAXW;m++,w++){ | |
| const md=moonDef(R.idx[r],m); | |
| const isKey=(!keyFound && R.idx[r]===kRoom && m===0); | |
| let size=(0.45+(md.mass/3.0)*0.75)*P.mGain*d.massK; | |
| size=Math.min(size,1.6); if(isKey)size=Math.min(size*1.25,1.8); | |
| /* the key swings wide, off the walking line — you must go to it */ | |
| const ax=isKey?Math.min(md.ax*1.6,0.55):md.ax, | |
| ay=isKey?Math.min(md.ay*1.5,0.50):md.ay, | |
| az=isKey?Math.min(md.az*1.6,0.55):md.az; | |
| wellsF[w*4+0]=c[0]+A*ax*Math.sin(md.fx*t+md.px); | |
| wellsF[w*4+1]=c[1]+Ay*ay*Math.sin(md.fy*t+md.py); | |
| wellsF[w*4+2]=c[2]+A*az*Math.sin(md.fz*t+md.pz); | |
| wellsF[w*4+3]=size; | |
| const cc=isKey?[1.0,0.84,0.36]:moonCol(md.hue); | |
| colsF[w*3]=cc[0];colsF[w*3+1]=cc[1];colsF[w*3+2]=cc[2]; | |
| parF[w*4]=md.ra*t+md.pa;parF[w*4+1]=md.rb*t+md.pb;parF[w*4+2]=r;parF[w*4+3]=0; | |
| if(isKey){keyWell=w;keyPos=[wellsF[w*4],wellsF[w*4+1],wellsF[w*4+2]];keyR=size;} | |
| } | |
| } | |
| /* the seal: a red membrane in the corridor after the level's last room */ | |
| const eRoom=exitRoomOf(LEVEL); | |
| if(!keyFound && CI===eRoom){ | |
| sealV=[(R.pos[1][0]+R.pos[2][0])*0.5,(R.pos[1][1]+R.pos[2][1])*0.5, | |
| (R.pos[1][2]+R.pos[2][2])*0.5,2.6]; | |
| }else sealV=[0,0,0,-1]; | |
| return w; | |
| } | |
| /* ---------------- camera & input (mouse INVERTED, as requested) ------------ */ | |
| let camP=[0,0,0], yaw=0, pitch=0; | |
| function faceForwardGate(){yaw=Math.atan2(R.dirOut[0],R.dirOut[1]);pitch=0;} | |
| rebuildChain();faceForwardGate(); | |
| const keys={}; | |
| addEventListener('keydown',e=>{keys[e.code]=1; | |
| if(['KeyW','KeyA','KeyS','KeyD','KeyQ','KeyE'].includes(e.code))e.preventDefault();}); | |
| addEventListener('keyup',e=>{keys[e.code]=0;}); | |
| let dragging=false,lx=0,ly=0; | |
| cvs.addEventListener('pointerdown',e=>{dragging=true;lx=e.clientX;ly=e.clientY; | |
| cvs.classList.add('drag');cvs.setPointerCapture(e.pointerId); | |
| boot.classList.add('gone');startMusic();}); | |
| cvs.addEventListener('pointerup',()=>{dragging=false;cvs.classList.remove('drag');}); | |
| cvs.addEventListener('pointermove',e=>{ | |
| if(!dragging)return; | |
| yaw+=(e.clientX-lx)*0.0032;pitch-=(e.clientY-ly)*0.0032; /* horiz inverted · vert natural */ | |
| pitch=Math.max(-1.5,Math.min(1.5,pitch)); | |
| lx=e.clientX;ly=e.clientY; | |
| }); | |
| boot.addEventListener('click',()=>{boot.classList.add('gone');startMusic();}); | |
| function insideVal(px,py,pz){ | |
| let v=-worldSDJ(px,py,pz); | |
| if(sealV[3]>0) | |
| v=Math.min(v,Math.hypot(px-sealV[0],py-sealV[1],pz-sealV[2])-sealV[3]); | |
| return v; | |
| } | |
| function tryMove(nx,ny,nz){ | |
| if(insideVal(nx,ny,nz)>0.35){camP=[nx,ny,nz];return;} | |
| if(insideVal(nx,camP[1],camP[2])>0.35)camP[0]=nx; | |
| if(insideVal(camP[0],ny,camP[2])>0.35)camP[1]=ny; | |
| if(insideVal(camP[0],camP[1],nz)>0.35)camP[2]=nz; | |
| } | |
| /* ---------------- music: game1..4.mp3 next to this file, endless ---------- */ | |
| const TRACKS=['game1.mp3','game2.mp3','game3.mp3','game4.mp3','game5.mp3', | |
| 'game6.mp3','game7.mp3','game8.mp3','game9.mp3','game10.mp3']; | |
| let musicOn=true, musicStarted=false, mIdx=Math.floor(Math.random()*TRACKS.length), mFails=0; | |
| const audio=(typeof Audio!=='undefined')?new Audio():null; | |
| if(audio){ | |
| audio.preload='auto'; audio.volume=0.5; | |
| audio.addEventListener('ended',()=>{mFails=0;playTrack(mIdx+1);}); | |
| audio.addEventListener('error',()=>{ | |
| if(++mFails<TRACKS.length)playTrack(mIdx+1); | |
| else $('musV').textContent='no mp3s found'; | |
| }); | |
| audio.addEventListener('playing',()=>{mFails=0;$('musV').textContent=TRACKS[mIdx];}); | |
| } | |
| function playTrack(i){ | |
| if(!audio||!musicOn)return; | |
| mIdx=((i%TRACKS.length)+TRACKS.length)%TRACKS.length; | |
| audio.src=TRACKS[mIdx]; | |
| audio.play().catch(()=>{/* autoplay policy: waits for a click */}); | |
| } | |
| function startMusic(){ | |
| if(musicStarted||!musicOn||!audio)return; | |
| musicStarted=true; playTrack(mIdx); | |
| } | |
| $('bMus').onclick=e=>{ | |
| musicOn=!musicOn; | |
| e.target.classList.toggle('on',musicOn); | |
| e.target.textContent='Music: '+(musicOn?'on':'off'); | |
| if(!audio)return; | |
| if(musicOn){musicStarted=false;startMusic();} | |
| else {audio.pause();$('musV').textContent='off';} | |
| }; | |
| $('musV').textContent='starts on first click'; | |
| /* ---------------- save / load ---------------- | |
| the whole game is 6 numbers. everything else is the hash. */ | |
| function saveObj(){return {v:1,s:SEED,l:LEVEL,c:CI,k:keyFound?1:0,m:maxReached};} | |
| function saveCode(){return btoa(JSON.stringify(saveObj()));} | |
| function applySave(o){ | |
| if(!o||o.v!==1||typeof o.s!=='number')throw new Error('bad save'); | |
| SEED=o.s>>>0; LEVEL=Math.max(1,o.l|0); CI=o.c|0; | |
| keyFound=!!o.k; maxReached=Math.max(LEVEL,o.m|0); | |
| dirIn=computeDirIn(CI); camP=[0,0,0]; | |
| rebuildChain(); faceForwardGate(); updateLevelHud(); | |
| } | |
| function autoSave(){ | |
| try{localStorage.setItem('brainDescentSave',saveCode());}catch(e){/* fine: file:// or sandbox */} | |
| } | |
| $('bSave').onclick=()=>{ | |
| autoSave(); | |
| $('stat').textContent='saved · level '+LEVEL+' room #'+CI+' (Save code gives a portable copy)'; | |
| }; | |
| $('bCode').onclick=()=>{ | |
| const c=saveCode(); | |
| $('stat').textContent='code: '+c; | |
| if(navigator.clipboard)navigator.clipboard.writeText(c).catch(()=>{}); | |
| }; | |
| $('bLoad').onclick=()=>{ | |
| let code=''; | |
| try{code=localStorage.getItem('brainDescentSave')||'';}catch(e){} | |
| const inp=prompt('Paste a save code (leave as-is to load the local save):',code); | |
| if(inp===null)return; | |
| try{applySave(JSON.parse(atob(inp.trim()))); | |
| $('stat').textContent='loaded · level '+LEVEL+' room #'+CI+' · seed '+SEED.toString(16); | |
| banner('LEVEL '+LEVEL,'re-dreamed from the hash'); | |
| }catch(e){$('stat').textContent='could not read that save code';} | |
| }; | |
| /* ---------------- controls ---------------- */ | |
| const MODES=[ | |
| {name:'Boundary', note:'the walls are your camera; the moons are curved mirrors that lens and reflect them — your face drags where a moon passes.'}, | |
| {name:'Projection', note:'the moons carry your face; every wall is a mirror. the image emanates from the bulk — the holographic swap.'}, | |
| {name:'Cascade', note:'every crystal wears your face and mirrors its neighbours — worlds inside worlds, moon to wall to moon, built entirely from you.'}, | |
| {name:'Glass', note:'heavy optics: the ray refracts through each crystal (Snell + Beer-Lambert) and keeps flying — the honest-physics exhibit.'}, | |
| ]; | |
| function setMode(mi){ | |
| mode=mi; | |
| ['m0','m1','m2','m3'].forEach((id,i)=>$(id).classList.toggle('on',i===mi)); | |
| $('modeV').textContent=MODES[mi].name; | |
| $('modeNote').textContent=MODES[mi].note; | |
| } | |
| setMode(0); | |
| function bind(id,lab,fn){const el=$(id);el.addEventListener('input',()=>{fn(parseFloat(el.value));$(lab).textContent=el.value;});} | |
| bind('m','m_l',v=>{P.mGain=v;}); | |
| bind('g','g_l',v=>{P.lensK=v;}); | |
| bind('v','v_l',v=>{if(audio)audio.volume=v;}); | |
| $('m0').onclick=()=>setMode(0); | |
| $('m1').onclick=()=>setMode(1); | |
| $('m2').onclick=()=>setMode(2); | |
| $('m3').onclick=()=>setMode(3); | |
| $('bSrc').onclick=()=>{useCam=!useCam;setSrcLabel();}; | |
| $('bSeed').onclick=()=>{ SEED=(Math.random()*4294967296)>>>0; | |
| LEVEL=1;maxReached=1;keyFound=false;CI=0; | |
| dirIn=[Math.sin(rng(0,99)*6.283),Math.cos(rng(0,99)*6.283)]; | |
| camP=[0,0,0]; rebuildChain(); faceForwardGate(); updateLevelHud(); | |
| banner('LEVEL 1','a new brain · seed '+SEED.toString(16)); | |
| $('stat').textContent='new brain · seed '+SEED.toString(16); }; | |
| $('bIdle').onclick=e=>{idle^=1;e.target.classList.toggle('on',!!idle); | |
| e.target.textContent='Idle spin: '+(idle?'on':'off');}; | |
| if(matchMedia('(prefers-reduced-motion: reduce)').matches){ | |
| idle=0;$('bIdle').classList.remove('on');$('bIdle').textContent='Idle spin: off'; | |
| } | |
| /* try to resume a local autosave silently */ | |
| try{const c=localStorage.getItem('brainDescentSave'); | |
| if(c){applySave(JSON.parse(atob(c))); | |
| $('stat').textContent='resumed · level '+LEVEL+' room #'+CI;} | |
| }catch(e){/* fresh brain */} | |
| updateLevelHud(); | |
| /* ---------------- resize ---------------- */ | |
| function resize(){ | |
| const dpr=Math.min(devicePixelRatio||1,1.35); | |
| cvs.width=Math.floor(innerWidth*dpr); | |
| cvs.height=Math.floor(innerHeight*dpr); | |
| gl.viewport(0,0,cvs.width,cvs.height); | |
| } | |
| addEventListener('resize',resize);resize(); | |
| /* ---------------- resonance: hot / cold toward the key ---------------- */ | |
| function updateResonance(){ | |
| if(keyFound){$('resT').textContent='key held';$('resFill').style.width='100%';return;} | |
| const kRoom=keyRoomOf(LEVEL); | |
| let res,txt; | |
| if(keyPos){ | |
| const d=Math.hypot(camP[0]-keyPos[0],camP[1]-keyPos[1],camP[2]-keyPos[2]); | |
| res=Math.max(0.35,Math.min(1,1-(d-2)/42)); | |
| txt='very close — '+d.toFixed(0)+' m'; | |
| }else{ | |
| const away=kRoom-CI; | |
| res=Math.max(0.04,0.34/(1+Math.abs(away))); | |
| txt=Math.abs(away)+' room'+(Math.abs(away)===1?'':'s')+(away>0?' ahead':' behind'); | |
| } | |
| $('resT').textContent=txt; | |
| $('resFill').style.width=(res*100).toFixed(0)+'%'; | |
| } | |
| /* ---------------- shader compile: DEFERRED so the boot screen paints first, | |
| PARALLEL where the driver allows it, and any failure shows its log -------- */ | |
| let prog=null,U={}; | |
| function sh(type,src){const s=gl.createShader(type);gl.shaderSource(s,src); | |
| gl.compileShader(s);return s;} | |
| function compileAndStart(){ | |
| const vs=sh(gl.VERTEX_SHADER,VERT), fs=sh(gl.FRAGMENT_SHADER,FRAG); | |
| prog=gl.createProgram(); | |
| gl.attachShader(prog,vs);gl.attachShader(prog,fs); | |
| gl.linkProgram(prog); | |
| const ext=gl.getExtension('KHR_parallel_shader_compile'); | |
| function finish(){ | |
| if(window.dreamTick){clearInterval(window.dreamTick);window.dreamTick=null;} | |
| if(!gl.getProgramParameter(prog,gl.LINK_STATUS)){ | |
| const log=(gl.getShaderInfoLog(fs)||'')+' '+(gl.getShaderInfoLog(vs)||'') | |
| +' '+(gl.getProgramInfoLog(prog)||''); | |
| bootErr('Shader failed: '+log.trim()); | |
| return; | |
| } | |
| gl.useProgram(prog); | |
| ['iRes','iTime','camP','camR','camU','camF','nWell','mode','keyWell','mGain','lensK','camTex', | |
| 'marchN','glassN','escN', | |
| 'wells','wellCol','wellPar','roomPos','roomPar','roomMod','roomShp','roomTintU','seal'] | |
| .forEach(k=>U[k]=gl.getUniformLocation(prog,k)); | |
| gl.uniform1i(U.camTex,0); | |
| /* loop bounds — same values as the old constants, now opaque to the compiler */ | |
| gl.uniform1i(U.marchN,180); | |
| gl.uniform1i(U.glassN,26); | |
| gl.uniform1i(U.escN,10); | |
| $('bootStat').textContent='ready — click to enter'; | |
| requestAnimationFrame(loop); | |
| } | |
| if(ext){ | |
| (function poll(){ | |
| if(gl.getProgramParameter(prog,ext.COMPLETION_STATUS_KHR))finish(); | |
| else requestAnimationFrame(poll); | |
| })(); | |
| }else finish(); | |
| } | |
| /* ---------------- loop ---------------- */ | |
| let t0=performance.now(),simT=0,frames=0,fpsT=t0; | |
| function loop(now){ | |
| const dt=Math.min((now-t0)/1000,0.05);t0=now; | |
| if(idle)simT+=dt; else simT+=dt*0.06; | |
| const cp=Math.cos(pitch),sp=Math.sin(pitch),cy=Math.cos(yaw),sy=Math.sin(yaw); | |
| const F=[sy*cp,sp,cy*cp], Rv=[cy,0,-sy], Uv=[-sy*sp,cp,-cy*sp]; | |
| const spd=(keys.ShiftLeft||keys.ShiftRight?14:6)*dt; | |
| let mx=0,my=0,mz=0; | |
| if(keys.KeyW){mx+=F[0]*spd;my+=F[1]*spd;mz+=F[2]*spd;} | |
| if(keys.KeyS){mx-=F[0]*spd;my-=F[1]*spd;mz-=F[2]*spd;} | |
| if(keys.KeyD){mx+=Rv[0]*spd;mz+=Rv[2]*spd;} | |
| if(keys.KeyA){mx-=Rv[0]*spd;mz-=Rv[2]*spd;} | |
| if(keys.KeyE)my+=spd; | |
| if(keys.KeyQ)my-=spd; | |
| if(mx||my||mz)tryMove(camP[0]+mx,camP[1]+my,camP[2]+mz); | |
| /* room transition: deep inside next -> dream forward; inside prev -> re-dream back */ | |
| if(roomSDJ(camP[0],camP[1],camP[2],2)<-1.0)advance(); | |
| else if(roomSDJ(camP[0],camP[1],camP[2],0)<-1.0)retreat(); | |
| uploadBoundary(simT); | |
| const nW=packWorld(simT); | |
| /* key pickup: touch the gold crystal */ | |
| if(!keyFound && keyPos){ | |
| const d=Math.hypot(camP[0]-keyPos[0],camP[1]-keyPos[1],camP[2]-keyPos[2]); | |
| if(d<keyR+1.0){ | |
| keyFound=true; keyWell=-1; sealV=[0,0,0,-1]; | |
| banner('KEY CRYSTAL','the seal dissolves — find the far gate'); | |
| updateLevelHud(); autoSave(); | |
| } | |
| } | |
| gl.useProgram(prog); | |
| gl.uniform2f(U.iRes,cvs.width,cvs.height); | |
| gl.uniform1f(U.iTime,simT); | |
| gl.uniform3f(U.camP,camP[0],camP[1],camP[2]); | |
| gl.uniform3f(U.camR,Rv[0],Rv[1],Rv[2]); | |
| gl.uniform3f(U.camU,Uv[0],Uv[1],Uv[2]); | |
| gl.uniform3f(U.camF,F[0],F[1],F[2]); | |
| gl.uniform1i(U.nWell,nW); | |
| gl.uniform1i(U.mode,mode); | |
| gl.uniform1i(U.keyWell,keyWell); | |
| gl.uniform1f(U.mGain,P.mGain); | |
| gl.uniform1f(U.lensK,P.lensK); | |
| gl.uniform4fv(U.wells,wellsF); | |
| gl.uniform3fv(U.wellCol,colsF); | |
| gl.uniform4fv(U.wellPar,parF); | |
| gl.uniform3fv(U.roomPos,roomPosF); | |
| gl.uniform4fv(U.roomPar,roomParF); | |
| gl.uniform4fv(U.roomMod,roomModF); | |
| gl.uniform4fv(U.roomShp,roomShpF); | |
| gl.uniform3fv(U.roomTintU,roomTintF); | |
| gl.uniform4f(U.seal,sealV[0],sealV[1],sealV[2],sealV[3]); | |
| gl.drawArrays(gl.TRIANGLES,0,3); | |
| frames++; | |
| if(now-fpsT>400){ | |
| $('fps').textContent=Math.round(frames*1000/(now-fpsT))+' fps'; | |
| frames=0;fpsT=now; | |
| $('pos').textContent=camP.map(v=>v.toFixed(1)).join(', '); | |
| updateResonance(); | |
| } | |
| requestAnimationFrame(loop); | |
| } | |
| /* let the boot screen paint before the driver chews on the shader. | |
| the counter is honest elapsed time — compile progress isn't queryable */ | |
| const dreamT0=performance.now(); | |
| window.dreamTick=setInterval(()=>{ | |
| const s=((performance.now()-dreamT0)/1000)|0; | |
| $('bootStat').textContent='dreaming the shader… '+s+'s (first launch only — cached after)'; | |
| },500); | |
| $('bootStat').textContent='dreaming the shader… (first launch only — cached after)'; | |
| requestAnimationFrame(()=>{try{compileAndStart();}catch(e){bootErr(e&&e.stack||e);}}); | |
| </script> | |