Spaces:
Running
Running
Update index.html
Browse files- index.html +993 -18
index.html
CHANGED
|
@@ -1,19 +1,994 @@
|
|
| 1 |
<!doctype html>
|
| 2 |
-
<
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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=>({'<':'<','>':'>','&':'&'}[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>
|