Aluode commited on
Commit
0028e15
·
verified ·
1 Parent(s): dec6e30

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +993 -18
index.html CHANGED
@@ -1,19 +1,994 @@
1
  <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
19
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  <!doctype html>
2
+ <meta charset="utf-8">
3
+ <meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no">
4
+ <title>ANTTI'S BRAIN — DESCENT · infinite procedural key-hunt</title>
5
+ <style>
6
+ :root{
7
+ --void:#04060b; --ink:#c9d4e0; --amber:#e8b13a; --amber-dim:#8a6d2c;
8
+ --cyan:#54d2e2; --line:#1b2436; --dim:#67788d; --green:#4ec98f; --red:#e06060;
9
+ --gold:#ffd76a;
10
+ }
11
+ *{box-sizing:border-box}
12
+ html,body{margin:0;height:100%;background:var(--void);overflow:hidden;
13
+ font-family:ui-monospace,"SF Mono",Menlo,Consolas,monospace;color:var(--ink)}
14
+ #gl{position:fixed;inset:0;width:100%;height:100%;display:block;cursor:grab}
15
+ #gl.drag{cursor:grabbing}
16
+ .panel{position:fixed;background:rgba(6,9,16,.74);border:1px solid var(--line);
17
+ backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px)}
18
+ #hud{top:14px;left:14px;padding:12px 14px;min-width:270px;font-size:11px;line-height:1.7}
19
+ #hud .ttl{color:var(--amber);letter-spacing:.16em;font-size:10px;
20
+ text-transform:uppercase;margin-bottom:8px;display:flex;justify-content:space-between;gap:18px}
21
+ #hud .ttl b{color:var(--amber-dim);font-weight:400}
22
+ #hud .row{display:flex;justify-content:space-between;color:#8494a8;gap:14px}
23
+ #hud .row span{color:var(--ink)}
24
+ #hud .row .k{color:var(--dim)}
25
+ #hud .row .room{color:var(--cyan);letter-spacing:.08em}
26
+ #hud .row .lvl{color:var(--gold);letter-spacing:.12em}
27
+ #hud .row .keyok{color:var(--green)}
28
+ #hud .row .keyno{color:var(--gold)}
29
+ #resWrap{margin-top:6px}
30
+ #resWrap label{display:flex;justify-content:space-between;color:var(--dim);font-size:10px;margin-bottom:3px}
31
+ #resWrap label span{color:var(--gold)}
32
+ #resBar{height:4px;background:var(--line);position:relative;overflow:hidden}
33
+ #resFill{position:absolute;inset:0;width:0%;background:linear-gradient(90deg,#8a6d2c,var(--gold));
34
+ transition:width .25s}
35
+ #hud .note{margin-top:7px;border-top:1px solid var(--line);padding-top:6px;
36
+ color:var(--dim);font-size:10px;line-height:1.6;max-width:300px}
37
+ #ctl{bottom:14px;left:14px;padding:12px 14px;width:288px;font-size:11px}
38
+ #ctl h2{margin:0 0 9px;font-size:10px;letter-spacing:.16em;text-transform:uppercase;
39
+ color:var(--cyan);font-weight:600}
40
+ .modes{display:flex;gap:6px;margin-bottom:11px}
41
+ .modes button{flex:1}
42
+ .field{margin:8px 0}
43
+ .field label{display:flex;justify-content:space-between;color:#8494a8;margin-bottom:4px}
44
+ .field label var{color:var(--ink);font-style:normal}
45
+ input[type=range]{width:100%;height:2px;-webkit-appearance:none;appearance:none;
46
+ background:var(--line);outline:none;margin:6px 0}
47
+ input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;width:12px;height:12px;
48
+ border-radius:50%;background:var(--amber);cursor:pointer;border:2px solid var(--void)}
49
+ input[type=range]::-moz-range-thumb{width:12px;height:12px;border-radius:50%;
50
+ background:var(--amber);cursor:pointer;border:2px solid var(--void)}
51
+ .btns{display:flex;gap:6px;margin-top:9px;flex-wrap:wrap}
52
+ button{flex:1;min-width:56px;padding:7px 4px;background:transparent;color:var(--ink);
53
+ border:1px solid var(--line);font:inherit;font-size:10px;letter-spacing:.05em;
54
+ cursor:pointer;transition:.15s;text-transform:uppercase}
55
+ button:hover{border-color:var(--amber-dim);color:var(--amber)}
56
+ button.on{border-color:var(--cyan);color:var(--cyan)}
57
+ button:focus-visible{outline:2px solid var(--cyan);outline-offset:2px}
58
+ #stat{margin-top:7px;font-size:10px;color:var(--dim);min-height:13px;word-break:break-all}
59
+ #help{bottom:14px;right:14px;padding:11px 13px;font-size:10.5px;line-height:1.85;
60
+ color:#73859a;text-align:right}
61
+ #help b{color:var(--amber)}
62
+ #banner{position:fixed;top:38%;left:0;right:0;text-align:center;pointer-events:none;
63
+ opacity:0;transition:opacity .5s;z-index:8}
64
+ #banner .b1{font-size:22px;letter-spacing:.4em;color:var(--gold);text-transform:uppercase;
65
+ text-shadow:0 0 24px rgba(255,215,106,.55)}
66
+ #banner .b2{font-size:11px;color:var(--dim);margin-top:8px;letter-spacing:.14em}
67
+ #banner.show{opacity:1}
68
+ #boot{position:fixed;inset:0;display:flex;flex-direction:column;gap:10px;
69
+ align-items:center;justify-content:center;background:rgba(4,6,11,.92);z-index:9;
70
+ transition:opacity .8s;text-align:center;padding:20px}
71
+ #boot.gone{opacity:0;pointer-events:none}
72
+ #boot .big{font-size:15px;letter-spacing:.28em;color:var(--amber)}
73
+ #boot .sub{font-size:11px;color:var(--dim);line-height:1.9;max-width:520px}
74
+ #boot .sub em{color:var(--gold);font-style:normal}
75
+ #bootStat{font-size:11px;color:var(--cyan);letter-spacing:.12em;min-height:14px}
76
+ #boot.err .big{color:var(--red)}
77
+ #boot.err .sub{color:#c99;word-break:break-all;text-align:left}
78
+ </style>
79
+
80
+ <canvas id="gl"></canvas>
81
+
82
+ <div id="boot">
83
+ <div class="big">ANTTI'S BRAIN — DESCENT</div>
84
+ <div class="sub">an endless chain of chambers, hallucinated from one seed.<br>
85
+ each level is a run of rooms. somewhere in it, one crystal is not like the others:<br>
86
+ it pulses <em>gold</em>. touch it, and the red seal on the far gate dissolves.<br>
87
+ pass the gate: next level. rooms grow heavier, more twisted, stranger in shape.<br><br>
88
+ the <em>resonance</em> meter runs hot as you near the key. no enemies, no clocks —<br>
89
+ only geometry, depth, and the hash. nothing is stored; walk back and every<br>
90
+ room is re-dreamed identically. click / drag to enter · mouse look is inverted.</div>
91
+ <div id="bootStat">waking up…</div>
92
+ </div>
93
+
94
+ <div id="banner"><div class="b1" id="bn1"></div><div class="b2" id="bn2"></div></div>
95
+
96
+ <div id="hud" class="panel">
97
+ <div class="ttl">Brain — Descent <b id="fps">— fps</b></div>
98
+ <div class="row"><span class="k">level</span><span class="lvl" id="lvlV">1</span></div>
99
+ <div class="row"><span class="k">room in level</span><span id="progV">—</span></div>
100
+ <div class="row"><span class="k">chamber</span><span class="room" id="roomV">—</span></div>
101
+ <div class="row"><span class="k">index / shape</span><span id="roomI">—</span></div>
102
+ <div class="row"><span class="k">mass · twist</span><span id="massV">—</span></div>
103
+ <div class="row"><span class="k">key crystal</span><span id="keyV" class="keyno">not found</span></div>
104
+ <div id="resWrap"><label>resonance <span id="resT">—</span></label>
105
+ <div id="resBar"><div id="resFill"></div></div></div>
106
+ <div class="row" style="margin-top:6px"><span class="k">mode</span><span id="modeV">Boundary</span></div>
107
+ <div class="row"><span class="k">boundary</span><span id="src">—</span></div>
108
+ <div class="row"><span class="k">music</span><span id="musV">—</span></div>
109
+ <div class="row"><span class="k">position</span><span id="pos">—</span></div>
110
+ <div class="note" id="modeNote">—</div>
111
+ </div>
112
+
113
+ <div id="ctl" class="panel">
114
+ <h2>The descent</h2>
115
+ <div class="modes">
116
+ <button id="m0" class="on">Boundary</button>
117
+ <button id="m1">Projection</button>
118
+ <button id="m2">Cascade</button>
119
+ </div>
120
+ <div class="field"><label>Mass gain <var id="m_l">1.0</var></label>
121
+ <input id="m" type="range" min="0.2" max="2.2" step="0.05" value="1.0"></div>
122
+ <div class="field"><label>Lensing <var id="g_l">1.0</var></label>
123
+ <input id="g" type="range" min="0" max="3" step="0.05" value="1.0"></div>
124
+ <div class="field"><label>Music volume <var id="v_l">0.5</var></label>
125
+ <input id="v" type="range" min="0" max="1" step="0.05" value="0.5"></div>
126
+ <div class="btns">
127
+ <button id="bSave">Save</button>
128
+ <button id="bLoad">Load</button>
129
+ <button id="bCode">Save code</button>
130
+ </div>
131
+ <div class="btns">
132
+ <button id="bMus" class="on">Music: on</button>
133
+ <button id="bSrc">Test pattern</button>
134
+ </div>
135
+ <div class="btns">
136
+ <button id="bSeed">New brain</button>
137
+ <button id="bIdle" class="on">Idle spin: on</button>
138
+ </div>
139
+ <div id="stat"></div>
140
+ </div>
141
+
142
+ <div id="help" class="panel">
143
+ <b>W A S D</b> move · <b>Q E</b> down/up<br>
144
+ drag to look (inverted) · <b>shift</b> boost<br>
145
+ gold pulse = the key · red membrane = the seal<br>
146
+ the amber thread lives in the gates
147
+ </div>
148
+
149
+ <video id="vid" style="display:none" playsinline muted></video>
150
+
151
+ <script>
152
+ 'use strict';
153
+ /* Do not hype. Do not lie. Just show. */
154
+ const $=id=>document.getElementById(id);
155
+ const cvs=$('gl'), boot=$('boot');
156
+
157
+ /* any failure paints itself on the boot screen instead of dying silently */
158
+ function bootErr(t){
159
+ boot.classList.add('err');boot.classList.remove('gone');
160
+ boot.innerHTML='<div class="big">It broke — here is why</div><div class="sub">'
161
+ +String(t).replace(/[<>&]/g,c=>({'<':'&lt;','>':'&gt;','&':'&amp;'}[c])).slice(0,600)+'</div>';
162
+ }
163
+ window.onerror=(m,s,l,c,e)=>{bootErr((e&&e.stack)||m+' @ line '+l);};
164
+ window.addEventListener('unhandledrejection',e=>bootErr(e.reason));
165
+
166
+ let gl=cvs.getContext('webgl2',{antialias:false,powerPreference:'high-performance'});
167
+ if(!gl)gl=cvs.getContext('webgl2');
168
+ if(!gl){bootErr('No WebGL2 — this brain needs a WebGL2 browser.');throw new Error('no webgl2');}
169
+
170
+ const MAXW=15; /* up to 5 moons x 3 live rooms */
171
+
172
+ const VERT=`#version 300 es
173
+ void main(){vec2 p=vec2((gl_VertexID<<1)&2,gl_VertexID&2);gl_Position=vec4(p*2.0-1.0,0.0,1.0);}`;
174
+
175
+ const FRAG=`#version 300 es
176
+ precision highp float;
177
+ out vec4 O;
178
+ uniform vec2 iRes;
179
+ uniform float iTime;
180
+ uniform vec3 camP,camR,camU,camF;
181
+ uniform int nWell,mode,keyWell;
182
+ uniform vec4 wells[${MAXW}]; /* xyz pos · w size */
183
+ uniform vec3 wellCol[${MAXW}];
184
+ uniform vec4 wellPar[${MAXW}]; /* x shape · y angA · z angB · w star-n */
185
+ uniform vec3 roomPos[3]; /* prev · current · next */
186
+ uniform vec4 roomPar[3]; /* x shape · y size · z half-height · w star-n */
187
+ uniform vec4 roomMod[3]; /* x twist · y ripple amp · z ripple freq · w mass */
188
+ uniform vec3 roomTintU[3];
189
+ uniform vec4 seal; /* xyz pos · w radius (<0 = open) */
190
+ uniform float mGain,lensK;
191
+ uniform sampler2D camTex;
192
+
193
+ /* ---------- primitives ---------- */
194
+ 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);}
195
+ float sdBox3(vec3 p,vec3 b){vec3 q=abs(p)-b;return length(max(q,0.))+min(max(q.x,max(q.y,q.z)),0.);}
196
+ float sdHexP(vec3 p,vec2 h){ /* cross-section xy · half-height z */
197
+ const vec3 k=vec3(-0.8660254,0.5,0.57735);
198
+ p=abs(p);
199
+ p.xy-=2.0*min(dot(k.xy,p.xy),0.0)*k.xy;
200
+ vec2 d=vec2(length(p.xy-vec2(clamp(p.x,-k.z*h.x,k.z*h.x),h.x))*sign(p.y-h.x),p.z-h.y);
201
+ return min(max(d.x,d.y),0.0)+length(max(d,0.0));}
202
+ float sdStarN(vec2 p,float r,float n,float m){ /* n-pointed star, m in (2,n) */
203
+ float an=3.1415927/n, en=3.1415927/m;
204
+ vec2 acs=vec2(cos(an),sin(an)), ecs=vec2(cos(en),sin(en));
205
+ float bn=mod(atan(p.x,p.y),2.0*an)-an;
206
+ p=length(p)*vec2(cos(bn),abs(sin(bn)));
207
+ p-=r*acs;
208
+ p+=ecs*clamp(-dot(p,ecs),0.0,r*acs.y/ecs.y);
209
+ return length(p)*sign(p.x);}
210
+ float sdOcta(vec3 p,float s){p=abs(p);return (p.x+p.y+p.z-s)*0.57735;}
211
+ float opExtr(float d2,float z,float h){vec2 w=vec2(d2,abs(z)-h);
212
+ return min(max(w.x,w.y),0.0)+length(max(w,0.0));}
213
+ float sdSeg(vec3 p,vec3 a,vec3 b){vec3 pa=p-a,ba=b-a;
214
+ float h=clamp(dot(pa,ba)/dot(ba,ba),0.0,1.0);return length(pa-ba*h);}
215
+
216
+ /* ---------- the twelve shapes. rooms and moons share this ONE chain ----------
217
+ signed, negative inside. s = radius-ish, h = half-height, n = star points */
218
+ float shapeSD(vec3 q,float sh,float s,float h,float n){
219
+ int i=int(sh+0.5);
220
+ if(i==0) return sdBox3(q,vec3(s,h,s));
221
+ if(i==1) return length(q)-s;
222
+ if(i==2) return sdHexP(vec3(q.x,q.z,q.y),vec2(s,h));
223
+ if(i==3){float m=2.0+(n-2.0)*0.45;
224
+ return opExtr(sdStarN(q.xz,s*1.15,n,m),q.y,h);}
225
+ if(i==4) return sdOcta(q,s*1.5);
226
+ if(i==5) return max(length(q.xz)-s,abs(q.y)-h); /* cylinder */
227
+ if(i==6){vec3 c=q; c.y-=clamp(c.y,-h*0.55,h*0.55); /* capsule vault */
228
+ return length(c)-s*0.90;}
229
+ if(i==7) return max(sdBox3(q,vec3(s*0.95)),sdOcta(q,s*1.45)); /* cut crystal */
230
+ if(i==8) return (length(vec3(q.x,q.y*1.35,q.z))-s*1.08)*0.72; /* ellipsoid dome */
231
+ if(i==9) return min(sdBox3(q,vec3(s,h*0.55,s*0.38)), /* cross cathedral */
232
+ min(sdBox3(q,vec3(s*0.38,h*0.55,s)),
233
+ sdBox3(q,vec3(s*0.45,h,s*0.45))));
234
+ if(i==10){float a=atan(q.z,q.x); /* gear */
235
+ float rr=s*(0.88+0.11*cos(a*n*2.0));
236
+ return max(length(q.xz)-rr,abs(q.y)-h)*0.68;}
237
+ /* 11 = wheel: torus + hub + four spokes, all walkable */
238
+ float tor=length(vec2(length(q.xz)-s*0.72,q.y))-s*0.40;
239
+ float hub=length(q)-s*0.40;
240
+ float sp=min(min(sdSeg(q,vec3(0.),vec3(s*0.72,0.,0.)),
241
+ sdSeg(q,vec3(0.),vec3(-s*0.72,0.,0.))),
242
+ min(sdSeg(q,vec3(0.),vec3(0.,0.,s*0.72)),
243
+ sdSeg(q,vec3(0.),vec3(0.,0.,-s*0.72))))-s*0.17;
244
+ return min(tor,min(hub,sp));
245
+ }
246
+ float roomSD(vec3 p,int r){
247
+ vec3 q=p-roomPos[r];
248
+ float tw=roomMod[r].x;
249
+ if(abs(tw)>0.0005) q.xz=rot2(q.xz,q.y*tw);
250
+ float d=shapeSD(q,roomPar[r].x,roomPar[r].y,roomPar[r].z,roomPar[r].w);
251
+ float ra=roomMod[r].y;
252
+ if(ra>0.001) d+=ra*sin(q.x*roomMod[r].z)*sin(q.y*roomMod[r].z)*sin(q.z*roomMod[r].z);
253
+ return d/(1.0+abs(tw)*roomPar[r].y*0.6+ra*roomMod[r].z); /* Lipschitz guard */
254
+ }
255
+ float worldSD(vec3 p){
256
+ float d=min(roomSD(p,0),min(roomSD(p,1),roomSD(p,2)));
257
+ d=min(d,sdSeg(p,roomPos[0],roomPos[1])-1.7);
258
+ d=min(d,sdSeg(p,roomPos[1],roomPos[2])-1.7);
259
+ return d;}
260
+ int nearestRoom(vec3 p){
261
+ float d0=roomSD(p,0),d1=roomSD(p,1),d2=roomSD(p,2);
262
+ return d0<d1?(d0<d2?0:2):(d1<d2?1:2);}
263
+ vec3 wallNormal(vec3 p){ /* inward-facing */
264
+ const vec2 e=vec2(0.02,-0.02);
265
+ return -normalize(
266
+ e.xyy*worldSD(p+e.xyy)+e.yyx*worldSD(p+e.yyx)+
267
+ e.yxy*worldSD(p+e.yxy)+e.xxx*worldSD(p+e.xxx));}
268
+
269
+ /* ---------- moons: the same twelve shapes, shrunk and rounded into glass.
270
+ core-scale, height-scale and rounding radius per shape, from const tables —
271
+ this keeps the shader small enough for slow driver compilers ---------- */
272
+ const float CS[12]=float[12](0.50,1.00,0.60,0.765,0.613,0.52,0.644,0.590,0.740,0.600,0.568,0.55);
273
+ const float CH[12]=float[12](0.50,1.00,0.34,0.180,0.613,0.42,0.545,0.590,0.740,0.470,0.280,0.55);
274
+ const float CR[12]=float[12](0.26,0.00,0.22,0.16,0.24,0.20,0.00,0.18,0.06,0.14,0.12,0.10);
275
+ float moonSD(vec3 p,int k){
276
+ vec3 q=p-wells[k].xyz;
277
+ float s=wells[k].w;
278
+ float bs=length(q)-s*2.2;
279
+ if(bs>0.6) return bs;
280
+ q.xz=rot2(q.xz,wellPar[k].y);
281
+ q.xy=rot2(q.xy,wellPar[k].z);
282
+ int i=int(wellPar[k].x+0.5);
283
+ return shapeSD(q,wellPar[k].x,s*CS[i],s*CH[i],wellPar[k].w)-s*CR[i];
284
+ }
285
+ vec3 moonNormal(vec3 p,int k){
286
+ const vec2 e=vec2(0.012,-0.012);
287
+ return normalize(
288
+ e.xyy*moonSD(p+e.xyy,k)+e.yyx*moonSD(p+e.yyx,k)+
289
+ e.yxy*moonSD(p+e.yxy,k)+e.xxx*moonSD(p+e.xxx,k));}
290
+
291
+ /* ---------- boundary images ---------- */
292
+ vec3 eqSample(vec3 n){
293
+ float u=0.5+atan(n.z,n.x)*0.159155;
294
+ float v=0.5-asin(clamp(n.y,-1.0,1.0))*0.318310;
295
+ return texture(camTex,vec2(1.0-u,v)).rgb;}
296
+ vec3 wallImg(vec3 p){
297
+ int r=nearestRoom(p);
298
+ return eqSample(normalize(p-roomPos[r]))*roomTintU[r];}
299
+
300
+ void main(){
301
+ vec2 uv0=(gl_FragCoord.xy-0.5*iRes)/iRes.y;
302
+ vec3 x=camP;
303
+ vec3 rd=normalize(uv0.x*camR+uv0.y*camU+1.5*camF);
304
+ vec3 glow=vec3(0.0), tint=vec3(1.0), accum=vec3(0.0), col=vec3(0.0);
305
+ float trav=0.0, stp=0.05;
306
+ int bnc=0; bool done=false;
307
+ const int MAXB=12;
308
+ /* the amber thread lives only in the corridors, not in the rooms */
309
+ vec3 gA0=mix(roomPos[0],roomPos[1],0.28), gB0=mix(roomPos[0],roomPos[1],0.72);
310
+ vec3 gA1=mix(roomPos[1],roomPos[2],0.28), gB1=mix(roomPos[1],roomPos[2],0.72);
311
+
312
+ for(int i=0;i<180;i++){
313
+ /* lensing: deflection per path length, dtheta ~ gradPhi * ds */
314
+ if(lensK>0.001){
315
+ vec3 pull=vec3(0.0);
316
+ for(int k=0;k<nWell;k++){
317
+ vec3 dv=wells[k].xyz-x; float r2=dot(dv,dv)+0.4;
318
+ pull+=dv*inversesqrt(r2)*(wells[k].w*mGain/r2); }
319
+ rd=normalize(rd+pull*stp*0.055*lensK);
320
+ }
321
+ float dm=1e9; int hb=-1;
322
+ for(int k=0;k<nWell;k++){
323
+ float sd=moonSD(x,k);
324
+ float r=length(x-wells[k].xyz);
325
+ float e=wells[k].w*mGain/(r+1.0);
326
+ glow+=wellCol[k]*e*e*0.03*stp;
327
+ if(k==keyWell) /* the key crystal breathes gold */
328
+ glow+=vec3(1.0,0.80,0.30)*e*e*(0.05+0.045*sin(iTime*4.0))*stp;
329
+ if(sd<dm){dm=sd;hb=k;} }
330
+ float dw=-worldSD(x);
331
+ {
332
+ float dg=min(sdSeg(x,gA0,gB0),sdSeg(x,gA1,gB1));
333
+ glow+=vec3(0.91,0.69,0.23)*(0.010*stp/(dg*dg+0.15))*smoothstep(4.0,1.0,dg);
334
+ }
335
+ float dsl=1e9;
336
+ if(seal.w>0.0){
337
+ dsl=length(x-seal.xyz)-seal.w;
338
+ glow+=vec3(0.95,0.22,0.18)*(0.012*stp/(dsl*dsl+0.22))*smoothstep(6.0,1.5,dsl);
339
+ if(dsl<0.04){ /* the red membrane: sealed exit */
340
+ float pl=0.5+0.5*sin(iTime*3.0+x.y*2.2+x.x*1.3);
341
+ col=(vec3(0.72,0.10,0.09)+vec3(0.35,0.06,0.05)*pl)*tint;
342
+ done=true; break;
343
+ }
344
+ }
345
+
346
+ if(dm<0.03 && hb>=0){
347
+ vec3 n=moonNormal(x,hb);
348
+ float F=0.04+0.96*pow(1.0-max(dot(-rd,n),0.0),5.0); /* Schlick, glass F0 */
349
+ if(mode==1){ /* PROJECTION: moons carry the image (opaque) */
350
+ float L=0.0; for(int k=0;k<nWell;k++){
351
+ L+=wells[k].w*mGain/(length(x-wells[k].xyz)+1.0);}
352
+ col=tint*eqSample(n)*(0.55+0.7*min(L,1.4)); done=true; break;
353
+ }
354
+ /* BOUNDARY & CASCADE: heavy optical crystal.
355
+ Fresnel glints off the surface; the ray itself REFRACTS (Snell),
356
+ marches the negative-distance chord inside, exits the far side,
357
+ and keeps flying — you see the world warped through the glass. */
358
+ glow+=tint*eqSample(reflect(rd,n))*F*0.55; /* mirror glint, Fresnel-weighted */
359
+ if(mode==2) accum+=tint*eqSample(n)*F*1.1; /* cascade: crystal wears the face at the rims */
360
+ bnc++; if(bnc>MAXB){col=(mode==2)?accum:vec3(0.0);done=true;break;}
361
+ const float IOR=1.45;
362
+ vec3 d=refract(rd,n,1.0/IOR);
363
+ if(dot(d,d)<1e-4) d=reflect(rd,n); /* entry-grazing guard */
364
+ x-=n*0.04; /* step inside the glass */
365
+ float chord=0.0;
366
+ for(int j=0;j<26;j++){
367
+ float sd=moonSD(x,hb);
368
+ if(sd>-0.006){ /* reached the far surface */
369
+ vec3 ne=moonNormal(x,hb);
370
+ vec3 ro=refract(d,-ne,IOR);
371
+ if(dot(ro,ro)<1e-4){ d=reflect(d,-ne); x+=d*0.03; chord+=0.03; continue; } /* TIR: bounce inside */
372
+ d=ro; break; /* refract out */
373
+ }
374
+ float st=max(-sd*0.8,0.025);
375
+ x+=d*st; chord+=st;
376
+ }
377
+ /* Beer-Lambert: the crystal's colour tints what passes through, by chord length */
378
+ tint*=exp(-(vec3(1.0)-wellCol[hb])*chord*0.35)*0.96;
379
+ rd=d;
380
+ vec3 ne2=moonNormal(x,hb);
381
+ for(int j=0;j<10;j++){ if(moonSD(x,hb)>0.05)break; x+=ne2*0.05; }
382
+ x+=rd*0.04; trav+=chord+0.08; stp=0.05; continue;
383
+ }
384
+ if(dw<0.03){
385
+ if(mode==0){ /* BOUNDARY: walls are the image */
386
+ vec3 wc=wallImg(x);
387
+ float L=0.0; for(int k=0;k<nWell;k++){
388
+ L+=wells[k].w*mGain/(length(x-wells[k].xyz)+1.0);}
389
+ float fog=exp(-trav*0.010);
390
+ col=wc*tint*(0.30+1.1*min(L,1.6))*fog; done=true; break;
391
+ } else { /* PROJECTION / CASCADE: walls mirror */
392
+ vec3 n=wallNormal(x);
393
+ rd=reflect(rd,n); tint*=0.85; bnc++;
394
+ if(bnc>MAXB){ col=(mode==2)?accum:vec3(0.0); done=true; break; }
395
+ for(int j=0;j<10;j++){ if(-worldSD(x)>0.05)break; x+=n*0.05; }
396
+ x+=rd*0.04; stp=0.05; continue;
397
+ }
398
+ }
399
+ stp=clamp(min(min(dm,dw),dsl)*0.9,0.03,3.0);
400
+ x+=rd*stp; trav+=stp;
401
+ if(trav>260.0)break;
402
+ }
403
+ if(!done) col=(mode==2)?accum:vec3(0.0);
404
+ col+=glow;
405
+ col=col/(col+1.0);
406
+ col=pow(col,vec3(0.4545));
407
+ float vig=1.0-0.35*dot(uv0,uv0);
408
+ O=vec4(col*vig,1.0);
409
+ }`;
410
+
411
+ /* ---------------- boundary texture (webcam + fallback) ---------------- */
412
+ const vid=$('vid');
413
+ let camOK=false, useCam=true;
414
+ const camTex=gl.createTexture();
415
+ gl.activeTexture(gl.TEXTURE0);
416
+ gl.bindTexture(gl.TEXTURE_2D,camTex);
417
+ gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_MIN_FILTER,gl.LINEAR);
418
+ gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_MAG_FILTER,gl.LINEAR);
419
+ gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_WRAP_S,gl.REPEAT);
420
+ gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_WRAP_T,gl.CLAMP_TO_EDGE);
421
+
422
+ if(navigator.mediaDevices&&navigator.mediaDevices.getUserMedia){
423
+ navigator.mediaDevices.getUserMedia({video:{width:640,height:480,facingMode:'user'}})
424
+ .then(st=>{vid.srcObject=st;vid.play();camOK=true;setSrcLabel();})
425
+ .catch(()=>{useCam=false;camOK=false;setSrcLabel();});
426
+ }else{useCam=false;setSrcLabel();}
427
+
428
+ const pat=document.createElement('canvas');pat.width=256;pat.height=256;
429
+ const pctx=pat.getContext('2d');
430
+ function drawPattern(t){
431
+ const g=pctx.createLinearGradient(0,0,0,256);
432
+ g.addColorStop(0,'#2a3550');g.addColorStop(1,'#6a4a70');
433
+ pctx.fillStyle=g;pctx.fillRect(0,0,256,256);
434
+ const cx=128+64*Math.cos(0.6*t),cy=128+48*Math.sin(0.9*t);
435
+ for(let s=0;s<24;s++){
436
+ pctx.fillStyle=s%2?'#d8c890':'#705838';
437
+ pctx.beginPath();pctx.arc(cx,cy,46-s*1.9,0,7);pctx.fill();
438
+ }
439
+ pctx.fillStyle='#101018';pctx.fillRect(176,56,40,40);
440
+ pctx.fillStyle='#c9d4e0';pctx.font='14px monospace';
441
+ pctx.fillText('no camera — pattern',56,240);
442
+ }
443
+ function setSrcLabel(){
444
+ $('src').textContent=useCam&&camOK?'webcam live':'test pattern';
445
+ $('bSrc').textContent=useCam?'Test pattern':'Use camera';
446
+ }
447
+ function uploadBoundary(t){
448
+ gl.activeTexture(gl.TEXTURE0);
449
+ gl.bindTexture(gl.TEXTURE_2D,camTex);
450
+ gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL,true);
451
+ if(useCam&&camOK&&vid.readyState>=2){
452
+ gl.texImage2D(gl.TEXTURE_2D,0,gl.RGBA,gl.RGBA,gl.UNSIGNED_BYTE,vid);
453
+ }else{
454
+ drawPattern(t);
455
+ gl.texImage2D(gl.TEXTURE_2D,0,gl.RGBA,gl.RGBA,gl.UNSIGNED_BYTE,pat);
456
+ }
457
+ gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL,false);
458
+ }
459
+
460
+ /* =====================================================================
461
+ THE PROCEDURAL BRAIN — now with LEVELS
462
+ Every chamber is a pure function of (SEED, index). Nothing is stored.
463
+ Level L is a run of depth(L) rooms. One room hides the KEY CRYSTAL
464
+ (moon 0, gold, pulsing). The far gate of the level is SEALED until
465
+ the key is touched. Room mass, twist, ripple and the shape pool all
466
+ deepen with the level. Save = 6 numbers. Everything else is hash.
467
+ ===================================================================== */
468
+ let SEED=(Math.random()*4294967296)>>>0;
469
+ function rng(i,salt){
470
+ let t=(SEED ^ Math.imul(i|0,2654435761) ^ Math.imul(salt|0,40503))>>>0;
471
+ t=Math.imul(t^(t>>>15),t|1);
472
+ t^=t+Math.imul(t^(t>>>7),t|61);
473
+ return ((t^(t>>>14))>>>0)/4294967296;
474
+ }
475
+ const SHAPENAMES=['CUBE','SPHERE','HEX PRISM','STAR','OCTAHEDRON','CYLINDER',
476
+ 'CAPSULE VAULT','CUT CRYSTAL','DOME','CROSS CATHEDRAL','GEAR','WHEEL'];
477
+ const SYL=['KA','RA','JA','PIN','TA','VUO','LU','MI','SA','TÄH','TI','KI','VI','HEL','ME','NU','SO','LO'];
478
+
479
+ /* ------- level geometry: depth(L) rooms per level, cumulative starts ------- */
480
+ function depthOf(L){return Math.min(3+L,10);} /* L1: 4 rooms … caps at 10 */
481
+ function levelStartOf(L){let s=0;for(let k=1;k<L;k++)s+=depthOf(k);return s;}
482
+ function levelOfRoom(i){
483
+ if(i<0)return 1; /* the ante-brain: level 1 rules */
484
+ let L=1,s=0;
485
+ while(s+depthOf(L)<=i){s+=depthOf(L);L++;}
486
+ return L;
487
+ }
488
+ function rngL(L,salt){return rng(0x5EED+Math.imul(L,131),7000+salt);}
489
+ function keyRoomOf(L){const d=depthOf(L);
490
+ return levelStartOf(L)+1+Math.floor(rngL(L,1)*(d-1));}
491
+ function exitRoomOf(L){return levelStartOf(L)+depthOf(L)-1;}
492
+ function nMoonsOf(L){return Math.min(3+((L-1)>>1),5);}
493
+
494
+ function roomDef(i){
495
+ const L=levelOfRoom(i);
496
+ const pool=Math.min(6+Math.max(L-1,0),12); /* shapes unlock with depth */
497
+ const shape=Math.floor(rng(i,1)*pool);
498
+ const size=7.0+rng(i,2)*4.0+Math.min(L*0.15,3.0);
499
+ const height=4.5+rng(i,3)*2.5;
500
+ const starN=5+Math.floor(rng(i,4)*3); /* 5, 6 or 7 points */
501
+ const hue=rng(i,5);
502
+ const turn=(rng(i,6)-0.5)*2.4; /* gate heading vs. entry, +-69 deg */
503
+ const twist=(rng(i,8)-0.5)*2.0*Math.min(0.015+L*0.011,0.13);
504
+ const ripA=Math.min(Math.max(L-2,0)*0.06,0.45)*rng(i,9);
505
+ const ripF=0.5+rng(i,10)*1.2;
506
+ const massK=Math.min(0.7+rng(i,11)*0.9+L*0.04,2.8);
507
+ let name='';const nc=2+Math.floor(rng(i,7)*2);
508
+ for(let k=0;k<nc;k++)name+=SYL[Math.floor(rng(i,20+k)*SYL.length)];
509
+ const tint=[0.86+0.14*Math.cos(6.283*hue),
510
+ 0.86+0.14*Math.cos(6.283*(hue+0.33)),
511
+ 0.86+0.14*Math.cos(6.283*(hue+0.66))];
512
+ return {shape,size,height,starN,hue,turn,name,tint,twist,ripA,ripF,massK,level:L};
513
+ }
514
+ function moonDef(i,m){ /* moon m of room i — re-dreamed identically every visit */
515
+ const s=100+m*13;
516
+ const mass=-Math.log(1e-6+rng(i,s+1));
517
+ return {mass,
518
+ 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,
519
+ px:rng(i,s+5)*6.283, py:rng(i,s+6)*6.283, pz:rng(i,s+7)*6.283,
520
+ 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,
521
+ ra:(rng(i,s+11)-0.5)*1.2, rb:(rng(i,s+12)-0.5)*1.2,
522
+ pa:rng(i,s+13)*6.283, pb:rng(i,s+14)*6.283,
523
+ hue:rng(i,s+15)};
524
+ }
525
+
526
+ /* ------- game state ------- */
527
+ let LEVEL=1, keyFound=false, maxReached=1;
528
+ let CI=0, dirIn=[Math.sin(rng(0,99)*6.283),Math.cos(rng(0,99)*6.283)];
529
+ let R={}; /* the three live rooms: def, center, index */
530
+ 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]];}
531
+ function computeDirIn(N){ /* dirIn is a pure function of SEED and N */
532
+ let d=[Math.sin(rng(0,99)*6.283),Math.cos(rng(0,99)*6.283)];
533
+ if(N>0){for(let j=0;j<N;j++)d=rotXZ(d,roomDef(j).turn);}
534
+ else if(N<0){for(let j=-1;j>=N;j--)d=rotXZ(d,-roomDef(j).turn);}
535
+ return d;
536
+ }
537
+ function rebuildChain(){
538
+ const dPrev=roomDef(CI-1), dCur=roomDef(CI), dNext=roomDef(CI+1);
539
+ const dirOut=rotXZ(dirIn,dCur.turn);
540
+ const Dn=dCur.size+dNext.size+12.0, Dp=dPrev.size+dCur.size+12.0;
541
+ R={
542
+ idx:[CI-1,CI,CI+1],
543
+ def:[dPrev,dCur,dNext],
544
+ pos:[[-dirIn[0]*Dp,0,-dirIn[1]*Dp],[0,0,0],[dirOut[0]*Dn,0,dirOut[1]*Dn]],
545
+ dirOut
546
+ };
547
+ $('roomV').textContent=dCur.name;
548
+ $('roomI').textContent='#'+CI+' · '+(dCur.shape===3?'STAR-'+dCur.starN:SHAPENAMES[dCur.shape]);
549
+ $('massV').textContent=dCur.massK.toFixed(2)+' · '+(dCur.twist>=0?'+':'')+dCur.twist.toFixed(3);
550
+ updateLevelHud();
551
+ }
552
+ function banner(a,b){
553
+ $('bn1').textContent=a;$('bn2').textContent=b||'';
554
+ const el=$('banner');el.classList.add('show');
555
+ clearTimeout(banner.t);banner.t=setTimeout(()=>el.classList.remove('show'),2600);
556
+ }
557
+ function updateLevelHud(){
558
+ $('lvlV').textContent=LEVEL;
559
+ const rIn=CI-levelStartOf(LEVEL);
560
+ $('progV').textContent=(CI<0?'—':(rIn+1)+' / '+depthOf(LEVEL));
561
+ $('keyV').textContent=keyFound?'found — seal open':'not found';
562
+ $('keyV').className=keyFound?'keyok':'keyno';
563
+ }
564
+ function onRoomChanged(){
565
+ const newL=levelOfRoom(CI);
566
+ if(newL>LEVEL){
567
+ LEVEL=newL;
568
+ keyFound=(LEVEL+1<=maxReached); /* re-visiting a beaten level? seal stays open */
569
+ if(LEVEL>maxReached){maxReached=LEVEL;keyFound=false;
570
+ banner('LEVEL '+LEVEL,depthOf(LEVEL)+' rooms · find the gold crystal');
571
+ autoSave();}
572
+ }else if(newL<LEVEL){
573
+ LEVEL=newL;
574
+ keyFound=(LEVEL+1<=maxReached); /* walked back: that seal was already opened */
575
+ }
576
+ updateLevelHud();
577
+ }
578
+ function advance(){ /* stepped through the forward gate */
579
+ const shift=R.pos[2];
580
+ camP=[camP[0]-shift[0],camP[1]-shift[1],camP[2]-shift[2]];
581
+ dirIn=R.dirOut; CI++; rebuildChain(); onRoomChanged();
582
+ }
583
+ function retreat(){ /* stepped back — re-dream the past */
584
+ const shift=R.pos[0];
585
+ camP=[camP[0]-shift[0],camP[1]-shift[1],camP[2]-shift[2]];
586
+ const dPrev=roomDef(CI-1);
587
+ dirIn=rotXZ(dirIn,-dPrev.turn); CI--; rebuildChain(); onRoomChanged();
588
+ }
589
+
590
+ /* ---------------- JS twins of the SDFs (collision + transitions) ---------- */
591
+ function sdBoxJ(px,py,pz,bx,by,bz){
592
+ const qx=Math.abs(px)-bx,qy=Math.abs(py)-by,qz=Math.abs(pz)-bz;
593
+ return Math.hypot(Math.max(qx,0),Math.max(qy,0),Math.max(qz,0))
594
+ +Math.min(Math.max(qx,Math.max(qy,qz)),0);}
595
+ function sdHexJ(px,py,pz,h1,h2){
596
+ const kx=-0.8660254,ky=0.5,kz=0.57735;
597
+ px=Math.abs(px);py=Math.abs(py);pz=Math.abs(pz);
598
+ const d0=Math.min(kx*px+ky*py,0)*2;
599
+ px-=d0*kx;py-=d0*ky;
600
+ const cl=Math.min(Math.max(px,-kz*h1),kz*h1);
601
+ const dx=Math.hypot(px-cl,py-h1)*Math.sign(py-h1);
602
+ const dy=pz-h2;
603
+ return Math.min(Math.max(dx,dy),0)+Math.hypot(Math.max(dx,0),Math.max(dy,0));}
604
+ function sdStarNJ(px,py,r,n,m){
605
+ const an=Math.PI/n,en=Math.PI/m;
606
+ const ax=Math.cos(an),ay=Math.sin(an),ex=Math.cos(en),ey=Math.sin(en);
607
+ let bn=Math.atan2(px,py)%(2*an); if(bn<0)bn+=2*an; bn-=an;
608
+ const l=Math.hypot(px,py);
609
+ px=l*Math.cos(bn); py=l*Math.abs(Math.sin(bn));
610
+ px-=r*ax; py-=r*ay;
611
+ const cl=Math.min(Math.max(-(px*ex+py*ey),0),r*ay/ey);
612
+ px+=ex*cl; py+=ey*cl;
613
+ return Math.hypot(px,py)*Math.sign(px);}
614
+ function opExtrJ(d2,z,h){const wy=Math.abs(z)-h;
615
+ return Math.min(Math.max(d2,wy),0)+Math.hypot(Math.max(d2,0),Math.max(wy,0));}
616
+ function sdSegJ(px,py,pz,a,b){
617
+ const bax=b[0]-a[0],bay=b[1]-a[1],baz=b[2]-a[2];
618
+ const pax=px-a[0],pay=py-a[1],paz=pz-a[2];
619
+ const h=Math.min(Math.max((pax*bax+pay*bay+paz*baz)/(bax*bax+bay*bay+baz*baz),0),1);
620
+ return Math.hypot(pax-bax*h,pay-bay*h,paz-baz*h);}
621
+ function sdSegOJ(px,py,pz,ax,ay,az,bx,by,bz){
622
+ const bax=bx-ax,bay=by-ay,baz=bz-az;
623
+ const pax=px-ax,pay=py-ay,paz=pz-az;
624
+ const h=Math.min(Math.max((pax*bax+pay*bay+paz*baz)/(bax*bax+bay*bay+baz*baz),0),1);
625
+ return Math.hypot(pax-bax*h,pay-bay*h,paz-baz*h);}
626
+ function shapeSDJ(x,y,z,d){
627
+ const s=d.size,h=d.height;
628
+ switch(d.shape){
629
+ case 0:return sdBoxJ(x,y,z,s,h,s);
630
+ case 1:return Math.hypot(x,y,z)-s;
631
+ case 2:return sdHexJ(x,z,y,s,h);
632
+ case 3:{const m=2.0+(d.starN-2.0)*0.45;
633
+ return opExtrJ(sdStarNJ(x,z,s*1.15,d.starN,m),y,h);}
634
+ case 4:return (Math.abs(x)+Math.abs(y)+Math.abs(z)-s*1.5)*0.57735;
635
+ case 5:return Math.max(Math.hypot(x,z)-s,Math.abs(y)-h);
636
+ case 6:{const cy=y-Math.min(Math.max(y,-h*0.55),h*0.55);
637
+ return Math.hypot(x,cy,z)-s*0.90;}
638
+ case 7:return Math.max(sdBoxJ(x,y,z,s*0.95,s*0.95,s*0.95),
639
+ (Math.abs(x)+Math.abs(y)+Math.abs(z)-s*1.45)*0.57735);
640
+ case 8:return (Math.hypot(x,y*1.35,z)-s*1.08)*0.72;
641
+ case 9:return Math.min(sdBoxJ(x,y,z,s,h*0.55,s*0.38),
642
+ Math.min(sdBoxJ(x,y,z,s*0.38,h*0.55,s),
643
+ sdBoxJ(x,y,z,s*0.45,h,s*0.45)));
644
+ case 10:{const a=Math.atan2(z,x);
645
+ const rr=s*(0.88+0.11*Math.cos(a*d.starN*2.0));
646
+ return Math.max(Math.hypot(x,z)-rr,Math.abs(y)-h)*0.68;}
647
+ default:{const tor=Math.hypot(Math.hypot(x,z)-s*0.72,y)-s*0.40;
648
+ const hub=Math.hypot(x,y,z)-s*0.40;
649
+ let sp=Math.min(sdSegOJ(x,y,z,0,0,0,s*0.72,0,0),
650
+ sdSegOJ(x,y,z,0,0,0,-s*0.72,0,0));
651
+ sp=Math.min(sp,Math.min(sdSegOJ(x,y,z,0,0,0,0,0,s*0.72),
652
+ sdSegOJ(x,y,z,0,0,0,0,0,-s*0.72)))-s*0.17;
653
+ return Math.min(tor,Math.min(hub,sp));}
654
+ }
655
+ }
656
+ function roomSDJ(px,py,pz,r){
657
+ const d=R.def[r],c=R.pos[r];
658
+ let x=px-c[0],y=py-c[1],z=pz-c[2];
659
+ if(Math.abs(d.twist)>0.0005){
660
+ const ca=Math.cos(y*d.twist),sa=Math.sin(y*d.twist);
661
+ const nx=ca*x-sa*z, nz=sa*x+ca*z; x=nx; z=nz;
662
+ }
663
+ let sd=shapeSDJ(x,y,z,d);
664
+ if(d.ripA>0.001)
665
+ sd+=d.ripA*Math.sin(x*d.ripF)*Math.sin(y*d.ripF)*Math.sin(z*d.ripF);
666
+ return sd/(1.0+Math.abs(d.twist)*d.size*0.6+d.ripA*d.ripF);
667
+ }
668
+ function worldSDJ(px,py,pz){
669
+ let d=Math.min(roomSDJ(px,py,pz,0),Math.min(roomSDJ(px,py,pz,1),roomSDJ(px,py,pz,2)));
670
+ d=Math.min(d,sdSegJ(px,py,pz,R.pos[0],R.pos[1])-1.7);
671
+ d=Math.min(d,sdSegJ(px,py,pz,R.pos[1],R.pos[2])-1.7);
672
+ return d;}
673
+
674
+ /* ---------------- moons + key + seal upload ---------------- */
675
+ let P={mGain:1.0,lensK:1.0},mode=0,idle=1;
676
+ const wellsF=new Float32Array(MAXW*4), colsF=new Float32Array(MAXW*3),
677
+ parF=new Float32Array(MAXW*4),
678
+ roomPosF=new Float32Array(9), roomParF=new Float32Array(12),
679
+ roomModF=new Float32Array(12), roomTintF=new Float32Array(9);
680
+ function moonCol(h){
681
+ 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))];}
682
+ let keyWell=-1, keyPos=null, keyR=0;
683
+ let sealV=[0,0,0,-1];
684
+ function packWorld(t){
685
+ let w=0; keyWell=-1; keyPos=null;
686
+ const nM=nMoonsOf(LEVEL), kRoom=keyRoomOf(LEVEL);
687
+ for(let r=0;r<3;r++){
688
+ const d=R.def[r],c=R.pos[r];
689
+ roomPosF[r*3]=c[0];roomPosF[r*3+1]=c[1];roomPosF[r*3+2]=c[2];
690
+ roomParF[r*4]=d.shape;roomParF[r*4+1]=d.size;roomParF[r*4+2]=d.height;roomParF[r*4+3]=d.starN;
691
+ roomModF[r*4]=d.twist;roomModF[r*4+1]=d.ripA;roomModF[r*4+2]=d.ripF;roomModF[r*4+3]=d.massK;
692
+ roomTintF[r*3]=d.tint[0];roomTintF[r*3+1]=d.tint[1];roomTintF[r*3+2]=d.tint[2];
693
+ const A=d.size*0.62, Ay=d.height*0.55;
694
+ for(let m=0;m<nM && w<MAXW;m++,w++){
695
+ const md=moonDef(R.idx[r],m);
696
+ const isKey=(!keyFound && R.idx[r]===kRoom && m===0);
697
+ let size=(0.45+(md.mass/3.0)*0.75)*P.mGain*d.massK;
698
+ size=Math.min(size,1.6); if(isKey)size=Math.min(size*1.25,1.8);
699
+ /* the key swings wide, off the walking line — you must go to it */
700
+ const ax=isKey?Math.min(md.ax*1.6,0.55):md.ax,
701
+ ay=isKey?Math.min(md.ay*1.5,0.50):md.ay,
702
+ az=isKey?Math.min(md.az*1.6,0.55):md.az;
703
+ wellsF[w*4+0]=c[0]+A*ax*Math.sin(md.fx*t+md.px);
704
+ wellsF[w*4+1]=c[1]+Ay*ay*Math.sin(md.fy*t+md.py);
705
+ wellsF[w*4+2]=c[2]+A*az*Math.sin(md.fz*t+md.pz);
706
+ wellsF[w*4+3]=size;
707
+ const cc=isKey?[1.0,0.84,0.36]:moonCol(md.hue);
708
+ colsF[w*3]=cc[0];colsF[w*3+1]=cc[1];colsF[w*3+2]=cc[2];
709
+ parF[w*4]=d.shape;parF[w*4+1]=md.ra*t+md.pa;parF[w*4+2]=md.rb*t+md.pb;parF[w*4+3]=d.starN;
710
+ if(isKey){keyWell=w;keyPos=[wellsF[w*4],wellsF[w*4+1],wellsF[w*4+2]];keyR=size;}
711
+ }
712
+ }
713
+ /* the seal: a red membrane in the corridor after the level's last room */
714
+ const eRoom=exitRoomOf(LEVEL);
715
+ if(!keyFound && CI===eRoom){
716
+ sealV=[(R.pos[1][0]+R.pos[2][0])*0.5,(R.pos[1][1]+R.pos[2][1])*0.5,
717
+ (R.pos[1][2]+R.pos[2][2])*0.5,2.6];
718
+ }else sealV=[0,0,0,-1];
719
+ return w;
720
+ }
721
+
722
+ /* ---------------- camera & input (mouse INVERTED, as requested) ------------ */
723
+ let camP=[0,0,0], yaw=0, pitch=0;
724
+ function faceForwardGate(){yaw=Math.atan2(R.dirOut[0],R.dirOut[1]);pitch=0;}
725
+ rebuildChain();faceForwardGate();
726
+ const keys={};
727
+ addEventListener('keydown',e=>{keys[e.code]=1;
728
+ if(['KeyW','KeyA','KeyS','KeyD','KeyQ','KeyE'].includes(e.code))e.preventDefault();});
729
+ addEventListener('keyup',e=>{keys[e.code]=0;});
730
+ let dragging=false,lx=0,ly=0;
731
+ cvs.addEventListener('pointerdown',e=>{dragging=true;lx=e.clientX;ly=e.clientY;
732
+ cvs.classList.add('drag');cvs.setPointerCapture(e.pointerId);
733
+ boot.classList.add('gone');startMusic();});
734
+ cvs.addEventListener('pointerup',()=>{dragging=false;cvs.classList.remove('drag');});
735
+ cvs.addEventListener('pointermove',e=>{
736
+ if(!dragging)return;
737
+ yaw+=(e.clientX-lx)*0.0032;pitch-=(e.clientY-ly)*0.0032; /* horiz inverted · vert natural */
738
+ pitch=Math.max(-1.5,Math.min(1.5,pitch));
739
+ lx=e.clientX;ly=e.clientY;
740
+ });
741
+ boot.addEventListener('click',()=>{boot.classList.add('gone');startMusic();});
742
+
743
+ function insideVal(px,py,pz){
744
+ let v=-worldSDJ(px,py,pz);
745
+ if(sealV[3]>0)
746
+ v=Math.min(v,Math.hypot(px-sealV[0],py-sealV[1],pz-sealV[2])-sealV[3]);
747
+ return v;
748
+ }
749
+ function tryMove(nx,ny,nz){
750
+ if(insideVal(nx,ny,nz)>0.35){camP=[nx,ny,nz];return;}
751
+ if(insideVal(nx,camP[1],camP[2])>0.35)camP[0]=nx;
752
+ if(insideVal(camP[0],ny,camP[2])>0.35)camP[1]=ny;
753
+ if(insideVal(camP[0],camP[1],nz)>0.35)camP[2]=nz;
754
+ }
755
+
756
+ /* ---------------- music: game1..4.mp3 next to this file, endless ---------- */
757
+ const TRACKS=['game1.mp3','game2.mp3','game3.mp3','game4.mp3','game5.mp3',
758
+ 'game6.mp3','game7.mp3','game8.mp3','game9.mp3','game10.mp3'];
759
+ let musicOn=true, musicStarted=false, mIdx=Math.floor(Math.random()*TRACKS.length), mFails=0;
760
+ const audio=(typeof Audio!=='undefined')?new Audio():null;
761
+ if(audio){
762
+ audio.preload='auto'; audio.volume=0.5;
763
+ audio.addEventListener('ended',()=>{mFails=0;playTrack(mIdx+1);});
764
+ audio.addEventListener('error',()=>{
765
+ if(++mFails<TRACKS.length)playTrack(mIdx+1);
766
+ else $('musV').textContent='no mp3s found';
767
+ });
768
+ audio.addEventListener('playing',()=>{mFails=0;$('musV').textContent=TRACKS[mIdx];});
769
+ }
770
+ function playTrack(i){
771
+ if(!audio||!musicOn)return;
772
+ mIdx=((i%TRACKS.length)+TRACKS.length)%TRACKS.length;
773
+ audio.src=TRACKS[mIdx];
774
+ audio.play().catch(()=>{/* autoplay policy: waits for a click */});
775
+ }
776
+ function startMusic(){
777
+ if(musicStarted||!musicOn||!audio)return;
778
+ musicStarted=true; playTrack(mIdx);
779
+ }
780
+ $('bMus').onclick=e=>{
781
+ musicOn=!musicOn;
782
+ e.target.classList.toggle('on',musicOn);
783
+ e.target.textContent='Music: '+(musicOn?'on':'off');
784
+ if(!audio)return;
785
+ if(musicOn){musicStarted=false;startMusic();}
786
+ else {audio.pause();$('musV').textContent='off';}
787
+ };
788
+ $('musV').textContent='starts on first click';
789
+
790
+ /* ---------------- save / load ----------------
791
+ the whole game is 6 numbers. everything else is the hash. */
792
+ function saveObj(){return {v:1,s:SEED,l:LEVEL,c:CI,k:keyFound?1:0,m:maxReached};}
793
+ function saveCode(){return btoa(JSON.stringify(saveObj()));}
794
+ function applySave(o){
795
+ if(!o||o.v!==1||typeof o.s!=='number')throw new Error('bad save');
796
+ SEED=o.s>>>0; LEVEL=Math.max(1,o.l|0); CI=o.c|0;
797
+ keyFound=!!o.k; maxReached=Math.max(LEVEL,o.m|0);
798
+ dirIn=computeDirIn(CI); camP=[0,0,0];
799
+ rebuildChain(); faceForwardGate(); updateLevelHud();
800
+ }
801
+ function autoSave(){
802
+ try{localStorage.setItem('brainDescentSave',saveCode());}catch(e){/* fine: file:// or sandbox */}
803
+ }
804
+ $('bSave').onclick=()=>{
805
+ autoSave();
806
+ $('stat').textContent='saved · level '+LEVEL+' room #'+CI+' (Save code gives a portable copy)';
807
+ };
808
+ $('bCode').onclick=()=>{
809
+ const c=saveCode();
810
+ $('stat').textContent='code: '+c;
811
+ if(navigator.clipboard)navigator.clipboard.writeText(c).catch(()=>{});
812
+ };
813
+ $('bLoad').onclick=()=>{
814
+ let code='';
815
+ try{code=localStorage.getItem('brainDescentSave')||'';}catch(e){}
816
+ const inp=prompt('Paste a save code (leave as-is to load the local save):',code);
817
+ if(inp===null)return;
818
+ try{applySave(JSON.parse(atob(inp.trim())));
819
+ $('stat').textContent='loaded · level '+LEVEL+' room #'+CI+' · seed '+SEED.toString(16);
820
+ banner('LEVEL '+LEVEL,'re-dreamed from the hash');
821
+ }catch(e){$('stat').textContent='could not read that save code';}
822
+ };
823
+
824
+ /* ---------------- controls ---------------- */
825
+ const MODES=[
826
+ {name:'Boundary', note:'the walls are your camera. the moons are optical crystals — the room bends and refocuses through every one of them.'},
827
+ {name:'Projection', note:'the moons carry your face; every wall is a mirror. the image emanates from the bulk — the holographic swap.'},
828
+ {name:'Cascade', note:'heavy glass: Fresnel rims wear your face, the ray refracts through and keeps going, tinted by each crystal it crosses.'},
829
+ ];
830
+ function setMode(mi){
831
+ mode=mi;
832
+ ['m0','m1','m2'].forEach((id,i)=>$(id).classList.toggle('on',i===mi));
833
+ $('modeV').textContent=MODES[mi].name;
834
+ $('modeNote').textContent=MODES[mi].note;
835
+ }
836
+ setMode(0);
837
+ function bind(id,lab,fn){const el=$(id);el.addEventListener('input',()=>{fn(parseFloat(el.value));$(lab).textContent=el.value;});}
838
+ bind('m','m_l',v=>{P.mGain=v;});
839
+ bind('g','g_l',v=>{P.lensK=v;});
840
+ bind('v','v_l',v=>{if(audio)audio.volume=v;});
841
+ $('m0').onclick=()=>setMode(0);
842
+ $('m1').onclick=()=>setMode(1);
843
+ $('m2').onclick=()=>setMode(2);
844
+ $('bSrc').onclick=()=>{useCam=!useCam;setSrcLabel();};
845
+ $('bSeed').onclick=()=>{ SEED=(Math.random()*4294967296)>>>0;
846
+ LEVEL=1;maxReached=1;keyFound=false;CI=0;
847
+ dirIn=[Math.sin(rng(0,99)*6.283),Math.cos(rng(0,99)*6.283)];
848
+ camP=[0,0,0]; rebuildChain(); faceForwardGate(); updateLevelHud();
849
+ banner('LEVEL 1','a new brain · seed '+SEED.toString(16));
850
+ $('stat').textContent='new brain · seed '+SEED.toString(16); };
851
+ $('bIdle').onclick=e=>{idle^=1;e.target.classList.toggle('on',!!idle);
852
+ e.target.textContent='Idle spin: '+(idle?'on':'off');};
853
+ if(matchMedia('(prefers-reduced-motion: reduce)').matches){
854
+ idle=0;$('bIdle').classList.remove('on');$('bIdle').textContent='Idle spin: off';
855
+ }
856
+ /* try to resume a local autosave silently */
857
+ try{const c=localStorage.getItem('brainDescentSave');
858
+ if(c){applySave(JSON.parse(atob(c)));
859
+ $('stat').textContent='resumed · level '+LEVEL+' room #'+CI;}
860
+ }catch(e){/* fresh brain */}
861
+ updateLevelHud();
862
+
863
+ /* ---------------- resize ---------------- */
864
+ function resize(){
865
+ const dpr=Math.min(devicePixelRatio||1,1.35);
866
+ cvs.width=Math.floor(innerWidth*dpr);
867
+ cvs.height=Math.floor(innerHeight*dpr);
868
+ gl.viewport(0,0,cvs.width,cvs.height);
869
+ }
870
+ addEventListener('resize',resize);resize();
871
+
872
+ /* ---------------- resonance: hot / cold toward the key ---------------- */
873
+ function updateResonance(){
874
+ if(keyFound){$('resT').textContent='key held';$('resFill').style.width='100%';return;}
875
+ const kRoom=keyRoomOf(LEVEL);
876
+ let res,txt;
877
+ if(keyPos){
878
+ const d=Math.hypot(camP[0]-keyPos[0],camP[1]-keyPos[1],camP[2]-keyPos[2]);
879
+ res=Math.max(0.35,Math.min(1,1-(d-2)/42));
880
+ txt='very close — '+d.toFixed(0)+' m';
881
+ }else{
882
+ const away=kRoom-CI;
883
+ res=Math.max(0.04,0.34/(1+Math.abs(away)));
884
+ txt=Math.abs(away)+' room'+(Math.abs(away)===1?'':'s')+(away>0?' ahead':' behind');
885
+ }
886
+ $('resT').textContent=txt;
887
+ $('resFill').style.width=(res*100).toFixed(0)+'%';
888
+ }
889
+
890
+ /* ---------------- shader compile: DEFERRED so the boot screen paints first,
891
+ PARALLEL where the driver allows it, and any failure shows its log -------- */
892
+ let prog=null,U={};
893
+ function sh(type,src){const s=gl.createShader(type);gl.shaderSource(s,src);
894
+ gl.compileShader(s);return s;}
895
+ function compileAndStart(){
896
+ const vs=sh(gl.VERTEX_SHADER,VERT), fs=sh(gl.FRAGMENT_SHADER,FRAG);
897
+ prog=gl.createProgram();
898
+ gl.attachShader(prog,vs);gl.attachShader(prog,fs);
899
+ gl.linkProgram(prog);
900
+ const ext=gl.getExtension('KHR_parallel_shader_compile');
901
+ function finish(){
902
+ if(!gl.getProgramParameter(prog,gl.LINK_STATUS)){
903
+ const log=(gl.getShaderInfoLog(fs)||'')+' '+(gl.getShaderInfoLog(vs)||'')
904
+ +' '+(gl.getProgramInfoLog(prog)||'');
905
+ bootErr('Shader failed: '+log.trim());
906
+ return;
907
+ }
908
+ gl.useProgram(prog);
909
+ ['iRes','iTime','camP','camR','camU','camF','nWell','mode','keyWell','mGain','lensK','camTex',
910
+ 'wells','wellCol','wellPar','roomPos','roomPar','roomMod','roomTintU','seal']
911
+ .forEach(k=>U[k]=gl.getUniformLocation(prog,k));
912
+ gl.uniform1i(U.camTex,0);
913
+ $('bootStat').textContent='ready — click to enter';
914
+ requestAnimationFrame(loop);
915
+ }
916
+ if(ext){
917
+ (function poll(){
918
+ if(gl.getProgramParameter(prog,ext.COMPLETION_STATUS_KHR))finish();
919
+ else requestAnimationFrame(poll);
920
+ })();
921
+ }else finish();
922
+ }
923
+
924
+ /* ---------------- loop ---------------- */
925
+ let t0=performance.now(),simT=0,frames=0,fpsT=t0;
926
+ function loop(now){
927
+ const dt=Math.min((now-t0)/1000,0.05);t0=now;
928
+ if(idle)simT+=dt; else simT+=dt*0.06;
929
+
930
+ const cp=Math.cos(pitch),sp=Math.sin(pitch),cy=Math.cos(yaw),sy=Math.sin(yaw);
931
+ const F=[sy*cp,sp,cy*cp], Rv=[cy,0,-sy], Uv=[-sy*sp,cp,-cy*sp];
932
+ const spd=(keys.ShiftLeft||keys.ShiftRight?14:6)*dt;
933
+ let mx=0,my=0,mz=0;
934
+ if(keys.KeyW){mx+=F[0]*spd;my+=F[1]*spd;mz+=F[2]*spd;}
935
+ if(keys.KeyS){mx-=F[0]*spd;my-=F[1]*spd;mz-=F[2]*spd;}
936
+ if(keys.KeyD){mx+=Rv[0]*spd;mz+=Rv[2]*spd;}
937
+ if(keys.KeyA){mx-=Rv[0]*spd;mz-=Rv[2]*spd;}
938
+ if(keys.KeyE)my+=spd;
939
+ if(keys.KeyQ)my-=spd;
940
+ if(mx||my||mz)tryMove(camP[0]+mx,camP[1]+my,camP[2]+mz);
941
+
942
+ /* room transition: deep inside next -> dream forward; inside prev -> re-dream back */
943
+ if(roomSDJ(camP[0],camP[1],camP[2],2)<-1.0)advance();
944
+ else if(roomSDJ(camP[0],camP[1],camP[2],0)<-1.0)retreat();
945
+
946
+ uploadBoundary(simT);
947
+ const nW=packWorld(simT);
948
+
949
+ /* key pickup: touch the gold crystal */
950
+ if(!keyFound && keyPos){
951
+ const d=Math.hypot(camP[0]-keyPos[0],camP[1]-keyPos[1],camP[2]-keyPos[2]);
952
+ if(d<keyR+1.0){
953
+ keyFound=true; keyWell=-1; sealV=[0,0,0,-1];
954
+ banner('KEY CRYSTAL','the seal dissolves — find the far gate');
955
+ updateLevelHud(); autoSave();
956
+ }
957
+ }
958
+
959
+ gl.useProgram(prog);
960
+ gl.uniform2f(U.iRes,cvs.width,cvs.height);
961
+ gl.uniform1f(U.iTime,simT);
962
+ gl.uniform3f(U.camP,camP[0],camP[1],camP[2]);
963
+ gl.uniform3f(U.camR,Rv[0],Rv[1],Rv[2]);
964
+ gl.uniform3f(U.camU,Uv[0],Uv[1],Uv[2]);
965
+ gl.uniform3f(U.camF,F[0],F[1],F[2]);
966
+ gl.uniform1i(U.nWell,nW);
967
+ gl.uniform1i(U.mode,mode);
968
+ gl.uniform1i(U.keyWell,keyWell);
969
+ gl.uniform1f(U.mGain,P.mGain);
970
+ gl.uniform1f(U.lensK,P.lensK);
971
+ gl.uniform4fv(U.wells,wellsF);
972
+ gl.uniform3fv(U.wellCol,colsF);
973
+ gl.uniform4fv(U.wellPar,parF);
974
+ gl.uniform3fv(U.roomPos,roomPosF);
975
+ gl.uniform4fv(U.roomPar,roomParF);
976
+ gl.uniform4fv(U.roomMod,roomModF);
977
+ gl.uniform3fv(U.roomTintU,roomTintF);
978
+ gl.uniform4f(U.seal,sealV[0],sealV[1],sealV[2],sealV[3]);
979
+ gl.drawArrays(gl.TRIANGLES,0,3);
980
+
981
+ frames++;
982
+ if(now-fpsT>400){
983
+ $('fps').textContent=Math.round(frames*1000/(now-fpsT))+' fps';
984
+ frames=0;fpsT=now;
985
+ $('pos').textContent=camP.map(v=>v.toFixed(1)).join(', ');
986
+ updateResonance();
987
+ }
988
+ requestAnimationFrame(loop);
989
+ }
990
+
991
+ /* let the boot screen paint before the driver chews on the shader */
992
+ $('bootStat').textContent='dreaming the shader…';
993
+ requestAnimationFrame(()=>{try{compileAndStart();}catch(e){bootErr(e&&e.stack||e);}});
994
+ </script>