Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>ARTEMIS β Command Center</title> | |
| <style> | |
| *{margin:0;padding:0;box-sizing:border-box} | |
| html,body{width:100%;height:100%;overflow:hidden;background:#1a1a1a;font-family:'Segoe UI','Cascadia Code',system-ui,sans-serif;cursor:none} | |
| /* ββ Loading βββββββββββββββββββββββββββ */ | |
| #loading{position:fixed;inset:0;background:#1a1a1a;z-index:1000;display:flex;flex-direction:column; | |
| align-items:center;justify-content:center;gap:20px;transition:opacity .8s} | |
| #loading.done{opacity:0;pointer-events:none} | |
| #load-title{font-size:1.8rem;font-weight:300;color:#d4c5a9;letter-spacing:.3em;text-transform:uppercase} | |
| #load-sub{font-size:.85rem;color:#8a7e6a;letter-spacing:.15em} | |
| #load-bar{width:300px;height:2px;background:#2a2a2a;border-radius:1px;overflow:hidden;margin-top:8px} | |
| #load-fill{width:0%;height:100%;background:linear-gradient(90deg,#d4c5a9,#f0e6cc);transition:width .3s} | |
| /* ββ Lock Screen (click to start) ββββββ */ | |
| #lock-screen{position:fixed;inset:0;z-index:500;display:flex;flex-direction:column;align-items:center; | |
| justify-content:center;gap:16px;background:rgba(26,26,26,.85);cursor:pointer;transition:opacity .4s} | |
| #lock-screen.hidden{opacity:0;pointer-events:none} | |
| #lock-screen h2{font-size:1.2rem;font-weight:400;color:#d4c5a9;letter-spacing:.2em;text-transform:uppercase} | |
| #lock-screen p{font-size:.82rem;color:#8a7e6a;letter-spacing:.08em} | |
| /* ββ Crosshair βββββββββββββββββββββββββ */ | |
| #crosshair{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);pointer-events:none;z-index:10;opacity:0;transition:opacity .3s} | |
| #crosshair.visible{opacity:.5} | |
| #crosshair::before,#crosshair::after{content:'';position:absolute;background:#d4c5a9} | |
| #crosshair::before{width:1px;height:14px;left:50%;top:50%;transform:translate(-50%,-50%)} | |
| #crosshair::after{width:14px;height:1px;top:50%;left:50%;transform:translate(-50%,-50%)} | |
| /* ββ HUD βββββββββββββββββββββββββββββββ */ | |
| #hud{position:fixed;inset:0;pointer-events:none;z-index:10;opacity:0;transition:opacity .4s} | |
| #hud.visible{opacity:1} | |
| #hud *{pointer-events:auto} | |
| #room-label{position:absolute;top:20px;left:24px;font-size:.72rem;color:#8a7e6a;letter-spacing:.2em; | |
| text-transform:uppercase;transition:all .4s} | |
| #room-label span{color:#d4c5a9;font-weight:500} | |
| #backend-status{position:absolute;top:20px;right:24px;display:flex;align-items:center;gap:8px; | |
| font-size:.68rem;color:#8a7e6a;letter-spacing:.08em;text-transform:uppercase} | |
| #status-dot{width:8px;height:8px;border-radius:50%;background:#8a4a4a;transition:background .3s,box-shadow .3s} | |
| #status-dot.ok{background:#6a9a6a;box-shadow:0 0 8px rgba(106,154,106,.4)} | |
| #interact-prompt{position:absolute;bottom:120px;left:50%;transform:translateX(-50%); | |
| font-size:.82rem;color:#d4c5a9;letter-spacing:.1em;opacity:0;transition:opacity .3s; | |
| background:rgba(26,26,26,.7);padding:8px 20px;border-radius:3px; | |
| border:1px solid rgba(212,197,169,.15)} | |
| #interact-prompt.visible{opacity:1} | |
| #controls-hint{position:absolute;bottom:20px;left:50%;transform:translateX(-50%); | |
| display:flex;gap:24px;font-size:.65rem;color:rgba(138,126,106,.5);letter-spacing:.06em} | |
| #narrator{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%); | |
| font-size:1rem;color:#d4c5a9;letter-spacing:.06em;line-height:1.8;text-align:center; | |
| max-width:600px;opacity:0;transition:opacity .8s;pointer-events:none; | |
| text-shadow:0 0 20px rgba(26,26,26,.8)} | |
| #narrator.visible{opacity:1} | |
| /* ββ Tool Panel ββββββββββββββββββββββββ */ | |
| #tool-panel{position:fixed;top:0;right:-420px;width:420px;height:100%;z-index:100; | |
| background:rgba(32,30,26,.96);border-left:1px solid rgba(212,197,169,.1); | |
| backdrop-filter:blur(12px);transition:right .35s ease;display:flex;flex-direction:column; | |
| pointer-events:auto;cursor:default} | |
| #tool-panel.open{right:0} | |
| #tp-header{padding:20px 24px 16px;border-bottom:1px solid rgba(212,197,169,.08);display:flex; | |
| align-items:center;justify-content:space-between} | |
| #tp-title{font-size:.9rem;font-weight:600;color:#d4c5a9;letter-spacing:.08em;text-transform:uppercase} | |
| #tp-close{background:none;border:none;color:#8a7e6a;cursor:pointer;font-size:1.2rem;padding:4px 8px; | |
| font-family:inherit;transition:color .2s} | |
| #tp-close:hover{color:#c4a070} | |
| #tp-body{flex:1;overflow-y:auto;padding:20px 24px;display:flex;flex-direction:column;gap:12px} | |
| .tp-desc{font-size:.85rem;color:#a09080;line-height:1.6} | |
| .tp-section{margin-top:8px} | |
| .tp-section-label{font-size:.65rem;color:#8a7e6a;letter-spacing:.12em;text-transform:uppercase;margin-bottom:6px} | |
| .tp-tags{display:flex;flex-wrap:wrap;gap:4px} | |
| .tp-tag{font-size:.65rem;padding:3px 10px;border-radius:2px;border:1px solid rgba(212,197,169,.12); | |
| color:#a09080;background:rgba(212,197,169,.04);letter-spacing:.04em} | |
| .tp-actions{display:flex;flex-wrap:wrap;gap:8px;margin-top:12px} | |
| .tp-btn{display:inline-flex;align-items:center;gap:6px;padding:8px 16px; | |
| border:1px solid rgba(212,197,169,.15);border-radius:3px;background:rgba(212,197,169,.04); | |
| color:#a09080;font-family:inherit;font-size:.78rem;cursor:pointer; | |
| letter-spacing:.04em;transition:all .25s} | |
| .tp-btn:hover{color:#d4c5a9;border-color:rgba(212,197,169,.3);background:rgba(212,197,169,.08)} | |
| .tp-btn.launch{border-color:rgba(106,154,106,.3);color:#6a9a6a} | |
| .tp-btn.launch:hover{background:rgba(106,154,106,.1)} | |
| .tp-btn.stop{border-color:rgba(180,100,100,.3);color:#b46464} | |
| .tp-btn.stop:hover{background:rgba(180,100,100,.1)} | |
| .tp-status{display:flex;align-items:center;gap:8px;font-size:.78rem;color:#8a7e6a;margin-top:8px} | |
| .tp-status-dot{width:8px;height:8px;border-radius:50%;background:#5a5a5a} | |
| .tp-status-dot.running{background:#6a9a6a;box-shadow:0 0 6px rgba(106,154,106,.4);animation:pulse 2s infinite} | |
| @keyframes pulse{0%,100%{opacity:1}50%{opacity:.4}} | |
| /* ββ Toast ββββββββββββββββββββββββββββββ */ | |
| #toast-container{position:fixed;top:60px;right:24px;z-index:9999;display:flex;flex-direction:column;gap:8px;pointer-events:none} | |
| .toast{padding:10px 20px;border-radius:3px;font-size:.82rem;background:rgba(32,30,26,.95); | |
| border:1px solid rgba(212,197,169,.15);color:#d4c5a9;animation:toastIn .3s;pointer-events:auto} | |
| .toast.success{border-color:rgba(106,154,106,.3)} | |
| .toast.error{border-color:rgba(180,100,100,.3)} | |
| @keyframes toastIn{from{opacity:0;transform:translateX(20px)}to{opacity:1;transform:none}} | |
| /* ββ Output Modal ββββββββββββββββββββββ */ | |
| #output-modal{position:fixed;inset:0;z-index:200;display:none;align-items:center;justify-content:center; | |
| background:rgba(20,18,16,.8);backdrop-filter:blur(4px)} | |
| #output-modal.open{display:flex} | |
| #output-box{width:700px;max-height:70vh;background:rgba(32,30,26,.98);border:1px solid rgba(212,197,169,.1); | |
| border-radius:4px;display:flex;flex-direction:column} | |
| #output-header{padding:12px 20px;border-bottom:1px solid rgba(212,197,169,.08);display:flex; | |
| align-items:center;justify-content:space-between} | |
| #output-title{font-size:.82rem;color:#d4c5a9;letter-spacing:.06em;text-transform:uppercase} | |
| #output-close{background:none;border:none;color:#8a7e6a;cursor:pointer;font-size:1.1rem;font-family:inherit} | |
| #output-content{flex:1;overflow-y:auto;padding:16px 20px;font-family:'Cascadia Code',monospace; | |
| font-size:.78rem;color:#a09080;line-height:1.6;white-space:pre-wrap;word-break:break-all} | |
| /* Scrollbars */ | |
| ::-webkit-scrollbar{width:4px} | |
| ::-webkit-scrollbar-track{background:transparent} | |
| ::-webkit-scrollbar-thumb{background:rgba(212,197,169,.12);border-radius:2px} | |
| ::-webkit-scrollbar-thumb:hover{background:rgba(212,197,169,.25)} | |
| ::selection{background:rgba(212,197,169,.2)} | |
| /* βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| DATASET UPLOAD & DUAL BRAIN ANALYSIS SYSTEM | |
| ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| /* ββ Upload trigger button (fixed, bottom-left) ββββββββββββ */ | |
| #dataset-upload-btn { | |
| position:fixed; bottom:24px; left:24px; z-index:15; | |
| display:flex; align-items:center; gap:9px; | |
| padding:10px 20px; | |
| background:rgba(10,18,32,0.85); backdrop-filter:blur(10px); | |
| border:1px solid rgba(80,200,255,0.30); border-radius:3px; | |
| color:#50c8ff; font-family:inherit; font-size:.72rem; | |
| letter-spacing:.14em; text-transform:uppercase; | |
| cursor:pointer; transition:all .25s ease; | |
| text-shadow:0 0 10px rgba(80,200,255,.4); | |
| opacity:0; pointer-events:none; | |
| } | |
| #dataset-upload-btn.visible { opacity:1; pointer-events:auto; } | |
| #dataset-upload-btn:hover { | |
| border-color:rgba(80,200,255,.7); | |
| box-shadow:0 0 18px rgba(80,200,255,.2); | |
| background:rgba(15,30,55,.92); | |
| } | |
| #dataset-upload-btn .du-icon { font-size:1rem; line-height:1; } | |
| #dataset-upload-btn.analyzing { | |
| border-color:rgba(255,150,50,.6); | |
| color:#ffaa44; | |
| animation:duAnalyzing 1.6s infinite; | |
| } | |
| @keyframes duAnalyzing { | |
| 0%,100% { box-shadow:0 0 8px rgba(255,150,50,.3); } | |
| 50% { box-shadow:0 0 24px rgba(255,150,50,.7); } | |
| } | |
| /* ββ Upload drop panel βββββββββββββββββββββββββββββββββββββ */ | |
| #dataset-panel { | |
| position:fixed; inset:0; z-index:200; | |
| display:flex; align-items:center; justify-content:center; | |
| background:rgba(6,10,18,0.92); backdrop-filter:blur(18px); | |
| opacity:0; pointer-events:none; transition:opacity .3s; | |
| } | |
| #dataset-panel.open { opacity:1; pointer-events:auto; } | |
| #dataset-panel-inner { | |
| width:min(640px, 94vw); | |
| background:rgba(12,20,36,0.97); | |
| border:1px solid rgba(80,200,255,0.2); | |
| border-radius:8px; overflow:hidden; | |
| box-shadow:0 0 80px rgba(80,200,255,0.08); | |
| } | |
| #dp-header { | |
| padding:18px 22px 14px; | |
| border-bottom:1px solid rgba(80,200,255,0.08); | |
| display:flex; align-items:center; justify-content:space-between; | |
| } | |
| #dp-title { | |
| font-size:.88rem; font-weight:700; color:#50c8ff; | |
| letter-spacing:.15em; text-transform:uppercase; | |
| } | |
| #dp-close { | |
| background:none; border:none; color:#8a7e6a; | |
| cursor:pointer; font-size:1.2rem; padding:2px 8px; | |
| font-family:inherit; transition:color .2s; | |
| } | |
| #dp-close:hover { color:#ff6644; } | |
| /* Drop zone */ | |
| #dp-drop { | |
| margin:20px; border:2px dashed rgba(80,200,255,0.25); | |
| border-radius:6px; padding:36px 20px; text-align:center; | |
| cursor:pointer; transition:all .2s; | |
| position:relative; overflow:hidden; | |
| } | |
| #dp-drop:hover, #dp-drop.dragover { | |
| border-color:rgba(80,200,255,0.7); | |
| background:rgba(80,200,255,0.04); | |
| } | |
| #dp-drop .dp-drop-icon { font-size:2.8rem; margin-bottom:10px; opacity:0.6; } | |
| #dp-drop .dp-drop-label { | |
| font-size:.88rem; color:#d4c5a9; letter-spacing:.04em; | |
| margin-bottom:6px; | |
| } | |
| #dp-drop .dp-drop-sub { | |
| font-size:.72rem; color:#8a7e6a; letter-spacing:.06em; | |
| } | |
| #dp-file-input { display:none; } | |
| /* Format badges */ | |
| #dp-formats { | |
| display:flex; justify-content:center; gap:8px; margin:0 20px 16px; flex-wrap:wrap; | |
| } | |
| .dp-fmt { | |
| font-size:.62rem; padding:3px 10px; border-radius:2px; | |
| border:1px solid rgba(212,197,169,.14); color:#a09080; | |
| letter-spacing:.08em; text-transform:uppercase; | |
| } | |
| /* Analyze button */ | |
| #dp-analyze-btn { | |
| display:block; width:calc(100% - 40px); margin:0 20px 20px; | |
| padding:12px; border:1px solid rgba(80,200,255,0.4); | |
| background:rgba(80,200,255,0.08); border-radius:4px; | |
| color:#50c8ff; font-family:inherit; font-size:.82rem; | |
| letter-spacing:.15em; text-transform:uppercase; | |
| cursor:pointer; transition:all .2s; | |
| opacity:0.4; pointer-events:none; | |
| } | |
| #dp-analyze-btn.ready { opacity:1; pointer-events:auto; } | |
| #dp-analyze-btn.ready:hover { | |
| background:rgba(80,200,255,0.18); | |
| box-shadow:0 0 20px rgba(80,200,255,0.2); | |
| } | |
| #dp-file-name { | |
| text-align:center; font-size:.72rem; color:#8a7e6a; | |
| margin:-10px 20px 14px; letter-spacing:.04em; min-height:18px; | |
| } | |
| /* ββ DUAL BRAIN ANALYSIS HUD ββββββββββββββββββββββββββββ */ | |
| #brain-analysis-hud { | |
| position:fixed; inset:0; z-index:11; pointer-events:none; | |
| display:flex; flex-direction:column; | |
| opacity:0; transition:opacity .4s; | |
| } | |
| #brain-analysis-hud.visible { opacity:1; } | |
| /* Top bar β phase + progress */ | |
| #bah-top { | |
| position:absolute; top:0; left:0; right:0; | |
| height:3px; | |
| background:rgba(255,255,255,0.05); | |
| } | |
| #bah-progress { | |
| height:100%; width:0%; transition:width .15s; | |
| background:linear-gradient(90deg, #50c8ff, #ff88ff, #ffaa44); | |
| } | |
| #bah-phase-label { | |
| position:absolute; top:8px; left:50%; transform:translateX(-50%); | |
| font-size:.62rem; color:#d4c5a9; letter-spacing:.25em; | |
| text-transform:uppercase; white-space:nowrap; | |
| text-shadow:0 0 10px rgba(26,26,26,.9); | |
| background:rgba(10,14,22,0.6); padding:3px 14px; border-radius:2px; | |
| border:1px solid rgba(212,197,169,0.08); | |
| } | |
| /* Human brain side panel (left) */ | |
| #bah-human { | |
| position:absolute; top:32px; left:14px; width:240px; | |
| background:rgba(8,12,20,0.82); border:1px solid rgba(255,140,50,0.25); | |
| border-radius:6px; padding:12px 14px; | |
| backdrop-filter:blur(8px); | |
| } | |
| #bah-human-title { | |
| font-size:.62rem; color:#ffaa44; letter-spacing:.2em; | |
| text-transform:uppercase; margin-bottom:10px; display:flex; | |
| align-items:center; gap:6px; | |
| } | |
| #bah-human-title::before { | |
| content:''; width:7px; height:7px; border-radius:50%; | |
| background:#ffaa44; box-shadow:0 0 8px #ffaa44; | |
| display:inline-block; flex-shrink:0; | |
| animation:humanPulse 1.5s infinite; | |
| } | |
| @keyframes humanPulse { | |
| 0%,100%{opacity:.7;transform:scale(1)} | |
| 50%{opacity:1;transform:scale(1.3)} | |
| } | |
| /* AI brain side panel (right) */ | |
| #bah-ai { | |
| position:absolute; top:32px; right:14px; width:240px; | |
| background:rgba(8,12,20,0.82); border:1px solid rgba(80,200,255,0.25); | |
| border-radius:6px; padding:12px 14px; | |
| backdrop-filter:blur(8px); | |
| } | |
| #bah-ai-title { | |
| font-size:.62rem; color:#50c8ff; letter-spacing:.2em; | |
| text-transform:uppercase; margin-bottom:10px; display:flex; | |
| align-items:center; gap:6px; | |
| } | |
| #bah-ai-title::before { | |
| content:''; width:7px; height:7px; border-radius:50%; | |
| background:#50c8ff; box-shadow:0 0 8px #50c8ff; | |
| display:inline-block; flex-shrink:0; | |
| animation:aiPulse 1.2s infinite; | |
| } | |
| @keyframes aiPulse { | |
| 0%,100%{opacity:.8;transform:scale(1)} | |
| 50%{opacity:1;transform:scale(1.25)} | |
| } | |
| /* Lobe bar row */ | |
| .bah-lobe-row { | |
| display:flex; align-items:center; gap:6px; | |
| margin-bottom:5px; | |
| } | |
| .bah-lobe-name { | |
| font-size:.58rem; color:#8a7e6a; letter-spacing:.06em; | |
| text-transform:uppercase; width:68px; flex-shrink:0; | |
| } | |
| .bah-lobe-bar-wrap { | |
| flex:1; height:5px; background:rgba(255,255,255,0.05); | |
| border-radius:2px; overflow:hidden; | |
| } | |
| .bah-lobe-bar { | |
| height:100%; width:0%; border-radius:2px; | |
| transition:width .12s ease; | |
| } | |
| .bah-lobe-val { | |
| font-size:.58rem; color:#8a7e6a; width:26px; text-align:right; | |
| letter-spacing:.03em; | |
| } | |
| /* AI layer stack */ | |
| #bah-ai-layers { | |
| display:flex; gap:2px; height:40px; align-items:flex-end; | |
| margin-bottom:8px; | |
| } | |
| .bah-layer-bar { | |
| flex:1; background:rgba(80,200,255,0.3); | |
| border-radius:1px; min-height:2px; | |
| transition:height .1s ease; | |
| } | |
| /* Thought label */ | |
| .bah-thought { | |
| font-size:.65rem; color:#d4c5a9; letter-spacing:.03em; | |
| line-height:1.4; min-height:28px; | |
| border-top:1px solid rgba(255,255,255,0.05); | |
| padding-top:7px; margin-top:7px; | |
| font-style:italic; opacity:0.85; | |
| } | |
| /* Firing pattern badge */ | |
| .bah-pattern { | |
| display:inline-block; font-size:.55rem; padding:2px 7px; | |
| border-radius:2px; letter-spacing:.08em; text-transform:uppercase; | |
| margin-top:5px; font-style:normal; | |
| } | |
| .bah-pattern.burst { background:rgba(255,100,50,.2); color:#ff7744; border:1px solid rgba(255,100,50,.3); } | |
| .bah-pattern.gamma { background:rgba(200,80,255,.2); color:#cc88ff; border:1px solid rgba(200,80,255,.3); } | |
| .bah-pattern.theta { background:rgba(80,200,255,.2); color:#50c8ff; border:1px solid rgba(80,200,255,.3); } | |
| .bah-pattern.tonic { background:rgba(80,200,80,.2); color:#88cc88; border:1px solid rgba(80,200,80,.3); } | |
| .bah-pattern.synchronized { background:rgba(255,220,80,.2); color:#ffdd88; border:1px solid rgba(255,220,80,.3); } | |
| /* Bottom center β comparison / sync bar */ | |
| #bah-compare { | |
| position:absolute; bottom:90px; left:50%; transform:translateX(-50%); | |
| width:min(480px, 94vw); | |
| background:rgba(8,12,20,0.85); border:1px solid rgba(212,197,169,0.12); | |
| border-radius:6px; padding:12px 16px; backdrop-filter:blur(8px); | |
| } | |
| #bah-compare-title { | |
| font-size:.58rem; color:#8a7e6a; letter-spacing:.2em; | |
| text-transform:uppercase; text-align:center; margin-bottom:10px; | |
| } | |
| #bah-sync-row { | |
| display:flex; align-items:center; gap:10px; margin-bottom:8px; | |
| } | |
| #bah-sync-label { font-size:.58rem; color:#8a7e6a; width:80px; } | |
| #bah-sync-track { | |
| flex:1; height:6px; background:rgba(255,255,255,0.05); border-radius:3px; | |
| position:relative; overflow:hidden; | |
| } | |
| #bah-sync-fill { | |
| height:100%; width:50%; border-radius:3px; | |
| background:linear-gradient(90deg,#ff7744,#ffdd88,#88ffcc); | |
| transition:width .15s; | |
| } | |
| #bah-sync-val { font-size:.65rem; color:#d4c5a9; width:36px; text-align:right; } | |
| #bah-insight { | |
| text-align:center; font-size:.65rem; color:#ffdd88; | |
| letter-spacing:.1em; min-height:18px; | |
| text-shadow:0 0 12px rgba(255,220,80,.6); | |
| transition:opacity .3s; | |
| } | |
| #bah-diverge-row { | |
| display:flex; gap:6px; flex-wrap:wrap; justify-content:center; | |
| margin-top:6px; | |
| } | |
| .bah-divtag { | |
| font-size:.55rem; padding:2px 8px; border-radius:2px; | |
| letter-spacing:.06em; text-transform:uppercase; | |
| } | |
| .bah-divtag.diverge { background:rgba(255,80,80,.15); color:#ff6666; border:1px solid rgba(255,80,80,.2); } | |
| .bah-divtag.converge { background:rgba(80,255,150,.15); color:#66ff99; border:1px solid rgba(80,255,150,.2); } | |
| /* Dataset insight ribbon */ | |
| #bah-finding { | |
| position:absolute; bottom:54px; left:50%; transform:translateX(-50%); | |
| white-space:nowrap; font-size:.65rem; color:#d4c5a9; | |
| letter-spacing:.05em; background:rgba(8,12,20,0.75); | |
| padding:5px 16px; border-radius:2px; | |
| border:1px solid rgba(212,197,169,0.08); | |
| max-width:94vw; overflow:hidden; text-overflow:ellipsis; | |
| transition:opacity .3s; | |
| } | |
| /* ββ Analysis complete summary modal βββββββββββββββββββ */ | |
| #analysis-summary-modal { | |
| position:fixed; inset:0; z-index:250; | |
| display:flex; align-items:center; justify-content:center; | |
| background:rgba(6,10,18,0.94); backdrop-filter:blur(16px); | |
| opacity:0; pointer-events:none; transition:opacity .3s; | |
| } | |
| #analysis-summary-modal.open { opacity:1; pointer-events:auto; } | |
| #asm-inner { | |
| width:min(580px, 94vw); | |
| background:rgba(10,16,28,0.98); | |
| border:1px solid rgba(80,200,255,0.25); | |
| border-radius:8px; padding:28px; | |
| box-shadow:0 0 60px rgba(80,200,255,0.1); | |
| max-height:90vh; overflow-y:auto; | |
| } | |
| #asm-title { | |
| font-size:1rem; font-weight:700; color:#50c8ff; | |
| letter-spacing:.2em; text-transform:uppercase; margin-bottom:4px; | |
| } | |
| #asm-sub { | |
| font-size:.7rem; color:#8a7e6a; letter-spacing:.08em; | |
| margin-bottom:20px; | |
| } | |
| .asm-section-label { | |
| font-size:.6rem; color:#8a7e6a; letter-spacing:.18em; | |
| text-transform:uppercase; margin-bottom:8px; margin-top:16px; | |
| } | |
| .asm-row { | |
| display:flex; justify-content:space-between; align-items:baseline; | |
| padding:5px 0; border-bottom:1px solid rgba(255,255,255,0.04); | |
| font-size:.76rem; | |
| } | |
| .asm-key { color:#a09080; letter-spacing:.03em; } | |
| .asm-val { color:#d4c5a9; font-weight:600; letter-spacing:.04em; } | |
| .asm-finding-item { | |
| font-size:.74rem; color:#c0b090; line-height:1.6; | |
| padding:3px 0; border-bottom:1px solid rgba(255,255,255,0.04); | |
| } | |
| .asm-finding-item::before { content:'β '; color:#50c8ff; } | |
| #asm-recommendation { | |
| margin-top:14px; padding:12px 14px; | |
| background:rgba(80,200,255,0.06); | |
| border:1px solid rgba(80,200,255,0.15); border-radius:4px; | |
| font-size:.76rem; color:#c0d8f0; line-height:1.6; letter-spacing:.02em; | |
| } | |
| #asm-actions { | |
| display:flex; gap:10px; margin-top:18px; justify-content:flex-end; | |
| } | |
| .asm-btn { | |
| padding:8px 20px; border-radius:3px; font-family:inherit; | |
| font-size:.72rem; letter-spacing:.12em; text-transform:uppercase; | |
| cursor:pointer; transition:all .2s; | |
| } | |
| .asm-btn.secondary { | |
| background:none; border:1px solid rgba(212,197,169,.2); | |
| color:#8a7e6a; | |
| } | |
| .asm-btn.secondary:hover { border-color:rgba(212,197,169,.5); color:#d4c5a9; } | |
| .asm-btn.primary { | |
| background:rgba(80,200,255,0.12); border:1px solid rgba(80,200,255,.4); | |
| color:#50c8ff; | |
| } | |
| .asm-btn.primary:hover { | |
| background:rgba(80,200,255,0.25); | |
| box-shadow:0 0 14px rgba(80,200,255,0.2); | |
| } | |
| /* Insight flash overlay (full-screen brief) */ | |
| #bah-insight-flash { | |
| position:fixed; inset:0; z-index:300; pointer-events:none; | |
| background:radial-gradient(ellipse at center, rgba(255,240,100,0.18) 0%, transparent 65%); | |
| opacity:0; transition:opacity .1s; | |
| } | |
| #bah-insight-flash.flash { opacity:1; } | |
| /* ββ Neural-Sim Live Status Badge βββββββββββββββββββ */ | |
| #neural-live-badge{ | |
| position:fixed;bottom:80px;left:24px;z-index:15; | |
| padding:6px 14px;border-radius:3px; | |
| background:rgba(10,20,40,0.85); | |
| border:1px solid rgba(80,200,255,0.3); | |
| color:#50c8ff;font-size:.68rem;letter-spacing:.08em; | |
| text-transform:uppercase;display:none; | |
| animation:neuralPulse 2s infinite; | |
| pointer-events:none; | |
| } | |
| #neural-live-badge.visible{display:block} | |
| @keyframes neuralPulse{ | |
| 0%,100%{box-shadow:0 0 6px rgba(80,200,255,.3)} | |
| 50%{box-shadow:0 0 18px rgba(80,200,255,.7)} | |
| } | |
| /* ββ Game Menu βββββββββββββββββββββ */ | |
| #game-menu{position:fixed;inset:8% 20% 8% 20%;z-index:1500;background:rgba(18,18,18,.98);border:1px solid rgba(212,197,169,.06);color:#d4c5a9;display:none;flex-direction:column;border-radius:8px;overflow:hidden} | |
| #game-menu.open{display:flex} | |
| #game-menu .gm-header{display:flex;align-items:center;justify-content:space-between;padding:12px 16px;border-bottom:1px solid rgba(212,197,169,.04);font-weight:600} | |
| #game-menu .gm-body{display:flex;gap:12px;padding:12px 16px;flex:1;overflow:auto} | |
| .gm-column{flex:1;min-width:220px} | |
| .gm-section{margin-bottom:12px} | |
| .gm-label{font-size:.78rem;color:#a09080;margin-bottom:6px;display:block;text-transform:uppercase;letter-spacing:.06em} | |
| .gm-control{display:flex;align-items:center;gap:8px;margin-bottom:8px} | |
| .gm-slider{width:100%} | |
| .gm-actions{display:flex;gap:8px;padding:12px;border-top:1px solid rgba(212,197,169,.04);justify-content:flex-end} | |
| .gm-keybinds{display:flex;flex-direction:column;gap:6px} | |
| .gm-keybind-row{display:flex;align-items:center;justify-content:space-between;gap:8px;font-size:.82rem;color:#d4c5a9} | |
| .gm-keybind-row .gm-key-btn{min-width:80px;text-align:center;padding:4px 10px;background:rgba(212,197,169,.06);border:1px solid rgba(212,197,169,.15);border-radius:3px;color:#d4c5a9;cursor:pointer;font-family:'Cascadia Code',monospace;font-size:.75rem} | |
| .gm-keybind-row .gm-key-btn.listening{background:rgba(180,140,60,.18);border-color:rgba(212,170,80,.5);color:#f0d090} | |
| /* ββ Cinematic Transition Overlays βββ */ | |
| #fx-static{position:fixed;inset:0;z-index:1200;pointer-events:none;opacity:0;mix-blend-mode:screen; | |
| background-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='240' height='240'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.92' numOctaves='2' stitchTiles='stitch'/><feColorMatrix values='0 0 0 0 0.85 0 0 0 0 0.78 0 0 0 0 0.65 0 0 0 0.45 0'/></filter><rect width='100%' height='100%' filter='url(%23n)'/></svg>"); | |
| background-size:240px 240px;animation:fxStaticShift 240ms steps(6) infinite;transition:opacity .25s linear} | |
| @keyframes fxStaticShift{0%{background-position:0 0}25%{background-position:-30px 12px}50%{background-position:18px -22px}75%{background-position:-8px 30px}100%{background-position:24px 6px}} | |
| #fx-supernova{position:fixed;inset:0;z-index:1210;pointer-events:none;opacity:0;mix-blend-mode:screen; | |
| background:radial-gradient(circle at 50% 30%, rgba(255,250,220,1) 0%, rgba(255,210,120,.8) 12%, rgba(255,170,60,.4) 28%, rgba(120,60,20,.1) 50%, transparent 70%); | |
| transition:opacity .15s linear} | |
| #fx-nebula{position:fixed;inset:0;z-index:1190;pointer-events:none;opacity:0;mix-blend-mode:screen;transition:opacity .8s ease-out; | |
| background: | |
| radial-gradient(ellipse 60% 50% at 8% 12%, rgba(190, 90,255,.55), transparent 60%), | |
| radial-gradient(ellipse 50% 60% at 96% 18%, rgba( 80,200,255,.55), transparent 60%), | |
| radial-gradient(ellipse 65% 55% at 4% 92%, rgba(255, 80,200,.45), transparent 60%), | |
| radial-gradient(ellipse 55% 60% at 98% 88%, rgba(120, 80,255,.5), transparent 60%)} | |
| #fx-vignette{position:fixed;inset:0;z-index:1195;pointer-events:none;opacity:0; | |
| background:radial-gradient(ellipse at center, transparent 30%, rgba(0,0,0,.6) 75%, rgba(0,0,0,.95) 100%); | |
| transition:opacity .6s ease-out} | |
| #fx-shake{position:fixed;inset:0;z-index:1100;pointer-events:none} | |
| .fx-shudder{animation:fxShudder 380ms cubic-bezier(.36,.07,.19,.97) both} | |
| @keyframes fxShudder{ | |
| 10%,90%{transform:translate3d(-1px,0,0)} | |
| 20%,80%{transform:translate3d(2px,0,0)} | |
| 30%,50%,70%{transform:translate3d(-4px,1px,0)} | |
| 40%,60%{transform:translate3d(4px,-1px,0)} | |
| } | |
| /* Space-dust shimmer (crystalline shards in periphery) */ | |
| #fx-shimmer{position:fixed;inset:0;z-index:1188;pointer-events:none;opacity:0;mix-blend-mode:screen;transition:opacity 1.6s ease-out; | |
| background-image: | |
| radial-gradient(circle at 12% 18%, rgba(220,235,255,.9) 0, transparent 1.4px), | |
| radial-gradient(circle at 88% 22%, rgba(200,220,255,.85) 0, transparent 1.2px), | |
| radial-gradient(circle at 8% 78%, rgba(240,245,255,.9) 0, transparent 1.6px), | |
| radial-gradient(circle at 92% 84%, rgba(220,230,255,.85) 0, transparent 1.4px), | |
| radial-gradient(circle at 28% 92%, rgba(255,255,255,.7) 0, transparent 1.2px), | |
| radial-gradient(circle at 72% 8%, rgba(255,255,255,.7) 0, transparent 1.2px); | |
| background-size: 240px 240px; | |
| animation:fxShimmer 3.2s ease-in-out infinite alternate} | |
| @keyframes fxShimmer{ | |
| 0% {filter:brightness(1) blur(0)} | |
| 50% {filter:brightness(1.6) blur(.4px)} | |
| 100%{filter:brightness(1) blur(0)} | |
| } | |
| /* Galaxy button detonation */ | |
| #galaxy-btn.detonating{animation:fxBtnDetonate 480ms ease-out forwards;border-color:rgba(255,210,120,.9); | |
| box-shadow:0 0 60px rgba(255,200,100,.7),0 0 120px rgba(255,160,60,.4)} | |
| @keyframes fxBtnDetonate{ | |
| 0%{transform:scale(1)} | |
| 20%{transform:scale(1.18);background:rgba(80,60,30,.9)} | |
| 60%{transform:scale(.92)} | |
| 100%{transform:scale(1.06);opacity:0} | |
| } | |
| #galaxy-btn .g-dot.detonate{animation:fxDotDetonate 480ms ease-out forwards} | |
| @keyframes fxDotDetonate{ | |
| 0%{transform:scale(1);box-shadow:0 0 10px rgba(240,216,144,.7)} | |
| 40%{transform:scale(4);box-shadow:0 0 80px rgba(255,220,140,1),0 0 160px rgba(255,180,80,.6)} | |
| 100%{transform:scale(8);opacity:0;box-shadow:0 0 200px rgba(255,255,255,.0)} | |
| } | |
| /* ββ Galaxy Mode Button ββββββββββββββββ */ | |
| #galaxy-btn{position:fixed;bottom:60px;right:24px;z-index:15; | |
| padding:12px 22px;display:flex;align-items:center;gap:10px; | |
| background:rgba(26,26,26,.72);backdrop-filter:blur(10px); | |
| border:1px solid rgba(212,197,169,.22);border-radius:3px; | |
| color:#d4c5a9;font-family:inherit;font-size:.78rem;letter-spacing:.15em; | |
| text-transform:uppercase;cursor:pointer;opacity:0;pointer-events:none; | |
| transition:all .3s ease;text-shadow:0 0 12px rgba(212,197,169,.3)} | |
| #galaxy-btn.visible{opacity:1;pointer-events:auto} | |
| #galaxy-btn:hover{border-color:rgba(212,197,169,.55);background:rgba(40,36,30,.85); | |
| box-shadow:0 0 24px rgba(212,197,169,.12)} | |
| #galaxy-btn .g-dot{width:8px;height:8px;border-radius:50%; | |
| background:radial-gradient(circle,#f0d890 0%,#8a6a2a 70%); | |
| box-shadow:0 0 10px rgba(240,216,144,.7);animation:galaxy-pulse 2.4s infinite} | |
| @keyframes galaxy-pulse{0%,100%{opacity:.8;transform:scale(1)}50%{opacity:1;transform:scale(1.15)}} | |
| /* ββ Galaxy HUD (visible in galaxy mode) ββ */ | |
| #galaxy-hud{position:fixed;inset:0;pointer-events:none;z-index:12; | |
| opacity:0;transition:opacity .6s ease} | |
| #galaxy-hud.visible{opacity:1} | |
| #galaxy-title{position:absolute;top:24px;left:50%;transform:translateX(-50%); | |
| font-size:.9rem;color:#d4c5a9;letter-spacing:.3em;text-transform:uppercase; | |
| text-shadow:0 0 20px rgba(26,26,26,.8)} | |
| #galaxy-subtitle{position:absolute;top:50px;left:50%;transform:translateX(-50%); | |
| font-size:.68rem;color:#8a7e6a;letter-spacing:.2em;text-transform:uppercase} | |
| #galaxy-exit-hint{position:absolute;bottom:24px;left:50%;transform:translateX(-50%); | |
| font-size:.75rem;color:#d4c5a9;letter-spacing:.15em;text-transform:uppercase; | |
| background:rgba(26,26,26,.6);padding:8px 18px;border-radius:3px; | |
| border:1px solid rgba(212,197,169,.15);text-shadow:0 0 10px rgba(26,26,26,.6)} | |
| #galaxy-controls-hint{position:absolute;bottom:56px;left:50%;transform:translateX(-50%); | |
| font-size:.62rem;color:rgba(138,126,106,.55);letter-spacing:.1em; | |
| display:flex;gap:20px} | |
| /* ββ Galaxy tooltip (on hover) βββββββββββ */ | |
| #galaxy-tooltip{position:fixed;z-index:80;pointer-events:none; | |
| background:rgba(32,30,26,.92);backdrop-filter:blur(10px); | |
| border:1px solid rgba(212,197,169,.2);border-radius:3px; | |
| padding:10px 14px;max-width:360px;font-size:.78rem; | |
| opacity:0;transition:opacity .15s;color:#d4c5a9} | |
| #galaxy-tooltip.visible{opacity:1} | |
| #galaxy-tooltip .gt-name{font-weight:600;color:#f0e6cc;margin-bottom:2px; | |
| word-break:break-all} | |
| #galaxy-tooltip .gt-path{color:#a09080;font-size:.7rem;margin-bottom:4px; | |
| word-break:break-all} | |
| #galaxy-tooltip .gt-meta{color:#8a7e6a;font-size:.68rem;letter-spacing:.04em} | |
| /* ββ Galaxy loading overlay βββββββββββββββ */ | |
| #galaxy-loading{position:fixed;inset:0;z-index:300;display:none; | |
| flex-direction:column;align-items:center;justify-content:center;gap:14px; | |
| background:rgba(8,8,12,.94)} | |
| #galaxy-loading.visible{display:flex} | |
| #galaxy-loading h3{font-size:1.1rem;color:#d4c5a9;letter-spacing:.25em; | |
| text-transform:uppercase;font-weight:300} | |
| #galaxy-loading p{font-size:.72rem;color:#8a7e6a;letter-spacing:.1em} | |
| #galaxy-loading .gl-bar{width:280px;height:2px;background:rgba(212,197,169,.1); | |
| overflow:hidden} | |
| #galaxy-loading .gl-fill{height:100%;width:0%; | |
| background:linear-gradient(90deg,#d4c5a9,#f0e6cc);transition:width .25s} | |
| /* ββ Galaxy Sidebar (filters, search, sliders) ββ */ | |
| #galaxy-sidebar{position:fixed;top:0;left:0;height:100%;width:min(340px,92vw); | |
| z-index:13;background:rgba(26,24,20,.82);backdrop-filter:blur(14px); | |
| border-right:1px solid rgba(212,197,169,.12); | |
| transform:translateX(-100%);transition:transform .4s ease; | |
| display:flex;flex-direction:column;padding:20px 22px;overflow-y:auto; | |
| pointer-events:auto;cursor:default} | |
| #galaxy-sidebar.open{transform:translateX(0)} | |
| #galaxy-sidebar h3{font-size:.95rem;color:#d4c5a9;letter-spacing:.22em; | |
| text-transform:uppercase;font-weight:400;margin-bottom:4px} | |
| #galaxy-sidebar .gs-sub{font-size:.68rem;color:#8a7e6a;letter-spacing:.1em; | |
| margin-bottom:18px} | |
| #galaxy-sidebar .gs-section{font-size:.62rem;color:#8a7e6a;letter-spacing:.2em; | |
| text-transform:uppercase;margin:16px 0 8px;font-weight:500} | |
| #galaxy-sidebar-toggle{position:fixed;top:20px;left:20px;z-index:14; | |
| width:36px;height:36px;display:flex;align-items:center;justify-content:center; | |
| background:rgba(26,24,20,.72);backdrop-filter:blur(10px); | |
| border:1px solid rgba(212,197,169,.18);border-radius:3px; | |
| color:#d4c5a9;font-size:1.1rem;cursor:pointer; | |
| opacity:0;pointer-events:none;transition:all .3s ease;font-family:inherit} | |
| #galaxy-sidebar-toggle.visible{opacity:1;pointer-events:auto} | |
| #galaxy-sidebar-toggle:hover{border-color:rgba(212,197,169,.45); | |
| background:rgba(40,36,30,.85)} | |
| #galaxy-sidebar-toggle.shifted{left:calc(min(340px,92vw) + 12px)} | |
| /* Source toggles */ | |
| .gs-src-row{display:flex;align-items:center;gap:10px;padding:6px 0; | |
| cursor:pointer;font-size:.78rem;color:#d4c5a9} | |
| .gs-src-row input{width:14px;height:14px;accent-color:#c4a070;cursor:pointer} | |
| .gs-src-dot{width:10px;height:10px;border-radius:50%; | |
| box-shadow:0 0 6px currentColor;flex-shrink:0} | |
| .gs-src-label{flex:1;color:#d4c5a9} | |
| .gs-src-count{color:#8a7e6a;font-size:.68rem;letter-spacing:.04em} | |
| /* Sliders */ | |
| .gs-slider-row{display:flex;align-items:center;gap:10px;margin-bottom:4px} | |
| .gs-slider-row input[type=range]{flex:1;accent-color:#c4a070;cursor:pointer} | |
| .gs-slider-val{font-size:.72rem;color:#a09080;min-width:44px;text-align:right; | |
| letter-spacing:.04em} | |
| /* Search */ | |
| #galaxy-search{width:100%;padding:9px 12px; | |
| background:rgba(212,197,169,.04);border:1px solid rgba(212,197,169,.15); | |
| border-radius:3px;color:#d4c5a9;font-family:inherit;font-size:.82rem; | |
| outline:none;transition:border-color .2s;letter-spacing:.02em} | |
| #galaxy-search:focus{border-color:rgba(212,197,169,.5)} | |
| #galaxy-search::placeholder{color:rgba(138,126,106,.6)} | |
| #galaxy-search-results{margin-top:8px;max-height:40vh;overflow-y:auto} | |
| .gs-result{padding:7px 10px;border-radius:2px;cursor:pointer; | |
| font-size:.76rem;color:#d4c5a9;display:flex;align-items:center;gap:8px; | |
| transition:background .15s} | |
| .gs-result:hover{background:rgba(212,197,169,.08)} | |
| .gs-result .gs-res-dot{width:7px;height:7px;border-radius:50%;flex-shrink:0; | |
| box-shadow:0 0 5px currentColor} | |
| .gs-result .gs-res-name{flex:1;white-space:nowrap;overflow:hidden; | |
| text-overflow:ellipsis;color:#d4c5a9} | |
| .gs-result .gs-res-ext{color:#8a7e6a;font-size:.68rem;flex-shrink:0} | |
| .gs-result-more{padding:8px;color:#8a7e6a;font-size:.7rem; | |
| letter-spacing:.04em;text-align:center} | |
| </style> | |
| </head> | |
| <body> | |
| <!-- Loading --> | |
| <div id="loading"> | |
| <div id="load-title">ARTEMIS</div> | |
| <div id="load-sub">Initializing Environment</div> | |
| <div id="load-bar"><div id="load-fill"></div></div> | |
| </div> | |
| <!-- Lock screen (click to start FPS) --> | |
| <div id="lock-screen" class="hidden"> | |
| <h2>Click to Enter</h2> | |
| <p>WASD to move · Mouse to look · E to interact · ESC to release</p> | |
| </div> | |
| <!-- Crosshair --> | |
| <div id="crosshair"></div> | |
| <!-- HUD --> | |
| <div id="hud"> | |
| <div id="room-label">LOCATION <span id="room-name">Central Hub</span></div> | |
| <div id="backend-status">BACKEND <div id="status-dot"></div></div> | |
| <div id="interact-prompt">Press E to interact</div> | |
| <div id="controls-hint"> | |
| <span>WASD Move</span><span>Mouse Look</span><span>E Interact</span><span>F Galaxy</span><span>ESC Menu</span><span>SHIFT Sprint</span> | |
| </div> | |
| <div id="narrator"></div> | |
| </div> | |
| <!-- Pre-recorded voice narration --> | |
| <audio id="voice-built-tools" preload="auto" src="/assets/The%20Workshop.wav"></audio> | |
| <audio id="voice-central-hub" preload="auto" src="/assets/The%20Center%20Narration.wav"></audio> | |
| <!-- Tool Panel --> | |
| <div id="tool-panel"> | |
| <div id="tp-header"> | |
| <span id="tp-title">β</span> | |
| <button id="tp-close" onclick="closeToolPanel()">β</button> | |
| </div> | |
| <div id="tp-body"></div> | |
| </div> | |
| <!-- Output Modal --> | |
| <div id="output-modal"> | |
| <div id="output-box"> | |
| <div id="output-header"> | |
| <span id="output-title">Output</span> | |
| <button id="output-close" onclick="closeOutputModal()">β</button> | |
| </div> | |
| <div id="output-content"></div> | |
| </div> | |
| </div> | |
| <!-- Toast --> | |
| <div id="toast-container"></div> | |
| <!-- ββββββββββ DATASET UPLOAD TRIGGER ββββββββββ --> | |
| <button id="dataset-upload-btn" onclick="openDatasetPanel()"> | |
| <span class="du-icon">β¬</span> | |
| <span id="du-btn-label">Upload Dataset</span> | |
| </button> | |
| <!-- ββββββββββ DATASET UPLOAD PANEL βββββββββββ --> | |
| <div id="dataset-panel"> | |
| <div id="dataset-panel-inner"> | |
| <div id="dp-header"> | |
| <span id="dp-title">β Dataset Analysis</span> | |
| <button id="dp-close" onclick="closeDatasetPanel()">β</button> | |
| </div> | |
| <div id="dp-drop" | |
| onclick="document.getElementById('dp-file-input').click()" | |
| ondragover="dpDragOver(event)" | |
| ondragleave="dpDragLeave(event)" | |
| ondrop="dpDrop(event)"> | |
| <div class="dp-drop-icon">π§ </div> | |
| <div class="dp-drop-label">Drop your dataset here, or click to browse</div> | |
| <div class="dp-drop-sub">The human brain and AI will analyze it together in real time</div> | |
| </div> | |
| <input type="file" id="dp-file-input" | |
| accept=".csv,.json,.txt,.md,.tsv,.jsonl,.log,.xml,.yaml,.yml" | |
| onchange="dpFileSelected(event)"> | |
| <div id="dp-formats"> | |
| <span class="dp-fmt">CSV</span> | |
| <span class="dp-fmt">JSON</span> | |
| <span class="dp-fmt">TXT</span> | |
| <span class="dp-fmt">JSONL</span> | |
| <span class="dp-fmt">TSV</span> | |
| <span class="dp-fmt">XML</span> | |
| <span class="dp-fmt">YAML</span> | |
| <span class="dp-fmt">LOG</span> | |
| </div> | |
| <div id="dp-file-name"></div> | |
| <button id="dp-analyze-btn" onclick="dpStartAnalysis()"> | |
| β Begin Neural Analysis | |
| </button> | |
| </div> | |
| </div> | |
| <!-- ββββββββββ DUAL BRAIN ANALYSIS HUD ββββββββ --> | |
| <div id="brain-analysis-hud"> | |
| <div id="bah-top"><div id="bah-progress"></div></div> | |
| <div id="bah-phase-label">⬀ IDLE</div> | |
| <!-- Human brain panel --> | |
| <div id="bah-human"> | |
| <div id="bah-human-title">β Human Mind</div> | |
| <div id="bah-human-lobes"></div> | |
| <div class="bah-thought" id="bah-human-thought">Awaiting dataset...</div> | |
| <span class="bah-pattern tonic" id="bah-human-pattern">Tonic</span> | |
| </div> | |
| <!-- AI brain panel --> | |
| <div id="bah-ai"> | |
| <div id="bah-ai-title">β AI Mind</div> | |
| <div id="bah-ai-layers"></div> | |
| <div id="bah-ai-lobes"></div> | |
| <div class="bah-thought" id="bah-ai-thought">Awaiting dataset...</div> | |
| <span class="bah-pattern tonic" id="bah-ai-pattern">Tonic</span> | |
| </div> | |
| <!-- Comparison bar --> | |
| <div id="bah-compare"> | |
| <div id="bah-compare-title">β Synchronization</div> | |
| <div id="bah-sync-row"> | |
| <span id="bah-sync-label">Sync Score</span> | |
| <div id="bah-sync-track"><div id="bah-sync-fill"></div></div> | |
| <span id="bah-sync-val">β</span> | |
| </div> | |
| <div id="bah-insight"></div> | |
| <div id="bah-diverge-row"></div> | |
| </div> | |
| <div id="bah-finding"></div> | |
| </div> | |
| <!-- ββββββββββ INSIGHT FLASH ββββββββββββββββ --> | |
| <div id="bah-insight-flash"></div> | |
| <!-- ββββββββββ ANALYSIS COMPLETE MODAL ββββββ --> | |
| <div id="analysis-summary-modal"> | |
| <div id="asm-inner"> | |
| <div id="asm-title">β Analysis Complete</div> | |
| <div id="asm-sub" id="asm-sub-text"></div> | |
| <div id="asm-body"></div> | |
| <div id="asm-recommendation"></div> | |
| <div id="asm-actions"> | |
| <button class="asm-btn secondary" onclick="closeAnalysisSummary()">Dismiss</button> | |
| <button class="asm-btn primary" onclick="replayAnalysis()">β³ Replay</button> | |
| </div> | |
| </div> | |
| </div> | |
| <div id="neural-live-badge">β Neural-Sim Live</div> | |
| <!-- Cinematic transition overlays (galaxy enter/exit) --> | |
| <div id="fx-nebula"></div> | |
| <div id="fx-shimmer"></div> | |
| <div id="fx-vignette"></div> | |
| <div id="fx-static"></div> | |
| <div id="fx-supernova"></div> | |
| <!-- Game Menu (M) --> | |
| <div id="game-menu" aria-hidden="true"> | |
| <div class="gm-header"><div>Menu</div><button id="gm-close" class="tp-btn">β</button></div> | |
| <div class="gm-body"> | |
| <div class="gm-column"> | |
| <div class="gm-section"><div class="gm-label">Audio</div> | |
| <div class="gm-control"><label>Master</label><input id="gm-audio-master" type="range" min="0" max="1" step="0.01" class="gm-slider" value="1"></div> | |
| <div class="gm-control"><button id="gm-audio-mute" class="tp-btn" data-muted="0">Mute</button></div> | |
| </div> | |
| <div class="gm-section"><div class="gm-label">Display</div> | |
| <div class="gm-control"><label>Brightness</label><input id="gm-display-bright" type="range" min="0.2" max="2" step="0.05" class="gm-slider" value="1"></div> | |
| <div class="gm-control"><label>FOV</label><input id="gm-display-fov" type="range" min="45" max="110" step="1" class="gm-slider" value="75"></div> | |
| </div> | |
| </div> | |
| <div class="gm-column"> | |
| <div class="gm-section"><div class="gm-label">Gameplay</div> | |
| <div class="gm-control"><label>Walk Speed</label><input id="gm-walk-speed" type="range" min="1" max="10" step="0.1" class="gm-slider" value="4.5"></div> | |
| <div class="gm-control"><label>Sprint Mult</label><input id="gm-sprint-mult" type="range" min="1" max="3" step="0.05" class="gm-slider" value="1.8"></div> | |
| <div class="gm-control"><label>Mouse Sens</label><input id="gm-mouse-sens" type="range" min="0.2" max="3" step="0.05" class="gm-slider" value="1"></div> | |
| </div> | |
| <div class="gm-section"><div class="gm-label">Controls</div> | |
| <div id="gm-keybinds" class="gm-keybinds"></div> | |
| <div class="gm-control" style="margin-top:8px"><button id="gm-rebind-reset" class="tp-btn">Reset Bindings</button></div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="gm-actions"><button id="gm-reset" class="tp-btn">Reset</button><button id="gm-save" class="tp-btn launch">Save</button></div> | |
| </div> | |
| <!-- βββ Galaxy Mode UI βββββββββββββββββββ --> | |
| <button id="galaxy-btn" onclick="enterGalaxyMode()"> | |
| <span class="g-dot"></span> | |
| <span>File Galaxy</span> | |
| </button> | |
| <div id="galaxy-hud"> | |
| <div id="galaxy-title">File Galaxy</div> | |
| <div id="galaxy-subtitle">1,748,673 files Β· orbital view</div> | |
| <div id="galaxy-controls-hint"> | |
| <span>Drag Rotate</span><span>Scroll Zoom</span><span>Right-drag Pan</span><span>Click Fly-to</span> | |
| </div> | |
| <div id="galaxy-exit-hint">ESC or F Β· Return to Command Center</div> | |
| </div> | |
| <div id="galaxy-tooltip"> | |
| <div class="gt-name"></div> | |
| <div class="gt-path"></div> | |
| <div class="gt-meta"></div> | |
| </div> | |
| <div id="galaxy-loading"> | |
| <h3>Loading File Galaxy</h3> | |
| <p id="gl-status">Fetching constellation dataβ¦</p> | |
| <div class="gl-bar"><div class="gl-fill" id="gl-fill"></div></div> | |
| </div> | |
| <!-- Galaxy Sidebar --> | |
| <button id="galaxy-sidebar-toggle" onclick="toggleGalaxySidebar()">β°</button> | |
| <div id="galaxy-sidebar"> | |
| <h3>File Galaxy</h3> | |
| <div class="gs-sub" id="gs-stats">β</div> | |
| <div class="gs-section">Sources</div> | |
| <div id="gs-source-toggles"></div> | |
| <div class="gs-section">Density</div> | |
| <div class="gs-slider-row"> | |
| <input type="range" id="gs-density" min="5" max="100" value="100" | |
| oninput="onGalaxyDensityChange(this.value)"> | |
| <span class="gs-slider-val" id="gs-density-val">100%</span> | |
| </div> | |
| <div class="gs-section">Point Size</div> | |
| <div class="gs-slider-row"> | |
| <input type="range" id="gs-size" min="1" max="8" step="0.5" value="3" | |
| oninput="onGalaxySizeChange(this.value)"> | |
| <span class="gs-slider-val" id="gs-size-val">3</span> | |
| </div> | |
| <div class="gs-section">Search</div> | |
| <input type="text" id="galaxy-search" placeholder="filename, path, or extension" | |
| oninput="onGalaxySearch(this.value)"> | |
| <div id="galaxy-search-results"></div> | |
| </div> | |
| <!-- ββββββββββββ THREE.JS ββββββββββββ --> | |
| <script type="importmap"> | |
| { | |
| "imports": { | |
| "three": "https://cdn.jsdelivr.net/npm/three@0.170.0/build/three.module.js", | |
| "three/addons/": "https://cdn.jsdelivr.net/npm/three@0.170.0/examples/jsm/" | |
| } | |
| } | |
| </script> | |
| <script type="module"> | |
| import * as THREE from 'three'; | |
| import { PointerLockControls } from 'three/addons/controls/PointerLockControls.js'; | |
| import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; | |
| import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js'; | |
| import { RenderPass } from 'three/addons/postprocessing/RenderPass.js'; | |
| import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js'; | |
| // Guard against optional/undefined objects being added to the scene graph. | |
| const _origObject3DAdd = THREE.Object3D.prototype.add; | |
| THREE.Object3D.prototype.add = function (...objects) { | |
| const valid = objects.filter((obj) => obj && obj.isObject3D); | |
| if (valid.length === 0) return this; | |
| return _origObject3DAdd.apply(this, valid); | |
| }; | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // ββ CONFIGURATION | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| const CFG = { | |
| CEILING_H: 3.4, | |
| EYE_H: 1.65, | |
| WALL_THICK: 0.15, | |
| DOOR_W: 2.8, | |
| DOOR_H: 2.6, | |
| MOVE_SPEED: 4.5, | |
| SPRINT_MULT: 1.8, | |
| PLAYER_RADIUS: 0.35, | |
| INTERACT_DIST: 2.8, | |
| MOUSE_SENS: 0.002, | |
| }; | |
| const API = location.origin; | |
| const runningTools = {}; | |
| let backendOk = false; | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // ββ GLOBALS | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| let scene, camera, renderer, composer, controls; | |
| let clock, raycaster, interactRay; | |
| let collisionBoxes = []; | |
| let interactiveObjects = []; | |
| let roomZones = []; | |
| let currentRoom = 'Central Hub'; | |
| let toolPanelOpen = false; | |
| // Movement state | |
| const moveState = { forward: false, backward: false, left: false, right: false, sprint: false }; | |
| const velocity = new THREE.Vector3(); | |
| const direction = new THREE.Vector3(); | |
| const playerPos = new THREE.Vector3(0, CFG.EYE_H, 0); | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // ββ TOOL DATA | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| const TOOLS = { | |
| artemis: { name: 'ARTEMIS (Sage)', desc: 'Unified AI kernel β memory, mind, senses, voice, bridge, guardian, feelings, appraisal. The central nervous system.', tags: ['Python','FastAPI','SQLite','WebSocket'], hasLaunch: true }, | |
| ears: { name: 'ARTEMIS Ears', desc: 'Adaptive audio perception. Speech-to-text via faster-whisper, audio analysis via librosa. The hearing sense.', tags: ['Python','faster-whisper','librosa'], hasLaunch: true }, | |
| csm: { name: 'CSM-1B Voice', desc: 'Conversational speech synthesis. Wav-to-wav generation on RTX 5070 Ti via WSL CUDA.', tags: ['Python','PyTorch','WSL/CUDA'], hasLaunch: true }, | |
| 'neural-sim': { name: 'Neural Simulation', desc: 'Real-time LLM training dashboard. Anatomical 8192-point brain clouds (human + AI), 48 diagnostic metrics, LoRA fine-tuning with MAML meta-learning, SAM optimizer, self-modification engine with Bayesian optimization & rollback, EWC continual learning, gradient surgery, EMA teacher distillation, live WebSocket brain reactivity.', tags: ['Python','Three.js','WSL/CUDA','LoRA','MAML','SAM','EWC','BayesOpt'], hasLaunch: true, url: 'http://localhost:8765' }, | |
| 'artemis-server': { name: 'Artemis Server', desc: 'Portable HTTP file server with Cloudflare tunnel integration and Tkinter GUI.', tags: ['Go','Python/Tkinter'], hasLaunch: true }, | |
| comms: { name: 'Comms Platform', desc: 'AI-coordinated node-to-node encrypted communication platform.', tags: ['Python','FastAPI','WebSocket'], hasLaunch: true }, | |
| 'converter-gui': { name: 'BatβExe Converter', desc: 'Reverse-engineered reimplementation. Converts .bat scripts to standalone .exe with custom C stubs.', tags: ['Python','C'], hasLaunch: true }, | |
| gateway: { name: 'Gateway Router', desc: 'Dual-node virtual private router. WireGuard config generation and keepalive management.', tags: ['Python','WireGuard'], hasLaunch: true }, | |
| mirror: { name: 'Mirror', desc: 'ADB screen mirror service. Captures Android device screen and serves it via FastAPI.', tags: ['Python','FastAPI','ADB'], hasLaunch: true }, | |
| ghidra: { name: 'Ghidra 12.0.4', desc: 'Full static analysis suite for binaries β DEX, SO, APK, ELF, PE. Pre-built distribution.', tags: ['Java','Ghidra'], hasLaunch: true }, | |
| frida: { name: 'Frida 17.8.2', desc: 'Runtime function hooking for Android. ARM + ARM64 server binaries ready.', tags: ['JavaScript','Android'], hasLaunch: false }, | |
| wireshark: { name: 'Wireshark + Zeek + nDPI', desc: 'Packet capture, protocol dissection, JA3 fingerprinting, deep packet inspection.', tags: ['Network','WSL'], hasLaunch: false }, | |
| 'ghidra-scripts': { name: 'Ghidra Scripts', desc: '9 automated analysis scripts β decompilation, string extraction, import analysis, section mapping.', tags: ['Python','Ghidra'], hasLaunch: false }, | |
| 'frida-hooks': { name: 'Frida Hooks', desc: 'SSL pinning bypass, function tracing, memory dumping hook scripts.', tags: ['JavaScript','Frida'], hasLaunch: false }, | |
| playbooks: { name: 'Playbooks', desc: '4 YAML automation playbooks β guardian-start, neural-sim-launch, WSL dep-check, WSL emergency-stop.', tags: ['YAML','Python'], hasLaunch: false }, | |
| 'ref-apks': { name: 'Reference APK Library', desc: '13 decompiled APK packages studied for architectural patterns and design principles.', tags: ['APK','Java','Smali'], hasLaunch: false }, | |
| }; | |
| // Room definitions: which tools are in which room | |
| const ROOM_TOOLS = { | |
| 'AI Systems Lab': ['artemis','ears','csm','neural-sim'], | |
| 'Built Tools Workshop': ['artemis-server','comms','converter-gui','gateway','mirror'], | |
| 'Analysis Lab': ['ghidra','frida','wireshark'], | |
| 'Scripts & Automation': ['ghidra-scripts','frida-hooks','playbooks','ref-apks'], | |
| }; | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // ββ TEXTURE GENERATORS (Canvas) | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function makeCanvasTexture(w, h, drawFn) { | |
| const canvas = document.createElement('canvas'); | |
| canvas.width = w; canvas.height = h; | |
| const ctx = canvas.getContext('2d'); | |
| drawFn(ctx, w, h); | |
| const tex = new THREE.CanvasTexture(canvas); | |
| tex.colorSpace = THREE.SRGBColorSpace; | |
| return tex; | |
| } | |
| function wallTexture() { | |
| return makeCanvasTexture(256, 256, (ctx, w, h) => { | |
| // Warm cream base | |
| ctx.fillStyle = '#e8e0d4'; | |
| ctx.fillRect(0, 0, w, h); | |
| // Subtle noise | |
| for (let i = 0; i < 800; i++) { | |
| const x = Math.random() * w, y = Math.random() * h; | |
| const v = 200 + Math.random() * 40; | |
| ctx.fillStyle = `rgb(${v},${v-8},${v-16})`; | |
| ctx.fillRect(x, y, 2, 2); | |
| } | |
| // Faint horizontal line (drywall seam) | |
| ctx.strokeStyle = 'rgba(180,170,155,.15)'; | |
| ctx.lineWidth = 1; | |
| ctx.beginPath(); ctx.moveTo(0, h/2); ctx.lineTo(w, h/2); ctx.stroke(); | |
| }); | |
| } | |
| function floorTexture() { | |
| return makeCanvasTexture(512, 512, (ctx, w, h) => { | |
| // Dark industrial floor | |
| ctx.fillStyle = '#3a3832'; | |
| ctx.fillRect(0, 0, w, h); | |
| // Tile grid | |
| const tileSize = 64; | |
| ctx.strokeStyle = 'rgba(80,76,68,.3)'; | |
| ctx.lineWidth = 1; | |
| for (let x = 0; x <= w; x += tileSize) { | |
| ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, h); ctx.stroke(); | |
| } | |
| for (let y = 0; y <= h; y += tileSize) { | |
| ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(w, y); ctx.stroke(); | |
| } | |
| // Subtle noise | |
| for (let i = 0; i < 2000; i++) { | |
| const x = Math.random() * w, y = Math.random() * h; | |
| const v = 50 + Math.random() * 20; | |
| ctx.fillStyle = `rgba(${v},${v-2},${v-4},.4)`; | |
| ctx.fillRect(x, y, 1, 1); | |
| } | |
| }); | |
| } | |
| function ceilingTexture() { | |
| return makeCanvasTexture(256, 256, (ctx, w, h) => { | |
| ctx.fillStyle = '#f0ece4'; | |
| ctx.fillRect(0, 0, w, h); | |
| // Ceiling tile grid | |
| ctx.strokeStyle = 'rgba(180,172,160,.2)'; | |
| ctx.lineWidth = 1; | |
| const tile = 128; | |
| for (let x = 0; x <= w; x += tile) { | |
| ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, h); ctx.stroke(); | |
| } | |
| for (let y = 0; y <= h; y += tile) { | |
| ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(w, y); ctx.stroke(); | |
| } | |
| }); | |
| } | |
| function trimTexture() { | |
| return makeCanvasTexture(64, 64, (ctx, w, h) => { | |
| ctx.fillStyle = '#5a4a38'; | |
| ctx.fillRect(0, 0, w, h); | |
| // Wood grain | |
| for (let y = 0; y < h; y += 2) { | |
| ctx.strokeStyle = `rgba(${70+Math.random()*20},${55+Math.random()*15},${40+Math.random()*10},.3)`; | |
| ctx.beginPath(); ctx.moveTo(0, y + Math.random()*2); ctx.lineTo(w, y + Math.random()*2); ctx.stroke(); | |
| } | |
| }); | |
| } | |
| function doorTexture() { | |
| return makeCanvasTexture(128, 256, (ctx, w, h) => { | |
| ctx.fillStyle = '#6a5a48'; | |
| ctx.fillRect(0, 0, w, h); | |
| // Door panel insets | |
| ctx.strokeStyle = 'rgba(90,75,55,.5)'; | |
| ctx.lineWidth = 2; | |
| ctx.strokeRect(12, 12, w-24, h/2 - 20); | |
| ctx.strokeRect(12, h/2 + 8, w-24, h/2 - 20); | |
| // Wood grain | |
| for (let y = 0; y < h; y += 3) { | |
| ctx.strokeStyle = `rgba(${80+Math.random()*20},${65+Math.random()*15},${48+Math.random()*10},.15)`; | |
| ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(w, y); ctx.stroke(); | |
| } | |
| // Handle | |
| ctx.fillStyle = '#b8a888'; | |
| ctx.fillRect(w - 24, h/2 - 6, 8, 12); | |
| }); | |
| } | |
| function signTexture(text, accent = '#8a9aa8') { | |
| return makeCanvasTexture(512, 128, (ctx, w, h) => { | |
| // Dark background | |
| ctx.fillStyle = '#2a2824'; | |
| ctx.fillRect(0,0,w,h); | |
| // Border | |
| ctx.strokeStyle = accent; | |
| ctx.lineWidth = 2; | |
| ctx.strokeRect(4,4,w-4,h-4); | |
| // Text | |
| ctx.font = 'bold 32px Segoe UI, sans-serif'; | |
| ctx.textAlign = 'center'; | |
| ctx.textBaseline = 'middle'; | |
| ctx.fillStyle = accent; | |
| ctx.fillText(text, w/2, h/2); | |
| }); | |
| } | |
| function monitorTexture(toolId) { | |
| const tool = TOOLS[toolId]; | |
| if (!tool) return makeCanvasTexture(512, 384, (ctx,w,h) => { ctx.fillStyle='#111';ctx.fillRect(0,0,w,h); }); | |
| return makeCanvasTexture(512, 384, (ctx, w, h) => { | |
| // Screen bg | |
| ctx.fillStyle = '#0c0e10'; | |
| ctx.fillRect(0,0,w,h); | |
| // Subtle grid | |
| ctx.strokeStyle = 'rgba(100,140,160,.06)'; | |
| ctx.lineWidth = 1; | |
| for (let x = 0; x < w; x += 32) { ctx.beginPath();ctx.moveTo(x,0);ctx.lineTo(x,h);ctx.stroke(); } | |
| for (let y = 0; y < h; y += 32) { ctx.beginPath();ctx.moveTo(0,y);ctx.lineTo(w,y);ctx.stroke(); } | |
| // Header bar | |
| ctx.fillStyle = 'rgba(60,90,110,.15)'; | |
| ctx.fillRect(0,0,w,48); | |
| ctx.strokeStyle = 'rgba(100,140,160,.2)'; | |
| ctx.beginPath();ctx.moveTo(0,48);ctx.lineTo(w,48);ctx.stroke(); | |
| // Title | |
| ctx.font = 'bold 20px Segoe UI, sans-serif'; | |
| ctx.fillStyle = '#a0c0d0'; | |
| ctx.textAlign = 'left'; | |
| ctx.fillText(tool.name, 16, 32); | |
| // Status indicator | |
| const running = runningTools[toolId]; | |
| ctx.beginPath(); | |
| ctx.arc(w - 24, 24, 6, 0, Math.PI*2); | |
| ctx.fillStyle = running ? '#6a9a6a' : '#5a5a5a'; | |
| ctx.fill(); | |
| // Description | |
| ctx.font = '14px Segoe UI, sans-serif'; | |
| ctx.fillStyle = '#7a8a90'; | |
| const words = tool.desc.split(' '); | |
| let line = '', lineY = 80; | |
| for (const word of words) { | |
| const test = line + word + ' '; | |
| if (ctx.measureText(test).width > w - 32) { | |
| ctx.fillText(line.trim(), 16, lineY); | |
| line = word + ' '; lineY += 20; | |
| if (lineY > 160) break; | |
| } else { line = test; } | |
| } | |
| if (line.trim()) ctx.fillText(line.trim(), 16, lineY); | |
| // Tags | |
| let tagX = 16; | |
| const tagY = 200; | |
| ctx.font = '12px Cascadia Code, monospace'; | |
| for (const tag of tool.tags) { | |
| const tw = ctx.measureText(tag).width + 16; | |
| ctx.strokeStyle = 'rgba(100,140,160,.25)'; | |
| ctx.strokeRect(tagX, tagY, tw, 22); | |
| ctx.fillStyle = '#6a8a9a'; | |
| ctx.fillText(tag, tagX + 8, tagY + 15); | |
| tagX += tw + 6; | |
| if (tagX > w - 40) break; | |
| } | |
| // Bottom status bar | |
| ctx.fillStyle = 'rgba(60,90,110,.1)'; | |
| ctx.fillRect(0, h-36, w, 36); | |
| ctx.font = '11px Cascadia Code, monospace'; | |
| ctx.fillStyle = running ? '#6a9a6a' : '#5a5a5a'; | |
| ctx.fillText(running ? 'β RUNNING' : 'β STOPPED', 16, h-14); | |
| if (tool.hasLaunch) { | |
| ctx.fillStyle = '#4a6a7a'; | |
| ctx.fillText('PRESS E TO MANAGE', w - 160, h-14); | |
| } | |
| // Scanline effect | |
| for (let y = 0; y < h; y += 4) { | |
| ctx.fillStyle = 'rgba(0,0,0,.04)'; | |
| ctx.fillRect(0, y, w, 2); | |
| } | |
| }); | |
| } | |
| function hubFloorTexture() { | |
| return makeCanvasTexture(512, 512, (ctx, w, h) => { | |
| // Dark floor base | |
| ctx.fillStyle = '#3a3832'; | |
| ctx.fillRect(0, 0, w, h); | |
| // Compass rose / directional markers | |
| ctx.strokeStyle = 'rgba(160,150,130,.15)'; | |
| ctx.lineWidth = 2; | |
| // Circle | |
| ctx.beginPath(); ctx.arc(w/2, h/2, 200, 0, Math.PI*2); ctx.stroke(); | |
| ctx.beginPath(); ctx.arc(w/2, h/2, 140, 0, Math.PI*2); ctx.stroke(); | |
| // Cross lines | |
| ctx.beginPath(); ctx.moveTo(w/2, 40); ctx.lineTo(w/2, h-40); ctx.stroke(); | |
| ctx.beginPath(); ctx.moveTo(40, h/2); ctx.lineTo(w-40, h/2); ctx.stroke(); | |
| // ARTEMIS text | |
| ctx.font = 'bold 36px Segoe UI, sans-serif'; | |
| ctx.textAlign = 'center'; | |
| ctx.fillStyle = 'rgba(160,150,130,.12)'; | |
| ctx.fillText('ARTEMIS', w/2, h/2 + 12); | |
| // Direction labels | |
| ctx.font = '14px Segoe UI, sans-serif'; | |
| ctx.fillStyle = 'rgba(160,150,130,.2)'; | |
| ctx.fillText('AI SYSTEMS', w/2, 70); | |
| ctx.fillText('SCRIPTS', w/2, h - 56); | |
| ctx.save(); ctx.translate(w-56, h/2); ctx.rotate(-Math.PI/2); ctx.fillText('ANALYSIS', 0, 0); ctx.restore(); | |
| ctx.save(); ctx.translate(56, h/2); ctx.rotate(Math.PI/2); ctx.fillText('TOOLS', 0, 0); ctx.restore(); | |
| }); | |
| } | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // ββ MATERIALS | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| let MAT = {}; | |
| function buildMaterials() { | |
| const wallTex = wallTexture(); | |
| wallTex.wrapS = wallTex.wrapT = THREE.RepeatWrapping; | |
| wallTex.repeat.set(2, 1); | |
| const floorTex = floorTexture(); | |
| floorTex.wrapS = floorTex.wrapT = THREE.RepeatWrapping; | |
| const ceilTex = ceilingTexture(); | |
| ceilTex.wrapS = ceilTex.wrapT = THREE.RepeatWrapping; | |
| MAT = { | |
| wall: new THREE.MeshStandardMaterial({ map: wallTex, roughness: 0.85, metalness: 0.0 }), | |
| floor: new THREE.MeshStandardMaterial({ map: floorTex, roughness: 0.75, metalness: 0.1 }), | |
| ceiling: new THREE.MeshStandardMaterial({ map: ceilTex, roughness: 0.9, metalness: 0.0 }), | |
| trim: new THREE.MeshStandardMaterial({ map: trimTexture(), roughness: 0.6, metalness: 0.1 }), | |
| doorFrame: new THREE.MeshStandardMaterial({ color: 0x4a3a2a, roughness: 0.5, metalness: 0.1 }), | |
| monitorBezel: new THREE.MeshStandardMaterial({ color: 0x1a1a1a, roughness: 0.3, metalness: 0.6 }), | |
| desk: new THREE.MeshStandardMaterial({ color: 0x5a5048, roughness: 0.55, metalness: 0.15 }), | |
| metal: new THREE.MeshStandardMaterial({ color: 0x8a8a8a, roughness: 0.35, metalness: 0.7 }), | |
| lightPanel: new THREE.MeshStandardMaterial({ | |
| color: 0xfff8ee, emissive: 0xfff4e0, emissiveIntensity: 0.8, roughness: 1.0, metalness: 0.0 | |
| }), | |
| lightPanelOff: new THREE.MeshStandardMaterial({ | |
| color: 0xe0d8cc, roughness: 0.9, metalness: 0.0 | |
| }), | |
| }; | |
| } | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // ββ MAP DEFINITION | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // Each room: { name, x1,z1,x2,z2 (floor bounds), openings[], color, tools[] } | |
| // Openings define which wall has a doorway and where | |
| const ROOMS = [ | |
| // Central Hub | |
| { name: 'Central Hub', x1:-4.5, z1:-4.5, x2:4.5, z2:4.5, color: '#a09888', | |
| openings: [ | |
| { wall:'n', center:0 }, { wall:'e', center:0 }, | |
| { wall:'s', center:0 }, { wall:'w', center:0 }, | |
| ] | |
| }, | |
| // North corridor | |
| { name: 'North Corridor', x1:-1.5, z1:4.5, x2:1.5, z2:9, color: '#8a9aa8', openings: [ | |
| { wall:'s', center:0 }, { wall:'n', center:0 }, | |
| ]}, | |
| // AI Systems Lab | |
| { name: 'AI Systems Lab', x1:-5.5, z1:9, x2:5.5, z2:17, color: '#8aaab8', openings: [ | |
| { wall:'s', center:0 }, | |
| ], tools: ['artemis','ears','csm','neural-sim'] }, | |
| // East corridor | |
| { name: 'East Corridor', x1:4.5, z1:-1.5, x2:9, z2:1.5, color: '#a0a888', openings: [ | |
| { wall:'w', center:0 }, { wall:'e', center:0 }, | |
| ]}, | |
| // Analysis Lab | |
| { name: 'Analysis Lab', x1:9, z1:-5.5, x2:18, z2:5.5, color: '#a89a88', openings: [ | |
| { wall:'w', center:0 }, | |
| ], tools: ['ghidra','frida','wireshark'] }, | |
| // South corridor | |
| { name: 'South Corridor', x1:-1.5, z1:-9, x2:1.5, z2:-4.5, color: '#9a8ab8', openings: [ | |
| { wall:'n', center:0 }, { wall:'s', center:0 }, | |
| ]}, | |
| // Scripts & Automation | |
| { name: 'Scripts & Automation', x1:-5.5, z1:-17, x2:5.5, z2:-9, color: '#9a88b8', openings: [ | |
| { wall:'n', center:0 }, | |
| ], tools: ['ghidra-scripts','frida-hooks','playbooks','ref-apks'] }, | |
| // West corridor | |
| { name: 'West Corridor', x1:-9, z1:-1.5, x2:-4.5, z2:1.5, color: '#88a898', openings: [ | |
| { wall:'e', center:0 }, { wall:'w', center:0 }, | |
| ]}, | |
| // Built Tools Workshop | |
| { name: 'Built Tools Workshop', x1:-18, z1:-5.5, x2:-9, z2:5.5, color: '#88a888', openings: [ | |
| { wall:'e', center:0 }, | |
| ], tools: ['artemis-server','comms','converter-gui','gateway','mirror'] }, | |
| ]; | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // ββ GEOMETRY BUILDERS | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function addBox(w, h, d, x, y, z, mat, castShadow=false, receiveShadow=false) { | |
| const geo = new THREE.BoxGeometry(w, h, d); | |
| const mesh = new THREE.Mesh(geo, mat); | |
| mesh.position.set(x, y, z); | |
| mesh.castShadow = castShadow; | |
| mesh.receiveShadow = receiveShadow; | |
| scene.add(mesh); | |
| return mesh; | |
| } | |
| function addCollisionBox(x1, z1, x2, z2, y=0, h=CFG.CEILING_H) { | |
| collisionBoxes.push(new THREE.Box3( | |
| new THREE.Vector3(Math.min(x1,x2), y, Math.min(z1,z2)), | |
| new THREE.Vector3(Math.max(x1,x2), y+h, Math.max(z1,z2)) | |
| )); | |
| } | |
| function buildWallSegment(x, z, w, h, d, mat) { | |
| const mesh = addBox(w, h, d, x, h/2, z, mat, false, true); | |
| return mesh; | |
| } | |
| function buildRoom(room) { | |
| const { x1, z1, x2, z2, openings = [], name } = room; | |
| const w = x2 - x1, d = z2 - z1; | |
| const cx = (x1 + x2) / 2, cz = (z1 + z2) / 2; | |
| const H = CFG.CEILING_H; | |
| const T = CFG.WALL_THICK; | |
| const DW = CFG.DOOR_W; | |
| const DH = CFG.DOOR_H; | |
| // Floor | |
| const floorMat = (name === 'Central Hub') ? | |
| new THREE.MeshStandardMaterial({ map: hubFloorTexture(), roughness: 0.7, metalness: 0.1 }) : | |
| MAT.floor.clone(); | |
| floorMat.map = floorMat.map || MAT.floor.map; | |
| if (floorMat.map && name !== 'Central Hub') { | |
| floorMat.map = floorMat.map.clone(); | |
| floorMat.map.repeat.set(w/4, d/4); | |
| floorMat.map.wrapS = floorMat.map.wrapT = THREE.RepeatWrapping; | |
| } | |
| const floor = new THREE.Mesh(new THREE.PlaneGeometry(w, d), floorMat); | |
| floor.rotation.x = -Math.PI / 2; | |
| floor.position.set(cx, 0.001, cz); | |
| floor.receiveShadow = true; | |
| scene.add(floor); | |
| // Ceiling | |
| const ceilMat = MAT.ceiling.clone(); | |
| if (ceilMat.map) { | |
| ceilMat.map = ceilMat.map.clone(); | |
| ceilMat.map.repeat.set(w/3, d/3); | |
| ceilMat.map.wrapS = ceilMat.map.wrapT = THREE.RepeatWrapping; | |
| } | |
| const ceil = new THREE.Mesh(new THREE.PlaneGeometry(w, d), ceilMat); | |
| ceil.rotation.x = Math.PI / 2; | |
| ceil.position.set(cx, H, cz); | |
| scene.add(ceil); | |
| // Room zone for detection | |
| roomZones.push({ name, x1, z1, x2, z2 }); | |
| // Walls β for each side, check if there's an opening | |
| const sides = [ | |
| { wall: 'n', sx: x1, sz: z2, ex: x2, ez: z2, axis: 'x', normal: [0,0,-1] }, | |
| { wall: 's', sx: x1, sz: z1, ex: x2, ez: z1, axis: 'x', normal: [0,0,1] }, | |
| { wall: 'e', sx: x2, sz: z1, ex: x2, ez: z2, axis: 'z', normal: [-1,0,0] }, | |
| { wall: 'w', sx: x1, sz: z1, ex: x1, ez: z2, axis: 'z', normal: [1,0,0] }, | |
| ]; | |
| for (const side of sides) { | |
| const opening = openings.find(o => o.wall === side.wall); | |
| if (side.axis === 'x') { | |
| // Wall runs along X | |
| const wallLen = Math.abs(side.ex - side.sx); | |
| const wallZ = side.sz; | |
| const wallCx = (side.sx + side.ex) / 2; | |
| if (opening) { | |
| const openCenter = wallCx + (opening.center || 0); | |
| const leftLen = openCenter - DW/2 - side.sx; | |
| const rightLen = side.ex - (openCenter + DW/2); | |
| // Left segment | |
| if (leftLen > 0.1) { | |
| const lx = side.sx + leftLen/2; | |
| buildWallSegment(lx, wallZ, leftLen, H, T, MAT.wall); | |
| addCollisionBox(side.sx, wallZ - T/2, side.sx + leftLen, wallZ + T/2); | |
| } | |
| // Right segment | |
| if (rightLen > 0.1) { | |
| const rx = side.ex - rightLen/2; | |
| buildWallSegment(rx, wallZ, rightLen, H, T, MAT.wall); | |
| addCollisionBox(side.ex - rightLen, wallZ - T/2, side.ex, wallZ + T/2); | |
| } | |
| // Lintel above door | |
| const lintelH = H - DH; | |
| if (lintelH > 0.1) { | |
| buildWallSegment(openCenter, wallZ, DW, lintelH, T, MAT.wall); | |
| // No collision for lintel (above player) | |
| } | |
| // Door frame | |
| addBox(0.08, DH, T+0.04, openCenter - DW/2, DH/2, wallZ, MAT.doorFrame); | |
| addBox(0.08, DH, T+0.04, openCenter + DW/2, DH/2, wallZ, MAT.doorFrame); | |
| addBox(DW+0.08, 0.08, T+0.04, openCenter, DH, wallZ, MAT.doorFrame); | |
| } else { | |
| // Solid wall | |
| buildWallSegment(wallCx, wallZ, wallLen, H, T, MAT.wall); | |
| addCollisionBox(side.sx, wallZ - T/2, side.ex, wallZ + T/2); | |
| } | |
| } else { | |
| // Wall runs along Z | |
| const wallLen = Math.abs(side.ez - side.sz); | |
| const wallX = side.sx; | |
| const wallCz = (side.sz + side.ez) / 2; | |
| if (opening) { | |
| const openCenter = wallCz + (opening.center || 0); | |
| const leftLen = openCenter - DW/2 - side.sz; | |
| const rightLen = side.ez - (openCenter + DW/2); | |
| if (leftLen > 0.1) { | |
| const lz = side.sz + leftLen/2; | |
| buildWallSegment(wallX, lz, T, H, leftLen, MAT.wall); | |
| addCollisionBox(wallX - T/2, side.sz, wallX + T/2, side.sz + leftLen); | |
| } | |
| if (rightLen > 0.1) { | |
| const rz = side.ez - rightLen/2; | |
| buildWallSegment(wallX, rz, T, H, rightLen, MAT.wall); | |
| addCollisionBox(wallX - T/2, side.ez - rightLen, wallX + T/2, side.ez); | |
| } | |
| const lintelH = H - DH; | |
| if (lintelH > 0.1) { | |
| buildWallSegment(wallX, openCenter, T, lintelH, DW, MAT.wall); | |
| } | |
| // Door frame | |
| addBox(T+0.04, DH, 0.08, wallX, DH/2, openCenter - DW/2, MAT.doorFrame); | |
| addBox(T+0.04, DH, 0.08, wallX, DH/2, openCenter + DW/2, MAT.doorFrame); | |
| addBox(T+0.04, 0.08, DW+0.08, wallX, DH, openCenter, MAT.doorFrame); | |
| } else { | |
| buildWallSegment(wallX, wallCz, T, H, wallLen, MAT.wall); | |
| addCollisionBox(wallX - T/2, side.sz, wallX + T/2, side.ez); | |
| } | |
| } | |
| } | |
| // Baseboards | |
| const baseH = 0.12, baseD = 0.03; | |
| const baseColor = new THREE.MeshStandardMaterial({ color: 0x4a3a2a, roughness: 0.6, metalness: 0.1 }); | |
| // North baseboard | |
| if (!openings.find(o => o.wall === 'n')) | |
| addBox(w, baseH, baseD, cx, baseH/2, z2 - baseD/2, baseColor); | |
| // South baseboard | |
| if (!openings.find(o => o.wall === 's')) | |
| addBox(w, baseH, baseD, cx, baseH/2, z1 + baseD/2, baseColor); | |
| // East baseboard | |
| if (!openings.find(o => o.wall === 'e')) | |
| addBox(baseD, baseH, d, x2 - baseD/2, baseH/2, cz, baseColor); | |
| // West baseboard | |
| if (!openings.find(o => o.wall === 'w')) | |
| addBox(baseD, baseH, d, x1 + baseD/2, baseH/2, cz, baseColor); | |
| // Ceiling light panels | |
| buildCeilingLights(room); | |
| // Room sign above doorways (only for main rooms, not corridors) | |
| if (name.includes('Corridor') === false && name !== 'Central Hub') { | |
| buildRoomSign(room); | |
| } | |
| } | |
| function buildCeilingLights(room) { | |
| const { x1, z1, x2, z2 } = room; | |
| const w = x2 - x1, d = z2 - z1; | |
| const cx = (x1+x2)/2, cz = (z1+z2)/2; | |
| const H = CFG.CEILING_H; | |
| // Place lights in a grid pattern | |
| const spacing = 3.5; | |
| const panelW = 1.2, panelD = 0.4; | |
| for (let gx = cx - w/2 + spacing/2; gx < cx + w/2; gx += spacing) { | |
| for (let gz = cz - d/2 + spacing/2; gz < cz + d/2; gz += spacing) { | |
| // Light panel geometry | |
| addBox(panelW, 0.04, panelD, gx, H - 0.02, gz, MAT.lightPanel); | |
| // Recessed housing around it | |
| addBox(panelW + 0.1, 0.06, panelD + 0.1, gx, H - 0.04, gz, MAT.metal); | |
| // Actual light source | |
| const light = new THREE.PointLight(0xfff4e0, 0.6, 8, 1.5); | |
| light.position.set(gx, H - 0.1, gz); | |
| light.castShadow = false; // Performance | |
| scene.add(light); | |
| } | |
| } | |
| } | |
| function buildRoomSign(room) { | |
| // Find the room's entry opening | |
| const { openings, name, x1, z1, x2, z2 } = room; | |
| if (!openings || !openings.length) return; | |
| const opening = openings[0]; | |
| const H = CFG.CEILING_H; | |
| const cx = (x1+x2)/2, cz = (z1+z2)/2; | |
| const tex = signTexture(name.toUpperCase(), room.color || '#8a9aa8'); | |
| const signMat = new THREE.MeshStandardMaterial({ | |
| map: tex, emissive: 0xffffff, emissiveMap: tex, emissiveIntensity: 0.15, | |
| roughness: 0.8, metalness: 0.2 | |
| }); | |
| let signX, signZ, rotY; | |
| switch (opening.wall) { | |
| case 's': signX = cx; signZ = z1 + 0.2; rotY = 0; break; | |
| case 'n': signX = cx; signZ = z2 - 0.2; rotY = Math.PI; break; | |
| case 'w': signX = x1 + 0.2; signZ = cz; rotY = Math.PI/2; break; | |
| case 'e': signX = x2 - 0.2; signZ = cz; rotY = -Math.PI/2; break; | |
| } | |
| const sign = new THREE.Mesh(new THREE.PlaneGeometry(2.0, 0.5), signMat); | |
| sign.position.set(signX, H - 0.5, signZ); | |
| sign.rotation.y = rotY; | |
| scene.add(sign); | |
| } | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // ββ TERMINAL (MONITOR) BUILDERS | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| const monitorMeshes = {}; // toolId β mesh (for updating textures) | |
| function buildTerminal(toolId, x, y, z, rotY) { | |
| const group = new THREE.Group(); | |
| group.position.set(x, y, z); | |
| group.rotation.y = rotY; | |
| // Monitor bezel | |
| const bezelW = 1.1, bezelH = 0.85, bezelD = 0.06; | |
| const bezel = new THREE.Mesh(new THREE.BoxGeometry(bezelW, bezelH, bezelD), MAT.monitorBezel); | |
| bezel.castShadow = true; | |
| group.add(bezel); | |
| // Screen face | |
| const screenTex = monitorTexture(toolId); | |
| const screenMat = new THREE.MeshStandardMaterial({ | |
| map: screenTex, emissive: 0xffffff, emissiveMap: screenTex, | |
| emissiveIntensity: 0.5, roughness: 0.1, metalness: 0.0, | |
| }); | |
| const screen = new THREE.Mesh(new THREE.PlaneGeometry(1.0, 0.72), screenMat); | |
| screen.position.z = bezelD/2 + 0.002; | |
| screen.userData = { toolId, type: 'terminal' }; | |
| group.add(screen); | |
| interactiveObjects.push(screen); | |
| monitorMeshes[toolId] = screen; | |
| // Monitor glow light | |
| const hex = TOOLS[toolId] ? 0x405060 : 0x303030; | |
| const glow = new THREE.PointLight(hex, 0.15, 2.5, 2); | |
| glow.position.z = 0.15; | |
| group.add(glow); | |
| // Wall mount bracket | |
| const bracket = new THREE.Mesh( | |
| new THREE.BoxGeometry(0.15, 0.15, 0.08), | |
| MAT.metal | |
| ); | |
| bracket.position.z = -bezelD/2 - 0.04; | |
| group.add(bracket); | |
| scene.add(group); | |
| return group; | |
| } | |
| function buildDesk(x, z, w, d, rotY = 0) { | |
| const group = new THREE.Group(); | |
| group.position.set(x, 0, z); | |
| group.rotation.y = rotY; | |
| const topH = 0.04, legH = 0.72, topY = legH + topH/2; | |
| // Desktop surface | |
| const top = new THREE.Mesh(new THREE.BoxGeometry(w, topH, d), MAT.desk); | |
| top.position.y = topY; | |
| top.castShadow = true; | |
| top.receiveShadow = true; | |
| group.add(top); | |
| // Legs | |
| const legGeo = new THREE.BoxGeometry(0.04, legH, 0.04); | |
| const offX = w/2 - 0.06, offZ = d/2 - 0.06; | |
| [[-offX,-offZ],[offX,-offZ],[offX,offZ],[-offX,offZ]].forEach(([lx,lz]) => { | |
| const leg = new THREE.Mesh(legGeo, MAT.metal); | |
| leg.position.set(lx, legH/2, lz); | |
| group.add(leg); | |
| }); | |
| scene.add(group); | |
| // Add collision | |
| const cos = Math.cos(rotY), sin = Math.sin(rotY); | |
| const halfW = w/2 + 0.1, halfD = d/2 + 0.1; | |
| // Simplified AABB (ignores rotation for small desks) | |
| addCollisionBox(x - halfW, z - halfD, x + halfW, z + halfD, 0, topY + topH/2); | |
| return group; | |
| } | |
| function buildRoomFurniture(room) { | |
| if (!room.tools || !room.tools.length) return; | |
| const { x1, z1, x2, z2, tools } = room; | |
| const cx = (x1+x2)/2, cz = (z1+z2)/2; | |
| const rw = x2 - x1, rd = z2 - z1; | |
| // Place terminals along walls | |
| const monitorY = 1.55; // Eye-level mounted | |
| const wallOffset = 0.05; // Distance from wall | |
| // Determine which wall is the "back" (opposite to the opening) | |
| const opening = room.openings[0]; | |
| let backWall, sideWall1, sideWall2; | |
| switch (opening.wall) { | |
| case 's': // Entry from south, back wall is north | |
| backWall = { axis: 'x', z: z2, dir: Math.PI, xRange: [x1+1.2, x2-1.2] }; | |
| sideWall1 = { axis: 'z', x: x1, dir: Math.PI/2, zRange: [z1+1.5, z2-1] }; | |
| sideWall2 = { axis: 'z', x: x2, dir: -Math.PI/2, zRange: [z1+1.5, z2-1] }; | |
| break; | |
| case 'n': | |
| backWall = { axis: 'x', z: z1, dir: 0, xRange: [x1+1.2, x2-1.2] }; | |
| sideWall1 = { axis: 'z', x: x1, dir: Math.PI/2, zRange: [z1+1, z2-1.5] }; | |
| sideWall2 = { axis: 'z', x: x2, dir: -Math.PI/2, zRange: [z1+1, z2-1.5] }; | |
| break; | |
| case 'w': | |
| backWall = { axis: 'z', x: x2, dir: -Math.PI/2, zRange: [z1+1.2, z2-1.2] }; | |
| sideWall1 = { axis: 'x', z: z1, dir: 0, xRange: [x1+1.5, x2-1] }; | |
| sideWall2 = { axis: 'x', z: z2, dir: Math.PI, xRange: [x1+1.5, x2-1] }; | |
| break; | |
| case 'e': | |
| backWall = { axis: 'z', x: x1, dir: Math.PI/2, zRange: [z1+1.2, z2-1.2] }; | |
| sideWall1 = { axis: 'x', z: z1, dir: 0, xRange: [x1+1, x2-1.5] }; | |
| sideWall2 = { axis: 'x', z: z2, dir: Math.PI, xRange: [x1+1, x2-1.5] }; | |
| break; | |
| } | |
| // Distribute terminals | |
| const walls = [backWall, sideWall1, sideWall2]; | |
| let toolIdx = 0; | |
| for (const wall of walls) { | |
| if (toolIdx >= tools.length) break; | |
| // How many monitors fit on this wall | |
| let available; | |
| if (wall.axis === 'x') { | |
| available = (wall.xRange[1] - wall.xRange[0]); | |
| } else { | |
| available = (wall.zRange[1] - wall.zRange[0]); | |
| } | |
| const maxOnWall = Math.min(Math.floor(available / 1.6), tools.length - toolIdx, 3); | |
| if (maxOnWall <= 0) continue; | |
| for (let i = 0; i < maxOnWall && toolIdx < tools.length; i++, toolIdx++) { | |
| const t = (i + 0.5) / maxOnWall; // 0..1 evenly distributed | |
| let mx, mz; | |
| if (wall.axis === 'x') { | |
| mx = wall.xRange[0] + t * (wall.xRange[1] - wall.xRange[0]); | |
| mz = wall.z + (wall.dir === Math.PI ? -wallOffset : wallOffset); | |
| } else { | |
| mz = wall.zRange[0] + t * (wall.zRange[1] - wall.zRange[0]); | |
| mx = wall.x + (wall.dir === -Math.PI/2 ? -wallOffset : wallOffset); | |
| } | |
| buildTerminal(tools[toolIdx], mx, monitorY, mz, wall.dir); | |
| // Desk below each terminal | |
| const deskOff = 0.6; | |
| let dx = mx, dz = mz; | |
| if (wall.axis === 'x') { | |
| dz += (wall.dir === Math.PI ? -deskOff : deskOff); | |
| } else { | |
| dx += (wall.dir === -Math.PI/2 ? -deskOff : deskOff); | |
| } | |
| buildDesk(dx, dz, 1.0, 0.6, wall.dir); | |
| } | |
| } | |
| } | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // ββ LIGHTING | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function buildLighting() { | |
| // Ambient β very subtle warm fill | |
| const ambient = new THREE.AmbientLight(0xfff4e0, 0.15); | |
| scene.add(ambient); | |
| // Hemisphere β warm sky, cool ground | |
| const hemi = new THREE.HemisphereLight(0xfff4e0, 0x3a3832, 0.2); | |
| scene.add(hemi); | |
| } | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // ββ ENVIRONMENT DETAILS | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function buildEnvironmentDetails() { | |
| // Central Hub β large overhead light fixture | |
| const hubLight = new THREE.SpotLight(0xfff4e0, 1.5, 12, Math.PI/4, 0.5, 1); | |
| hubLight.position.set(0, CFG.CEILING_H - 0.1, 0); | |
| hubLight.target.position.set(0, 0, 0); | |
| scene.add(hubLight); | |
| scene.add(hubLight.target); | |
| // Hub center floor emblem glow | |
| const emblLight = new THREE.PointLight(0xd4c5a9, 0.1, 5, 2); | |
| emblLight.position.set(0, 0.1, 0); | |
| scene.add(emblLight); | |
| // Directional arrow signs on hub walls | |
| buildDirectionSign(0, 1.6, 4.3, 0, 'β AI SYSTEMS', '#8aaab8'); | |
| buildDirectionSign(0, 1.6, -4.3, Math.PI, 'β SCRIPTS', '#9a88b8'); | |
| buildDirectionSign(4.3, 1.6, 0, -Math.PI/2, 'β ANALYSIS', '#a89a88'); | |
| buildDirectionSign(-4.3, 1.6, 0, Math.PI/2, 'β TOOLS', '#88a888'); | |
| // Fire extinguisher in corridor (environmental detail) | |
| buildFireExtinguisher(-1.3, 0, 6.5); | |
| // Potted plant in hub corner | |
| buildPlant(3.5, 0, 3.5); | |
| buildPlant(-3.5, 0, -3.5); | |
| // Water cooler | |
| buildWaterCooler(3.8, 0, -3); | |
| // Clock on hub wall | |
| buildWallClock(0, 2.5, -4.4); | |
| } | |
| function buildDirectionSign(x, y, z, rotY, text, color) { | |
| const tex = makeCanvasTexture(400, 80, (ctx, w, h) => { | |
| ctx.fillStyle = '#2a2824'; | |
| ctx.fillRect(0,0,w,h); | |
| ctx.strokeStyle = color; | |
| ctx.lineWidth = 1; | |
| ctx.strokeRect(2,2,w-4,h-4); | |
| ctx.font = 'bold 22px Segoe UI, sans-serif'; | |
| ctx.textAlign = 'center'; | |
| ctx.textBaseline = 'middle'; | |
| ctx.fillStyle = color; | |
| ctx.fillText(text, w/2, h/2); | |
| }); | |
| const mat = new THREE.MeshStandardMaterial({ | |
| map: tex, emissive: 0xffffff, emissiveMap: tex, emissiveIntensity: 0.1, | |
| roughness: 0.8, metalness: 0.2 | |
| }); | |
| const sign = new THREE.Mesh(new THREE.PlaneGeometry(1.2, 0.24), mat); | |
| sign.position.set(x, y, z); | |
| sign.rotation.y = rotY; | |
| scene.add(sign); | |
| } | |
| function buildFireExtinguisher(x, y, z) { | |
| const group = new THREE.Group(); | |
| group.position.set(x, y, z); | |
| // Body | |
| const body = new THREE.Mesh( | |
| new THREE.CylinderGeometry(0.06, 0.06, 0.35, 8), | |
| new THREE.MeshStandardMaterial({ color: 0xcc3333, roughness: 0.4, metalness: 0.3 }) | |
| ); | |
| body.position.y = 0.85; | |
| group.add(body); | |
| // Top | |
| const top = new THREE.Mesh( | |
| new THREE.CylinderGeometry(0.03, 0.06, 0.06, 8), | |
| new THREE.MeshStandardMaterial({ color: 0x333333, roughness: 0.3, metalness: 0.6 }) | |
| ); | |
| top.position.y = 1.06; | |
| group.add(top); | |
| // Wall bracket | |
| const bracket = new THREE.Mesh( | |
| new THREE.BoxGeometry(0.15, 0.04, 0.08), | |
| MAT.metal | |
| ); | |
| bracket.position.y = 0.9; | |
| bracket.position.z = -0.05; | |
| group.add(bracket); | |
| scene.add(group); | |
| } | |
| function buildPlant(x, y, z) { | |
| const group = new THREE.Group(); | |
| group.position.set(x, y, z); | |
| // Pot | |
| const pot = new THREE.Mesh( | |
| new THREE.CylinderGeometry(0.15, 0.12, 0.25, 8), | |
| new THREE.MeshStandardMaterial({ color: 0x6a5a4a, roughness: 0.7, metalness: 0.1 }) | |
| ); | |
| pot.position.y = 0.125; | |
| group.add(pot); | |
| // Foliage (simple sphere cluster) | |
| const leafMat = new THREE.MeshStandardMaterial({ color: 0x4a7a4a, roughness: 0.8, metalness: 0.0 }); | |
| for (let i = 0; i < 5; i++) { | |
| const leaf = new THREE.Mesh(new THREE.SphereGeometry(0.08 + Math.random()*0.06, 6, 6), leafMat); | |
| leaf.position.set((Math.random()-0.5)*0.12, 0.3 + Math.random()*0.15, (Math.random()-0.5)*0.12); | |
| group.add(leaf); | |
| } | |
| scene.add(group); | |
| addCollisionBox(x-0.2, z-0.2, x+0.2, z+0.2, 0, 0.5); | |
| } | |
| function buildWaterCooler(x, y, z) { | |
| const group = new THREE.Group(); | |
| group.position.set(x, y, z); | |
| // Body | |
| const body = new THREE.Mesh( | |
| new THREE.BoxGeometry(0.3, 0.9, 0.3), | |
| new THREE.MeshStandardMaterial({ color: 0xe8e0d8, roughness: 0.5, metalness: 0.2 }) | |
| ); | |
| body.position.y = 0.45; | |
| group.add(body); | |
| // Water bottle | |
| const bottle = new THREE.Mesh( | |
| new THREE.CylinderGeometry(0.08, 0.1, 0.35, 8), | |
| new THREE.MeshStandardMaterial({ color: 0x88bbdd, roughness: 0.1, metalness: 0.0, transparent: true, opacity: 0.6 }) | |
| ); | |
| bottle.position.y = 1.1; | |
| group.add(bottle); | |
| scene.add(group); | |
| addCollisionBox(x-0.2, z-0.2, x+0.2, z+0.2, 0, 1.3); | |
| } | |
| function buildWallClock(x, y, z) { | |
| // Simple clock face | |
| const tex = makeCanvasTexture(128, 128, (ctx, w, h) => { | |
| ctx.fillStyle = '#f0ece4'; | |
| ctx.beginPath(); ctx.arc(w/2, h/2, 60, 0, Math.PI*2); ctx.fill(); | |
| ctx.strokeStyle = '#4a3a2a'; | |
| ctx.lineWidth = 3; | |
| ctx.beginPath(); ctx.arc(w/2, h/2, 60, 0, Math.PI*2); ctx.stroke(); | |
| // Hour marks | |
| for (let i = 0; i < 12; i++) { | |
| const a = (i/12) * Math.PI * 2 - Math.PI/2; | |
| ctx.beginPath(); | |
| ctx.moveTo(w/2 + Math.cos(a)*48, h/2 + Math.sin(a)*48); | |
| ctx.lineTo(w/2 + Math.cos(a)*55, h/2 + Math.sin(a)*55); | |
| ctx.stroke(); | |
| } | |
| // Hands (static β shows ~10:10) | |
| ctx.lineWidth = 3; | |
| ctx.beginPath(); ctx.moveTo(w/2, h/2); ctx.lineTo(w/2-20, h/2-30); ctx.stroke(); | |
| ctx.lineWidth = 2; | |
| ctx.beginPath(); ctx.moveTo(w/2, h/2); ctx.lineTo(w/2+18, h/2-35); ctx.stroke(); | |
| ctx.fillStyle = '#4a3a2a'; | |
| ctx.beginPath(); ctx.arc(w/2, h/2, 3, 0, Math.PI*2); ctx.fill(); | |
| }); | |
| const mat = new THREE.MeshStandardMaterial({ map: tex, roughness: 0.6, metalness: 0.1 }); | |
| const clock = new THREE.Mesh(new THREE.PlaneGeometry(0.4, 0.4), mat); | |
| clock.position.set(x, y, z); | |
| scene.add(clock); | |
| } | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // ββ AUDIO (Web Audio API) | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| let audioCtx, footstepGain, ambientGain, masterGain; | |
| let stepTimer = 0; | |
| const STEP_INTERVAL = 0.45; | |
| const SPRINT_STEP_INTERVAL = 0.28; | |
| function initAudio() { | |
| audioCtx = new (window.AudioContext || window.webkitAudioContext)(); | |
| // Master gain β single point that menu can control | |
| masterGain = audioCtx.createGain(); | |
| masterGain.gain.value = 1.0; | |
| masterGain.connect(audioCtx.destination); | |
| window.__artemis_master_gain = masterGain; | |
| // Apply any saved volume immediately | |
| try { | |
| const saved = JSON.parse(localStorage.getItem('artemis.settings.v1') || '{}'); | |
| if (typeof saved.master === 'number') masterGain.gain.value = saved.mute ? 0 : saved.master; | |
| } catch(e) {} | |
| // Ambient hum | |
| ambientGain = audioCtx.createGain(); | |
| ambientGain.gain.value = 0.008; | |
| ambientGain.connect(masterGain); | |
| // Very quiet HVAC-like noise | |
| const bufSize = audioCtx.sampleRate * 2; | |
| const noiseBuffer = audioCtx.createBuffer(1, bufSize, audioCtx.sampleRate); | |
| const data = noiseBuffer.getChannelData(0); | |
| for (let i = 0; i < bufSize; i++) { | |
| data[i] = (Math.random() * 2 - 1); | |
| } | |
| const noise = audioCtx.createBufferSource(); | |
| noise.buffer = noiseBuffer; | |
| noise.loop = true; | |
| // Low-pass to make it a hum | |
| const filter = audioCtx.createBiquadFilter(); | |
| filter.type = 'lowpass'; | |
| filter.frequency.value = 120; | |
| filter.Q.value = 0.5; | |
| noise.connect(filter); | |
| filter.connect(ambientGain); | |
| noise.start(); | |
| // Footstep gain node | |
| footstepGain = audioCtx.createGain(); | |
| footstepGain.gain.value = 0.08; | |
| footstepGain.connect(masterGain); | |
| } | |
| function playFootstep() { | |
| if (!audioCtx) return; | |
| const osc = audioCtx.createOscillator(); | |
| const gain = audioCtx.createGain(); | |
| osc.type = 'triangle'; | |
| osc.frequency.value = 80 + Math.random() * 60; | |
| gain.gain.setValueAtTime(0.06 + Math.random() * 0.03, audioCtx.currentTime); | |
| gain.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + 0.08); | |
| osc.connect(gain); | |
| gain.connect(masterGain || audioCtx.destination); | |
| osc.start(); | |
| osc.stop(audioCtx.currentTime + 0.1); | |
| } | |
| function playInteractSound() { | |
| if (!audioCtx) return; | |
| const osc = audioCtx.createOscillator(); | |
| const gain = audioCtx.createGain(); | |
| osc.type = 'sine'; | |
| osc.frequency.value = 600; | |
| osc.frequency.exponentialRampToValueAtTime(800, audioCtx.currentTime + 0.06); | |
| gain.gain.setValueAtTime(0.06, audioCtx.currentTime); | |
| gain.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + 0.12); | |
| osc.connect(gain); | |
| gain.connect(masterGain || audioCtx.destination); | |
| osc.start(); | |
| osc.stop(audioCtx.currentTime + 0.15); | |
| } | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // ββ NARRATOR SYSTEM (existing text overlay) | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| const narratorEl = document.getElementById('narrator'); | |
| let narratorTimeout = null; | |
| const visitedRooms = new Set(); | |
| const NARRATOR_LINES = { | |
| 'Central Hub': "You find yourself at the center of everything. Four corridors branch outward β each one leading somewhere that matters.", | |
| 'AI Systems Lab': "The AI Systems Lab. This is where thought happens, or at least, where the machinery that produces something resembling thought is kept carefully maintained.", | |
| 'Built Tools Workshop': "The workshop. Every tool here was built by hand β reverse-engineered from something that already existed, then rebuilt from scratch. Because that's how you learn what something really does.", | |
| 'Analysis Lab': "The Analysis Lab. Ghidra, Frida, Wireshark β the instruments of understanding. Nothing passes through this workspace without being understood first.", | |
| 'Scripts & Automation': "Scripts and automation. The quiet workers. They do the tedious things reliably so you can do the interesting things freely.", | |
| }; | |
| function narratorSpeak(text) { | |
| if (narratorTimeout) clearTimeout(narratorTimeout); | |
| narratorEl.textContent = text; | |
| narratorEl.classList.add('visible'); | |
| narratorTimeout = setTimeout(() => { | |
| narratorEl.classList.remove('visible'); | |
| }, 6000); | |
| } | |
| function checkRoomNarration(roomName) { | |
| if (visitedRooms.has(roomName)) return; | |
| if (!NARRATOR_LINES[roomName]) return; | |
| visitedRooms.add(roomName); | |
| setTimeout(() => narratorSpeak(NARRATOR_LINES[roomName]), 800); | |
| } | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // ββ VOICE NARRATION SYSTEM (new - proximity based) | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| let currentVoiceRoom = null; | |
| let voiceDelayTimer = null; | |
| let currentVoiceAudio = null; | |
| let ttsSpeaking = false; | |
| // Mapping of room names to potential audio element IDs (fallback to TTS if no element found) | |
| const ROOM_VOICE_IDS = { | |
| 'Central Hub': 'voice-central-hub', | |
| 'AI Systems Lab': 'voice-ai-systems', | |
| 'Built Tools Workshop': 'voice-built-tools', | |
| 'Analysis Lab': 'voice-analysis', | |
| 'Scripts & Automation': 'voice-scripts', | |
| 'North Corridor': 'voice-north-corridor', | |
| 'South Corridor': 'voice-south-corridor', | |
| 'East Corridor': 'voice-east-corridor', | |
| 'West Corridor': 'voice-west-corridor' | |
| }; | |
| function stopVoiceNarration() { | |
| if (voiceDelayTimer) { | |
| clearTimeout(voiceDelayTimer); | |
| voiceDelayTimer = null; | |
| } | |
| if (currentVoiceAudio) { | |
| currentVoiceAudio.pause(); | |
| currentVoiceAudio.currentTime = 0; | |
| currentVoiceAudio = null; | |
| } | |
| if ('speechSynthesis' in window && ttsSpeaking) { | |
| window.speechSynthesis.cancel(); | |
| ttsSpeaking = false; | |
| } | |
| currentVoiceRoom = null; | |
| } | |
| function playVoiceForRoom(roomName) { | |
| // Prefer audio tag if present (will auto-fallback to TTS since no audio tags exist) | |
| const audioId = ROOM_VOICE_IDS[roomName]; | |
| const audioEl = audioId ? document.getElementById(audioId) : null; | |
| if (audioEl) { | |
| currentVoiceAudio = audioEl; | |
| currentVoiceAudio.currentTime = 0; | |
| currentVoiceAudio.play().catch(() => {}); | |
| currentVoiceAudio.onended = () => { | |
| currentVoiceAudio = null; | |
| currentVoiceRoom = null; | |
| }; | |
| return; | |
| } | |
| // Fallback to TTS using existing text line for this room | |
| if ('speechSynthesis' in window && NARRATOR_LINES[roomName]) { | |
| const utter = new SpeechSynthesisUtterance(NARRATOR_LINES[roomName]); | |
| utter.rate = 1.0; | |
| utter.pitch = 1.0; | |
| utter.onend = () => { | |
| ttsSpeaking = false; | |
| currentVoiceRoom = null; | |
| }; | |
| ttsSpeaking = true; | |
| window.speechSynthesis.speak(utter); | |
| } | |
| } | |
| // Called whenever room changes (with up to 1s delay) | |
| function handleVoiceRoomChange(nextRoomName) { | |
| if (currentVoiceRoom === nextRoomName) return; | |
| // Leaving current area: stop any pending or playing narration | |
| stopVoiceNarration(); | |
| if (!nextRoomName || (!ROOM_VOICE_IDS[nextRoomName] && !NARRATOR_LINES[nextRoomName])) { | |
| return; | |
| } | |
| currentVoiceRoom = nextRoomName; | |
| // Start after up to 1 second; only if still in this room | |
| voiceDelayTimer = setTimeout(() => { | |
| if (currentVoiceRoom === nextRoomName) { | |
| playVoiceForRoom(nextRoomName); | |
| } | |
| }, 1000); | |
| } | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // ββ PLAYER / FPS CONTROLS | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function initControls() { | |
| controls = new PointerLockControls(camera, document.body); | |
| const lockScreen = document.getElementById('lock-screen'); | |
| const crosshair = document.getElementById('crosshair'); | |
| const hud = document.getElementById('hud'); | |
| lockScreen.addEventListener('click', () => { | |
| controls.lock(); | |
| }); | |
| controls.addEventListener('lock', () => { | |
| // Suppress FPS HUD reveal when not in FPS mode (e.g. during galaxy transition). | |
| if (typeof currentMode !== 'undefined' && currentMode !== 'fps') return; | |
| lockScreen.classList.add('hidden'); | |
| crosshair.classList.add('visible'); | |
| hud.classList.add('visible'); | |
| if (!audioCtx) initAudio(); | |
| else if (audioCtx.state === 'suspended') audioCtx.resume(); | |
| }); | |
| controls.addEventListener('unlock', () => { | |
| // Don't re-show FPS lock screen when we're in/entering galaxy mode. | |
| if (typeof currentMode !== 'undefined' && currentMode !== 'fps') return; | |
| if (!toolPanelOpen) { | |
| lockScreen.classList.remove('hidden'); | |
| } | |
| crosshair.classList.remove('visible'); | |
| }); | |
| // Keyboard (rebind-aware via window.__artemis_keymap) | |
| function _resolveAction(code) { | |
| const km = window.__artemis_keymap || {}; | |
| if (km[code]) return km[code]; | |
| switch (code) { | |
| case 'KeyW': case 'ArrowUp': return 'forward'; | |
| case 'KeyS': case 'ArrowDown': return 'backward'; | |
| case 'KeyA': case 'ArrowLeft': return 'left'; | |
| case 'KeyD': case 'ArrowRight': return 'right'; | |
| case 'ShiftLeft': case 'ShiftRight': return 'sprint'; | |
| case 'KeyE': return 'interact'; | |
| case 'Escape': return 'escape'; | |
| default: return null; | |
| } | |
| } | |
| document.addEventListener('keydown', (e) => { | |
| if (window.__artemis_consume_keys) return; | |
| const action = _resolveAction(e.code); | |
| switch (action) { | |
| case 'forward': moveState.forward = true; break; | |
| case 'backward': moveState.backward = true; break; | |
| case 'left': moveState.left = true; break; | |
| case 'right': moveState.right = true; break; | |
| case 'sprint': moveState.sprint = true; break; | |
| case 'interact': handleInteract(); break; | |
| case 'escape': | |
| if (toolPanelOpen) { closeToolPanel(); e.preventDefault(); } | |
| break; | |
| } | |
| }); | |
| document.addEventListener('keyup', (e) => { | |
| if (window.__artemis_consume_keys) return; | |
| const action = _resolveAction(e.code); | |
| switch (action) { | |
| case 'forward': moveState.forward = false; break; | |
| case 'backward': moveState.backward = false; break; | |
| case 'left': moveState.left = false; break; | |
| case 'right': moveState.right = false; break; | |
| case 'sprint': moveState.sprint = false; break; | |
| } | |
| }); | |
| scene.add(controls.object); | |
| } | |
| function testCollision(pos) { | |
| const r = CFG.PLAYER_RADIUS; | |
| const playerBox = new THREE.Box3( | |
| new THREE.Vector3(pos.x - r, 0, pos.z - r), | |
| new THREE.Vector3(pos.x + r, CFG.EYE_H + 0.2, pos.z + r) | |
| ); | |
| for (const box of collisionBoxes) { | |
| if (playerBox.intersectsBox(box)) return true; | |
| } | |
| return false; | |
| } | |
| function updateMovement(delta) { | |
| if (!controls.isLocked) return; | |
| const speed = CFG.MOVE_SPEED * (moveState.sprint ? CFG.SPRINT_MULT : 1.0); | |
| const damping = 8; | |
| // Deceleration | |
| velocity.x -= velocity.x * damping * delta; | |
| velocity.z -= velocity.z * damping * delta; | |
| direction.z = Number(moveState.forward) - Number(moveState.backward); | |
| direction.x = Number(moveState.right) - Number(moveState.left); | |
| direction.normalize(); | |
| if (moveState.forward || moveState.backward) velocity.z -= direction.z * speed * delta; | |
| if (moveState.left || moveState.right) velocity.x -= direction.x * speed * delta; | |
| // Get movement in world space | |
| const forward = new THREE.Vector3(); | |
| camera.getWorldDirection(forward); | |
| forward.y = 0; | |
| forward.normalize(); | |
| const right = new THREE.Vector3().crossVectors(forward, new THREE.Vector3(0,1,0)).normalize(); | |
| const moveX = right.x * (-velocity.x * delta) + forward.x * (-velocity.z * delta); | |
| const moveZ = right.z * (-velocity.x * delta) + forward.z * (-velocity.z * delta); | |
| // Try X movement | |
| const newPos = camera.position.clone(); | |
| newPos.x += moveX; | |
| if (!testCollision(newPos)) { | |
| camera.position.x = newPos.x; | |
| } | |
| // Try Z movement | |
| newPos.copy(camera.position); | |
| newPos.z += moveZ; | |
| if (!testCollision(newPos)) { | |
| camera.position.z = newPos.z; | |
| } | |
| // Footstep sounds | |
| const isMoving = moveState.forward || moveState.backward || moveState.left || moveState.right; | |
| if (isMoving && controls.isLocked) { | |
| const stepInterval = moveState.sprint ? SPRINT_STEP_INTERVAL : STEP_INTERVAL; | |
| stepTimer += delta; | |
| if (stepTimer >= stepInterval) { | |
| playFootstep(); | |
| stepTimer = 0; | |
| } | |
| } else { | |
| stepTimer = STEP_INTERVAL * 0.8; // Reset near trigger for immediate step on move | |
| } | |
| // Room detection | |
| detectRoom(); | |
| } | |
| function detectRoom() { | |
| const px = camera.position.x, pz = camera.position.z; | |
| let foundRoom = null; | |
| for (const zone of roomZones) { | |
| if (px >= zone.x1 && px <= zone.x2 && pz >= zone.z1 && pz <= zone.z2) { | |
| foundRoom = zone.name; | |
| if (currentRoom !== zone.name) { | |
| currentRoom = zone.name; | |
| document.getElementById('room-name').textContent = zone.name; | |
| checkRoomNarration(zone.name); | |
| handleVoiceRoomChange(zone.name); // NEW: trigger voice narrator | |
| } | |
| return; | |
| } | |
| } | |
| // Outside all defined zones | |
| if (currentRoom !== null) { | |
| currentRoom = null; | |
| document.getElementById('room-name').textContent = ''; | |
| handleVoiceRoomChange(null); // NEW: stop or reset voice when leaving | |
| } | |
| } | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // ββ INTERACTION SYSTEM | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| let aimedObject = null; | |
| const interactPrompt = document.getElementById('interact-prompt'); | |
| function updateAimDetection() { | |
| if (!controls.isLocked) { | |
| if (aimedObject) { aimedObject = null; interactPrompt.classList.remove('visible'); } | |
| return; | |
| } | |
| raycaster.setFromCamera(new THREE.Vector2(0,0), camera); | |
| const intersects = raycaster.intersectObjects(interactiveObjects, true); | |
| if (intersects.length > 0 && intersects[0].distance <= CFG.INTERACT_DIST) { | |
| const obj = intersects[0].object; | |
| if (obj.userData.toolId) { | |
| if (aimedObject !== obj) { | |
| aimedObject = obj; | |
| const tool = TOOLS[obj.userData.toolId]; | |
| interactPrompt.textContent = `Press E β ${tool ? tool.name : obj.userData.toolId}`; | |
| interactPrompt.classList.add('visible'); | |
| } | |
| } | |
| } else { | |
| if (aimedObject) { | |
| aimedObject = null; | |
| interactPrompt.classList.remove('visible'); | |
| } | |
| } | |
| } | |
| function handleInteract() { | |
| if (!aimedObject) return; | |
| const toolId = aimedObject.userData.toolId; | |
| if (toolId) { | |
| playInteractSound(); | |
| openToolPanel(toolId); | |
| } | |
| } | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // ββ TOOL PANEL (UI) | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function openToolPanel(toolId) { | |
| const tool = TOOLS[toolId]; | |
| if (!tool) return; | |
| toolPanelOpen = true; | |
| controls.unlock(); | |
| document.getElementById('tp-title').textContent = tool.name; | |
| const body = document.getElementById('tp-body'); | |
| body.innerHTML = ''; | |
| // Description | |
| const desc = document.createElement('div'); | |
| desc.className = 'tp-desc'; | |
| desc.textContent = tool.desc; | |
| body.appendChild(desc); | |
| // Tags | |
| if (tool.tags.length) { | |
| const section = document.createElement('div'); | |
| section.className = 'tp-section'; | |
| section.innerHTML = '<div class="tp-section-label">Stack</div>'; | |
| const tags = document.createElement('div'); | |
| tags.className = 'tp-tags'; | |
| tool.tags.forEach(t => { | |
| const tag = document.createElement('span'); | |
| tag.className = 'tp-tag'; | |
| tag.textContent = t; | |
| tags.appendChild(tag); | |
| }); | |
| section.appendChild(tags); | |
| body.appendChild(section); | |
| } | |
| // Status | |
| const status = document.createElement('div'); | |
| status.className = 'tp-status'; | |
| status.innerHTML = `<div class="tp-status-dot ${runningTools[toolId] ? 'running' : ''}"></div> | |
| <span>${runningTools[toolId] ? 'Running' : 'Stopped'}</span>`; | |
| body.appendChild(status); | |
| // Actions | |
| if (tool.hasLaunch) { | |
| const actions = document.createElement('div'); | |
| actions.className = 'tp-actions'; | |
| const launchBtn = document.createElement('button'); | |
| launchBtn.className = 'tp-btn launch'; | |
| launchBtn.textContent = 'βΆ Launch'; | |
| launchBtn.onclick = () => launchTool(toolId); | |
| actions.appendChild(launchBtn); | |
| const stopBtn = document.createElement('button'); | |
| stopBtn.className = 'tp-btn stop'; | |
| stopBtn.textContent = 'β Stop'; | |
| stopBtn.onclick = () => stopTool(toolId); | |
| actions.appendChild(stopBtn); | |
| const outputBtn = document.createElement('button'); | |
| outputBtn.className = 'tp-btn'; | |
| outputBtn.textContent = 'π₯ Output'; | |
| outputBtn.onclick = () => viewOutput(toolId); | |
| actions.appendChild(outputBtn); | |
| if (tool.url) { | |
| const webBtn = document.createElement('button'); | |
| webBtn.className = 'tp-btn'; | |
| webBtn.textContent = 'π Open'; | |
| webBtn.onclick = () => window.open(tool.url, '_blank'); | |
| actions.appendChild(webBtn); | |
| } | |
| body.appendChild(actions); | |
| } | |
| document.getElementById('tool-panel').classList.add('open'); | |
| document.body.style.cursor = 'default'; | |
| } | |
| function closeToolPanel() { | |
| toolPanelOpen = false; | |
| document.getElementById('tool-panel').classList.remove('open'); | |
| document.body.style.cursor = 'none'; | |
| // Re-lock after a short delay | |
| setTimeout(() => { | |
| if (!toolPanelOpen) controls.lock(); | |
| }, 100); | |
| } | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // ββ API INTEGRATION | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| async function checkBackend() { | |
| try { | |
| const resp = await fetch(`${API}/api/status`, { signal: AbortSignal.timeout(3000) }); | |
| if (resp.ok) { | |
| backendOk = true; | |
| document.getElementById('status-dot').classList.add('ok'); | |
| const data = await resp.json(); | |
| // Update running status | |
| if (data.tools) { | |
| for (const [id, info] of Object.entries(data.tools)) { | |
| runningTools[id] = info.running; | |
| } | |
| refreshMonitors(); | |
| } | |
| } | |
| } catch { | |
| backendOk = false; | |
| document.getElementById('status-dot').classList.remove('ok'); | |
| } | |
| } | |
| async function launchTool(id) { | |
| try { | |
| const r = await fetch(`${API}/api/launch`, { | |
| method: 'POST', headers: {'Content-Type':'application/json'}, | |
| body: JSON.stringify({tool_id: id}) | |
| }); | |
| if (r.ok) { | |
| runningTools[id] = true; | |
| showToast(`${TOOLS[id]?.name || id} launched`, 'success'); | |
| refreshMonitors(); | |
| openToolPanel(id); // Refresh panel | |
| } else { | |
| const d = await r.json().catch(() => ({})); | |
| showToast(d.detail || 'Launch failed', 'error'); | |
| } | |
| } catch(e) { showToast(`Error: ${e.message}`, 'error'); } | |
| } | |
| async function stopTool(id) { | |
| try { | |
| const r = await fetch(`${API}/api/stop`, { | |
| method: 'POST', headers: {'Content-Type':'application/json'}, | |
| body: JSON.stringify({tool_id: id}) | |
| }); | |
| if (r.ok) { | |
| runningTools[id] = false; | |
| showToast(`${TOOLS[id]?.name || id} stopped`, 'success'); | |
| refreshMonitors(); | |
| openToolPanel(id); | |
| } else { | |
| const d = await r.json().catch(() => ({})); | |
| showToast(d.detail || 'Stop failed', 'error'); | |
| } | |
| } catch(e) { showToast(`Error: ${e.message}`, 'error'); } | |
| } | |
| async function viewOutput(id) { | |
| const modal = document.getElementById('output-modal'); | |
| const content = document.getElementById('output-content'); | |
| document.getElementById('output-title').textContent = `${TOOLS[id]?.name || id} β Output`; | |
| content.textContent = 'Loading...'; | |
| modal.classList.add('open'); | |
| try { | |
| const r = await fetch(`${API}/api/output/${id}`); | |
| if (r.ok) { | |
| const d = await r.json(); | |
| content.textContent = d.output || '(no output)'; | |
| } else { | |
| content.textContent = '(failed to fetch output)'; | |
| } | |
| } catch(e) { | |
| content.textContent = `Error: ${e.message}`; | |
| } | |
| } | |
| function closeOutputModal() { | |
| document.getElementById('output-modal').classList.remove('open'); | |
| } | |
| function refreshMonitors() { | |
| for (const [toolId, mesh] of Object.entries(monitorMeshes)) { | |
| const newTex = monitorTexture(toolId); | |
| mesh.material.map = newTex; | |
| mesh.material.emissiveMap = newTex; | |
| mesh.material.needsUpdate = true; | |
| } | |
| } | |
| function showToast(msg, type='info') { | |
| const container = document.getElementById('toast-container'); | |
| const toast = document.createElement('div'); | |
| toast.className = `toast ${type}`; | |
| toast.textContent = msg; | |
| container.appendChild(toast); | |
| setTimeout(() => toast.remove(), 4000); | |
| } | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // ββ POST-PROCESSING | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function initPostProcessing() { | |
| composer = new EffectComposer(renderer); | |
| const renderPass = new RenderPass(scene, camera); | |
| composer.addPass(renderPass); | |
| const bloomPass = new UnrealBloomPass( | |
| new THREE.Vector2(window.innerWidth, window.innerHeight), | |
| 0.15, // strength (subtle) | |
| 0.8, // radius | |
| 0.85 // threshold (only bright things bloom) | |
| ); | |
| composer.addPass(bloomPass); | |
| } | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // ββ INIT | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function updateLoadBar(pct) { | |
| document.getElementById('load-fill').style.width = pct + '%'; | |
| } | |
| async function init() { | |
| clock = new THREE.Clock(); | |
| raycaster = new THREE.Raycaster(); | |
| // Scene | |
| scene = new THREE.Scene(); | |
| scene.background = new THREE.Color(0x1a1a1a); | |
| scene.fog = new THREE.Fog(0x1a1a1a, 15, 35); | |
| // Camera β far clip extended to 2000 for galaxy mode | |
| camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 0.05, 2000); | |
| camera.position.set(0, CFG.EYE_H, 0); | |
| // Renderer | |
| renderer = new THREE.WebGLRenderer({ antialias: true, powerPreference: 'high-performance' }); | |
| renderer.setSize(window.innerWidth, window.innerHeight); | |
| renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); | |
| renderer.toneMapping = THREE.ACESFilmicToneMapping; | |
| renderer.toneMappingExposure = 1.0; | |
| renderer.shadowMap.enabled = true; | |
| renderer.shadowMap.type = THREE.PCFSoftShadowMap; | |
| renderer.outputColorSpace = THREE.SRGBColorSpace; | |
| document.body.appendChild(renderer.domElement); | |
| updateLoadBar(10); | |
| // Materials | |
| buildMaterials(); | |
| updateLoadBar(25); | |
| // Build rooms | |
| for (const room of ROOMS) { | |
| buildRoom(room); | |
| } | |
| updateLoadBar(50); | |
| // Furniture and terminals | |
| for (const room of ROOMS) { | |
| buildRoomFurniture(room); | |
| } | |
| updateLoadBar(70); | |
| // Lighting | |
| buildLighting(); | |
| updateLoadBar(80); | |
| // Environment details | |
| buildEnvironmentDetails(); | |
| updateLoadBar(85); | |
| // Controls | |
| initControls(); | |
| updateLoadBar(90); | |
| // Post-processing | |
| initPostProcessing(); | |
| updateLoadBar(95); | |
| // Event listeners | |
| window.addEventListener('resize', onResize); | |
| // Backend check | |
| checkBackend(); | |
| setInterval(checkBackend, 8000); | |
| // Loading complete | |
| updateLoadBar(100); | |
| await new Promise(r => setTimeout(r, 400)); | |
| document.getElementById('loading').classList.add('done'); | |
| document.getElementById('lock-screen').classList.remove('hidden'); | |
| // Start the initial narration after player enters | |
| visitedRooms.clear(); | |
| // ββ BRAIN CLOUDS βββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| initBrainClouds(); | |
| buildNeuralHUD(); | |
| buildSelfModLog(); | |
| showDatasetUploadBtn(); | |
| // Render loop | |
| animate(); | |
| } | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // ββ ANIMATION LOOP | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function animate() { | |
| requestAnimationFrame(animate); | |
| const delta = Math.min(clock.getDelta(), 0.05); // Cap delta to prevent tunneling | |
| const time = performance.now() * 0.001; | |
| if (currentMode === 'fps') { | |
| updateMovement(delta); | |
| updateAimDetection(); | |
| } else if (currentMode === 'galaxy' || currentMode === 'transition') { | |
| if (orbitControls) orbitControls.update(); | |
| if (filePoints) { | |
| filePoints.rotation.y += 0.00012; | |
| if (filePoints.material.uniforms) { | |
| filePoints.material.uniforms.uTime.value = time; | |
| } | |
| } | |
| if (nebulaGroup) { | |
| nebulaGroup.rotation.y += 0.00012; | |
| nebulaGroup.children.forEach((sprite, i) => { | |
| sprite.material.opacity = 0.10 + 0.04 * Math.sin(time * 0.3 + i); | |
| }); | |
| } | |
| if (bgGalaxy) bgGalaxy.rotation.y += 0.0002; | |
| } | |
| // Cinematic mote burst (only active during transition) | |
| if (window.__fx_mote_update) window.__fx_mote_update(delta); | |
| // ββ BRAIN ANIMATION βββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| animateBrains(time); | |
| // Self-mod log refresh (every 60 frames) | |
| if (typeof brainAnimFrame !== 'undefined' && brainAnimFrame % 60 === 0) { | |
| renderSelfModLog(); | |
| } | |
| composer.render(); | |
| } | |
| function onResize() { | |
| camera.aspect = window.innerWidth / window.innerHeight; | |
| camera.updateProjectionMatrix(); | |
| renderer.setSize(window.innerWidth, window.innerHeight); | |
| composer.setSize(window.innerWidth, window.innerHeight); | |
| } | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // ββ GALAXY MODE ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| let currentMode = 'fps'; // 'fps' | 'galaxy' | 'transition' | |
| let roomGroup = null; // wraps all room/lab geometry | |
| let galaxyGroup = null; // wraps galaxy scene objects | |
| let orbitControls = null; | |
| let galaxyData = null; | |
| let galaxyLoaded = false; | |
| let galaxyLoadInFlight = false; | |
| let filePoints = null, bgGalaxy = null, starsObj = null, nebulaGroup = null; | |
| let galaxyCenter = new THREE.Vector3(); | |
| let galaxyRadius = 50; | |
| let spiralPositions = null; | |
| let baseFileSizes = null; | |
| let galaxyHoveredIndex = -1; | |
| let galaxyRaycaster = null; | |
| let galaxyMouse = null; | |
| let originalFog = null; | |
| let originalBackground = null; | |
| let savedFpsCamPos = new THREE.Vector3(); | |
| let savedFpsCamQuat = new THREE.Quaternion(); | |
| // Filter / search state | |
| let galaxyActiveSources = new Set(); | |
| let galaxyDensityPct = 100; | |
| let galaxyPointSize = 3; | |
| let galaxySearchMatches = new Set(); | |
| const GAL_HIGHLIGHT = new THREE.Color('#ffd88a'); | |
| const GAL_DIM_ALPHA = 0.08; | |
| const GAL_SRC_COLORS = { | |
| android: new THREE.Color('#4dff4d'), | |
| windows_c: new THREE.Color('#4d8dff'), | |
| windows_d: new THREE.Color('#c04dff'), | |
| wsl: new THREE.Color('#ff8c4d'), | |
| }; | |
| const GAL_WARM = new THREE.Color('#ffd080'); | |
| const GAL_COOL = new THREE.Color('#4060cc'); | |
| // ββ Galaxy textures (procedural) ββββββββββββββββββββββ | |
| function galMakeStarTex(size) { | |
| const c = document.createElement('canvas'); | |
| c.width = c.height = size; | |
| const ctx = c.getContext('2d'); | |
| const g = ctx.createRadialGradient(size/2, size/2, 0, size/2, size/2, size/2); | |
| g.addColorStop(0.0, 'rgba(255,255,255,0.9)'); | |
| g.addColorStop(0.08,'rgba(255,255,255,0.6)'); | |
| g.addColorStop(0.25,'rgba(200,220,255,0.15)'); | |
| g.addColorStop(0.5, 'rgba(100,140,255,0.03)'); | |
| g.addColorStop(1.0, 'rgba(0,0,0,0)'); | |
| ctx.fillStyle = g; ctx.fillRect(0, 0, size, size); | |
| const tex = new THREE.CanvasTexture(c); tex.needsUpdate = true; | |
| return tex; | |
| } | |
| function galMakeNebulaTex(hue, size) { | |
| const c = document.createElement('canvas'); | |
| c.width = c.height = size; | |
| const ctx = c.getContext('2d'); | |
| for (let k = 0; k < 5; k++) { | |
| const ox = size * (0.3 + Math.random() * 0.4); | |
| const oy = size * (0.3 + Math.random() * 0.4); | |
| const r = size * (0.2 + Math.random() * 0.3); | |
| const g = ctx.createRadialGradient(ox, oy, 0, ox, oy, r); | |
| const h = hue + (Math.random() - 0.5) * 30; | |
| g.addColorStop(0.0, `hsla(${h}, 80%, 60%, 0.12)`); | |
| g.addColorStop(0.4, `hsla(${h}, 70%, 40%, 0.06)`); | |
| g.addColorStop(1.0, `hsla(${h}, 60%, 20%, 0)`); | |
| ctx.fillStyle = g; ctx.fillRect(0, 0, size, size); | |
| } | |
| const tex = new THREE.CanvasTexture(c); tex.needsUpdate = true; | |
| return tex; | |
| } | |
| // ββ Background starfield (HDR-style) βββββββββββββββββββ | |
| function galCreateStars() { | |
| const count = 6000; | |
| const positions = new Float32Array(count * 3); | |
| const colors = new Float32Array(count * 3); | |
| const temps = [ | |
| new THREE.Color('#aaccff'), new THREE.Color('#ffffff'), | |
| new THREE.Color('#ffe8cc'), new THREE.Color('#ffccaa'), | |
| new THREE.Color('#ccddff'), | |
| ]; | |
| for (let i = 0; i < count; i++) { | |
| const theta = Math.random() * Math.PI * 2; | |
| const phi = Math.acos(2 * Math.random() - 1); | |
| const r = 250 + Math.random() * 500; | |
| positions[i*3] = r * Math.sin(phi) * Math.cos(theta); | |
| positions[i*3+1] = r * Math.sin(phi) * Math.sin(theta); | |
| positions[i*3+2] = r * Math.cos(phi); | |
| const c = temps[Math.floor(Math.random() * temps.length)]; | |
| colors[i*3] = c.r; colors[i*3+1] = c.g; colors[i*3+2] = c.b; | |
| } | |
| const geom = new THREE.BufferGeometry(); | |
| geom.setAttribute('position', new THREE.BufferAttribute(positions, 3)); | |
| geom.setAttribute('color', new THREE.BufferAttribute(colors, 3)); | |
| const mat = new THREE.PointsMaterial({ | |
| size: 1.0, sizeAttenuation: true, transparent: true, | |
| opacity: 0.85, vertexColors: true, depthWrite: false, | |
| blending: THREE.AdditiveBlending, map: galMakeStarTex(64), | |
| }); | |
| starsObj = new THREE.Points(geom, mat); | |
| galaxyGroup.add(starsObj); | |
| } | |
| // ββ Decorative background spiral galaxy ββββββββββββββββ | |
| function galCreateBgGalaxy() { | |
| const count = 30000; | |
| const positions = new Float32Array(count * 3); | |
| const colors = new Float32Array(count * 3); | |
| const arms = 4; | |
| const armSep = (2 * Math.PI) / arms; | |
| const radius = 110; | |
| const centerCol = new THREE.Color('#ffcc77'); | |
| const midCol = new THREE.Color('#cc88ff'); | |
| const armCol = new THREE.Color('#4466cc'); | |
| for (let i = 0; i < count; i++) { | |
| const dist = Math.pow(Math.random(), 1.8) * radius; | |
| const armIdx = Math.floor(Math.random() * arms); | |
| const windAngle = dist * 0.12; | |
| const spread = 0.8 + dist * 0.03; | |
| const angle = armIdx * armSep + windAngle + (Math.random() - 0.5) * spread * 0.5; | |
| const ySpread = Math.exp(-dist * 0.05) * 3; | |
| positions[i*3] = Math.cos(angle) * dist + (Math.random() - 0.5) * spread; | |
| positions[i*3+1] = (Math.random() - 0.5) * ySpread; | |
| positions[i*3+2] = Math.sin(angle) * dist + (Math.random() - 0.5) * spread; | |
| const t = dist / radius; | |
| const c = t < 0.4 | |
| ? new THREE.Color().lerpColors(centerCol, midCol, t / 0.4) | |
| : new THREE.Color().lerpColors(midCol, armCol, (t - 0.4) / 0.6); | |
| colors[i*3] = c.r; colors[i*3+1] = c.g; colors[i*3+2] = c.b; | |
| } | |
| const geom = new THREE.BufferGeometry(); | |
| geom.setAttribute('position', new THREE.BufferAttribute(positions, 3)); | |
| geom.setAttribute('color', new THREE.BufferAttribute(colors, 3)); | |
| const mat = new THREE.PointsMaterial({ | |
| size: 0.12, sizeAttenuation: true, transparent: true, | |
| opacity: 0.08, vertexColors: true, depthWrite: false, | |
| blending: THREE.AdditiveBlending, | |
| }); | |
| bgGalaxy = new THREE.Points(geom, mat); | |
| bgGalaxy.rotation.set(0.3, 0, -0.1); | |
| galaxyGroup.add(bgGalaxy); | |
| } | |
| // ββ Spiral remap (UMAP β galactic disk) βββββββββββββββ | |
| function galRemapToSpiral(files) { | |
| const n = files.length; | |
| let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity, minZ = Infinity, maxZ = -Infinity; | |
| for (const f of files) { | |
| if (f.x < minX) minX = f.x; if (f.x > maxX) maxX = f.x; | |
| if (f.y < minY) minY = f.y; if (f.y > maxY) maxY = f.y; | |
| if (f.z < minZ) minZ = f.z; if (f.z > maxZ) maxZ = f.z; | |
| } | |
| const rangeX = maxX - minX || 1; | |
| const rangeY = maxY - minY || 1; | |
| const rangeZ = maxZ - minZ || 1; | |
| const ARMS = 4, ARM_SEP = (2 * Math.PI) / ARMS; | |
| const WIND = 0.15, DISK_R = 65, DISK_H = 4; | |
| const positions = new Float32Array(n * 3); | |
| for (let i = 0; i < n; i++) { | |
| const f = files[i]; | |
| const nx = (f.x - minX) / rangeX; | |
| const ny = (f.y - minY) / rangeY; | |
| const nz = (f.z - minZ) / rangeZ; | |
| const cx = nx - 0.5, cz = nz - 0.5; | |
| const dist = Math.sqrt(cx*cx + cz*cz) * 2; | |
| const angle = Math.atan2(cz, cx); | |
| const armIdx = Math.round((angle / ARM_SEP) % ARMS); | |
| const armBase = armIdx * ARM_SEP; | |
| const spiralAngle = armBase + dist * WIND * 20 + (angle - armBase) * 0.3; | |
| const r = Math.pow(dist, 0.8) * DISK_R; | |
| const yScale = Math.exp(-dist * 2) * DISK_H; | |
| const yPos = (ny - 0.5) * yScale; | |
| positions[i*3] = Math.cos(spiralAngle) * r; | |
| positions[i*3+1] = yPos; | |
| positions[i*3+2] = Math.sin(spiralAngle) * r; | |
| } | |
| return positions; | |
| } | |
| // ββ Nebulae (drifting dust clouds) βββββββββββββββββββββ | |
| function galCreateNebulae(center, radius) { | |
| if (nebulaGroup) { galaxyGroup.remove(nebulaGroup); } | |
| nebulaGroup = new THREE.Group(); | |
| const hues = [240, 270, 200, 320, 180, 290, 210]; | |
| const count = 24; | |
| for (let i = 0; i < count; i++) { | |
| const angle = (i / count) * Math.PI * 2 + Math.random() * 0.5; | |
| const dist = radius * (0.2 + Math.random() * 0.7); | |
| const x = center.x + Math.cos(angle) * dist; | |
| const z = center.z + Math.sin(angle) * dist; | |
| const y = center.y + (Math.random() - 0.5) * 4; | |
| const size = 10 + Math.random() * 20; | |
| const hue = hues[i % hues.length]; | |
| const mat = new THREE.SpriteMaterial({ | |
| map: galMakeNebulaTex(hue, 256), transparent: true, | |
| opacity: 0.12 + Math.random() * 0.08, | |
| blending: THREE.AdditiveBlending, depthWrite: false, | |
| color: new THREE.Color(`hsl(${hue}, 60%, 50%)`), | |
| }); | |
| const sprite = new THREE.Sprite(mat); | |
| sprite.position.set(x, y, z); | |
| sprite.scale.set(size, size, 1); | |
| nebulaGroup.add(sprite); | |
| } | |
| galaxyGroup.add(nebulaGroup); | |
| } | |
| // ββ Build file point cloud βββββββββββββββββββββββββββββ | |
| function galBuildFilePoints(data) { | |
| if (filePoints) { | |
| galaxyGroup.remove(filePoints); | |
| filePoints.geometry.dispose(); | |
| filePoints.material.dispose(); | |
| } | |
| const files = data.files; | |
| const n = files.length; | |
| spiralPositions = galRemapToSpiral(files); | |
| let cx = 0, cy = 0, cz = 0; | |
| for (let i = 0; i < n; i++) { | |
| cx += spiralPositions[i*3]; cy += spiralPositions[i*3+1]; cz += spiralPositions[i*3+2]; | |
| } | |
| cx /= n; cy /= n; cz /= n; | |
| galaxyCenter.set(cx, cy, cz); | |
| let maxR = 0; | |
| for (let i = 0; i < n; i++) { | |
| const dx = spiralPositions[i*3] - cx; | |
| const dy = spiralPositions[i*3+1] - cy; | |
| const dz = spiralPositions[i*3+2] - cz; | |
| const r = Math.sqrt(dx*dx + dy*dy + dz*dz); | |
| if (r > maxR) maxR = r; | |
| } | |
| galaxyRadius = maxR || 50; | |
| const colors = new Float32Array(n * 3); | |
| const alphas = new Float32Array(n).fill(1.0); | |
| const sizes = new Float32Array(n); | |
| const twinklePhase = new Float32Array(n); | |
| baseFileSizes = new Float32Array(n); | |
| let maxLog = 0; | |
| for (let i = 0; i < n; i++) { | |
| const s = files[i].s || 1; | |
| baseFileSizes[i] = Math.log10(s + 1); | |
| if (baseFileSizes[i] > maxLog) maxLog = baseFileSizes[i]; | |
| } | |
| const tmpSrc = new THREE.Color(); | |
| const tmpGrad = new THREE.Color(); | |
| const tmpFinal = new THREE.Color(); | |
| const basePointSize = 3; | |
| for (let i = 0; i < n; i++) { | |
| const f = files[i]; | |
| const dx = spiralPositions[i*3] - cx; | |
| const dz = spiralPositions[i*3+2] - cz; | |
| const rFrac = Math.min(Math.sqrt(dx*dx + dz*dz) / galaxyRadius, 1); | |
| tmpSrc.copy(GAL_SRC_COLORS[f.src] || new THREE.Color('#ffffff')); | |
| tmpGrad.lerpColors(GAL_WARM, GAL_COOL, rFrac); | |
| tmpFinal.r = tmpSrc.r * 0.6 + tmpGrad.r * 0.4; | |
| tmpFinal.g = tmpSrc.g * 0.6 + tmpGrad.g * 0.4; | |
| tmpFinal.b = tmpSrc.b * 0.6 + tmpGrad.b * 0.4; | |
| colors[i*3] = tmpFinal.r; colors[i*3+1] = tmpFinal.g; colors[i*3+2] = tmpFinal.b; | |
| const sizeFactor = 0.5 + (baseFileSizes[i] / (maxLog || 1)) * 2.0; | |
| sizes[i] = basePointSize * sizeFactor; | |
| twinklePhase[i] = Math.random() * Math.PI * 2; | |
| } | |
| const geom = new THREE.BufferGeometry(); | |
| geom.setAttribute('position', new THREE.BufferAttribute(spiralPositions, 3)); | |
| geom.setAttribute('color', new THREE.BufferAttribute(colors, 3)); | |
| geom.setAttribute('alpha', new THREE.BufferAttribute(alphas, 1)); | |
| geom.setAttribute('aSize', new THREE.BufferAttribute(sizes, 1)); | |
| geom.setAttribute('phase', new THREE.BufferAttribute(twinklePhase, 1)); | |
| const mat = new THREE.ShaderMaterial({ | |
| uniforms: { uTime: { value: 0.0 }, uStarTex: { value: galMakeStarTex(128) } }, | |
| vertexShader: ` | |
| attribute float alpha; | |
| attribute float aSize; | |
| attribute float phase; | |
| uniform float uTime; | |
| varying vec3 vColor; | |
| varying float vAlpha; | |
| void main() { | |
| vColor = color; | |
| float twinkle = 0.85 + 0.15 * sin(uTime * 1.5 + phase * 6.2831); | |
| vAlpha = alpha * twinkle; | |
| vec4 mvPosition = modelViewMatrix * vec4(position, 1.0); | |
| gl_PointSize = aSize * (200.0 / -mvPosition.z); | |
| gl_PointSize = clamp(gl_PointSize, 1.0, 50.0); | |
| gl_Position = projectionMatrix * mvPosition; | |
| } | |
| `, | |
| fragmentShader: ` | |
| uniform sampler2D uStarTex; | |
| varying vec3 vColor; | |
| varying float vAlpha; | |
| void main() { | |
| vec4 texel = texture2D(uStarTex, gl_PointCoord); | |
| float brightness = texel.r; | |
| if (brightness < 0.01) discard; | |
| gl_FragColor = vec4(vColor * brightness, vAlpha * brightness); | |
| } | |
| `, | |
| transparent: true, depthWrite: false, vertexColors: true, | |
| blending: THREE.AdditiveBlending, | |
| }); | |
| filePoints = new THREE.Points(geom, mat); | |
| galaxyGroup.add(filePoints); | |
| galCreateNebulae(galaxyCenter, galaxyRadius); | |
| // Stash per-point metadata for visibility/search updates | |
| filePoints.userData.baseSizes = sizes.slice(); | |
| filePoints.userData.maxLog = maxLog || 1; | |
| } | |
| // ββ Recompute visibility (sources + density + search) β | |
| function galUpdateVisibility() { | |
| if (!galaxyData || !filePoints) return; | |
| const files = galaxyData.files; | |
| const n = files.length; | |
| const alphaAttr = filePoints.geometry.getAttribute('alpha'); | |
| const colorAttr = filePoints.geometry.getAttribute('color'); | |
| const sizeAttr = filePoints.geometry.getAttribute('aSize'); | |
| const hasSearch = galaxySearchMatches.size > 0; | |
| const maxLog = filePoints.userData.maxLog; | |
| const tmpSrc = new THREE.Color(); | |
| const tmpGrad = new THREE.Color(); | |
| const tmpFinal = new THREE.Color(); | |
| const cx = galaxyCenter.x, cz = galaxyCenter.z; | |
| let visCount = 0; | |
| for (let i = 0; i < n; i++) { | |
| const f = files[i]; | |
| const srcOn = galaxyActiveSources.has(f.src); | |
| const densityOk = (i % 100) < galaxyDensityPct; | |
| const visible = srcOn && densityOk; | |
| const sizeFactor = 0.5 + (baseFileSizes[i] / maxLog) * 2.0; | |
| const baseSize = galaxyPointSize * sizeFactor; | |
| if (hasSearch) { | |
| const isMatch = galaxySearchMatches.has(i); | |
| if (isMatch && srcOn) { | |
| colorAttr.setXYZ(i, GAL_HIGHLIGHT.r, GAL_HIGHLIGHT.g, GAL_HIGHLIGHT.b); | |
| sizeAttr.setX(i, baseSize * 2.5); | |
| alphaAttr.setX(i, 1.0); | |
| visCount++; | |
| } else { | |
| const dx = spiralPositions[i*3] - cx, dz = spiralPositions[i*3+2] - cz; | |
| const rFrac = Math.min(Math.sqrt(dx*dx + dz*dz) / galaxyRadius, 1); | |
| tmpSrc.copy(GAL_SRC_COLORS[f.src] || new THREE.Color('#ffffff')); | |
| tmpGrad.lerpColors(GAL_WARM, GAL_COOL, rFrac); | |
| tmpFinal.r = tmpSrc.r * 0.6 + tmpGrad.r * 0.4; | |
| tmpFinal.g = tmpSrc.g * 0.6 + tmpGrad.g * 0.4; | |
| tmpFinal.b = tmpSrc.b * 0.6 + tmpGrad.b * 0.4; | |
| colorAttr.setXYZ(i, tmpFinal.r, tmpFinal.g, tmpFinal.b); | |
| sizeAttr.setX(i, baseSize); | |
| alphaAttr.setX(i, visible ? GAL_DIM_ALPHA : 0.0); | |
| } | |
| } else { | |
| const dx = spiralPositions[i*3] - cx, dz = spiralPositions[i*3+2] - cz; | |
| const rFrac = Math.min(Math.sqrt(dx*dx + dz*dz) / galaxyRadius, 1); | |
| tmpSrc.copy(GAL_SRC_COLORS[f.src] || new THREE.Color('#ffffff')); | |
| tmpGrad.lerpColors(GAL_WARM, GAL_COOL, rFrac); | |
| tmpFinal.r = tmpSrc.r * 0.6 + tmpGrad.r * 0.4; | |
| tmpFinal.g = tmpSrc.g * 0.6 + tmpGrad.g * 0.4; | |
| tmpFinal.b = tmpSrc.b * 0.6 + tmpGrad.b * 0.4; | |
| colorAttr.setXYZ(i, tmpFinal.r, tmpFinal.g, tmpFinal.b); | |
| sizeAttr.setX(i, baseSize); | |
| alphaAttr.setX(i, visible ? 1.0 : 0.0); | |
| if (visible) visCount++; | |
| } | |
| } | |
| alphaAttr.needsUpdate = true; | |
| colorAttr.needsUpdate = true; | |
| sizeAttr.needsUpdate = true; | |
| const stats = document.getElementById('gs-stats'); | |
| if (stats && galaxyData) { | |
| stats.textContent = `${visCount.toLocaleString()} / ${n.toLocaleString()} points Β· ${galaxyData.meta.total_indexed.toLocaleString()} total indexed`; | |
| } | |
| } | |
| // ββ Populate sidebar source toggles from galaxy metadata β | |
| function galPopulateSourceToggles() { | |
| if (!galaxyData) return; | |
| // Reset sliders to authoritative defaults (Chromium may restore stale range values on reload) | |
| const densitySlider = document.getElementById('gs-density'); | |
| const sizeSlider = document.getElementById('gs-size'); | |
| densitySlider.value = 100; | |
| sizeSlider.value = 3; | |
| galaxyDensityPct = 100; | |
| galaxyPointSize = 3; | |
| document.getElementById('gs-density-val').textContent = '100%'; | |
| document.getElementById('gs-size-val').textContent = '3'; | |
| const root = document.getElementById('gs-source-toggles'); | |
| root.innerHTML = ''; | |
| galaxyActiveSources.clear(); | |
| for (const [key, meta] of Object.entries(galaxyData.meta.sources)) { | |
| galaxyActiveSources.add(key); | |
| const row = document.createElement('label'); | |
| row.className = 'gs-src-row'; | |
| row.innerHTML = | |
| `<input type="checkbox" class="gs-src-cb" data-src="${key}" checked>` + | |
| `<span class="gs-src-dot" style="background:${meta.color};color:${meta.color}"></span>` + | |
| `<span class="gs-src-label">${meta.label}</span>` + | |
| `<span class="gs-src-count">${meta.sampled_files.toLocaleString()}</span>`; | |
| root.appendChild(row); | |
| } | |
| root.querySelectorAll('.gs-src-cb').forEach((cb) => { | |
| cb.addEventListener('change', () => { | |
| galaxyActiveSources.clear(); | |
| root.querySelectorAll('.gs-src-cb').forEach((x) => { | |
| if (x.checked) galaxyActiveSources.add(x.dataset.src); | |
| }); | |
| galUpdateVisibility(); | |
| }); | |
| }); | |
| } | |
| // ββ Sidebar / slider / search handlers ββββββββββββββββ | |
| function toggleGalaxySidebar() { | |
| const sb = document.getElementById('galaxy-sidebar'); | |
| const btn = document.getElementById('galaxy-sidebar-toggle'); | |
| sb.classList.toggle('open'); | |
| btn.classList.toggle('shifted'); | |
| } | |
| function onGalaxyDensityChange(v) { | |
| galaxyDensityPct = parseInt(v, 10); | |
| document.getElementById('gs-density-val').textContent = galaxyDensityPct + '%'; | |
| galUpdateVisibility(); | |
| } | |
| function onGalaxySizeChange(v) { | |
| galaxyPointSize = parseFloat(v); | |
| document.getElementById('gs-size-val').textContent = v; | |
| galUpdateVisibility(); | |
| } | |
| function onGalaxySearch(q) { | |
| galaxySearchMatches.clear(); | |
| const resultsEl = document.getElementById('galaxy-search-results'); | |
| const query = (q || '').trim().toLowerCase(); | |
| if (!galaxyData || !query) { | |
| resultsEl.innerHTML = ''; | |
| galUpdateVisibility(); | |
| return; | |
| } | |
| const results = []; | |
| const files = galaxyData.files; | |
| for (let i = 0; i < files.length; i++) { | |
| const f = files[i]; | |
| if ((f.n && f.n.toLowerCase().includes(query)) | |
| || (f.p && f.p.toLowerCase().includes(query)) | |
| || (f.ext && f.ext.toLowerCase().includes(query))) { | |
| galaxySearchMatches.add(i); | |
| if (results.length < 50) results.push({ idx: i, ...f }); | |
| } | |
| } | |
| galUpdateVisibility(); | |
| const srcMeta = galaxyData.meta.sources; | |
| resultsEl.innerHTML = results.map((r) => { | |
| const col = (srcMeta[r.src] && srcMeta[r.src].color) || '#d4c5a9'; | |
| return `<div class="gs-result" data-idx="${r.idx}">` + | |
| `<span class="gs-res-dot" style="background:${col};color:${col}"></span>` + | |
| `<span class="gs-res-name">${escapeHtml(r.n || '')}</span>` + | |
| `<span class="gs-res-ext">${escapeHtml(r.ext || '')}</span></div>`; | |
| }).join(''); | |
| if (galaxySearchMatches.size > 50) { | |
| resultsEl.innerHTML += `<div class="gs-result-more">β¦and ${galaxySearchMatches.size - 50} more</div>`; | |
| } | |
| resultsEl.querySelectorAll('.gs-result').forEach((el) => { | |
| el.addEventListener('click', () => { | |
| const i = parseInt(el.dataset.idx, 10); | |
| galFlyToIndex(i); | |
| }); | |
| }); | |
| } | |
| function escapeHtml(s) { | |
| return s.replace(/[&<>"']/g, (c) => ({ '&':'&','<':'<','>':'>','"':'"',"'":''' }[c])); | |
| } | |
| function galFlyToIndex(i) { | |
| if (!spiralPositions) return; | |
| const target = new THREE.Vector3(spiralPositions[i*3], spiralPositions[i*3+1], spiralPositions[i*3+2]); | |
| if (filePoints) target.applyMatrix4(filePoints.matrixWorld); | |
| const dir = new THREE.Vector3().subVectors(camera.position, orbitControls.target).normalize(); | |
| const dest = target.clone().add(dir.multiplyScalar(10)); | |
| const startPos = camera.position.clone(); | |
| const startTarget = orbitControls.target.clone(); | |
| const duration = 1000; | |
| const t0 = performance.now(); | |
| (function fly() { | |
| const t = Math.min((performance.now() - t0) / duration, 1); | |
| const e = t < 0.5 ? 4*t*t*t : 1 - Math.pow(-2*t+2, 3)/2; | |
| camera.position.lerpVectors(startPos, dest, e); | |
| orbitControls.target.lerpVectors(startTarget, target, e); | |
| orbitControls.update(); | |
| if (t < 1) requestAnimationFrame(fly); | |
| })(); | |
| } | |
| window.toggleGalaxySidebar = toggleGalaxySidebar; | |
| window.onGalaxyDensityChange = onGalaxyDensityChange; | |
| window.onGalaxySizeChange = onGalaxySizeChange; | |
| window.onGalaxySearch = onGalaxySearch; | |
| // ββ Lazy-load galaxy JSON ββββββββββββββββββββββββββββββ | |
| async function loadGalaxyData() { | |
| if (galaxyLoaded || galaxyLoadInFlight) return; | |
| galaxyLoadInFlight = true; | |
| const status = document.getElementById('gl-status'); | |
| const fill = document.getElementById('gl-fill'); | |
| try { | |
| status.textContent = 'Fetching constellation dataβ¦'; | |
| fill.style.width = '20%'; | |
| const url = `${API}/galaxy_5k.json`; | |
| const resp = await fetch(url); | |
| if (!resp.ok) throw new Error(`HTTP ${resp.status}`); | |
| status.textContent = 'Parsing star catalogueβ¦'; | |
| fill.style.width = '55%'; | |
| galaxyData = await resp.json(); | |
| status.textContent = 'Rendering galactic diskβ¦'; | |
| fill.style.width = '80%'; | |
| galBuildFilePoints(galaxyData); | |
| galPopulateSourceToggles(); | |
| galUpdateVisibility(); | |
| fill.style.width = '100%'; | |
| galaxyLoaded = true; | |
| } catch (err) { | |
| console.error('[galaxy] load failed', err); | |
| status.textContent = `Load failed: ${err.message}`; | |
| throw err; | |
| } finally { | |
| galaxyLoadInFlight = false; | |
| } | |
| } | |
| // ββ Fade helper (traverse materials) βββββββββββββββββββ | |
| function fadeGroup(group, targetOpacity, duration, onDone) { | |
| const mats = []; | |
| group.traverse((obj) => { | |
| if (obj.material) { | |
| const arr = Array.isArray(obj.material) ? obj.material : [obj.material]; | |
| for (const m of arr) { | |
| if (!m.userData.__origOpacity) m.userData.__origOpacity = m.opacity ?? 1; | |
| if (!m.userData.__origTransparent) m.userData.__origTransparent = m.transparent; | |
| m.transparent = true; | |
| mats.push({ mat: m, from: m.opacity ?? 1, to: targetOpacity }); | |
| } | |
| } | |
| }); | |
| const start = performance.now(); | |
| function step() { | |
| const t = Math.min(1, (performance.now() - start) / duration); | |
| const e = t * t * (3 - 2 * t); // smoothstep | |
| for (const { mat, from, to } of mats) mat.opacity = from + (to - from) * e; | |
| if (t < 1) requestAnimationFrame(step); | |
| else if (onDone) onDone(); | |
| } | |
| step(); | |
| } | |
| // ββ Galaxy mouse handlers ββββββββββββββββββββββββββββββ | |
| function galOnPointerMove(e) { | |
| if (currentMode !== 'galaxy' || !filePoints || !galaxyData) return; | |
| galaxyMouse.x = (e.clientX / window.innerWidth) * 2 - 1; | |
| galaxyMouse.y = -(e.clientY / window.innerHeight) * 2 + 1; | |
| galaxyRaycaster.setFromCamera(galaxyMouse, camera); | |
| galaxyRaycaster.params.Points.threshold = 0.6; | |
| const hits = galaxyRaycaster.intersectObject(filePoints); | |
| const tip = document.getElementById('galaxy-tooltip'); | |
| if (hits.length > 0) { | |
| const idx = hits[0].index; | |
| const f = galaxyData.files[idx]; | |
| galaxyHoveredIndex = idx; | |
| tip.querySelector('.gt-name').textContent = f.n || '(unnamed)'; | |
| tip.querySelector('.gt-path').textContent = f.p || ''; | |
| tip.querySelector('.gt-meta').textContent = | |
| `${f.src || '?'} Β· ${formatBytes(f.s || 0)}`; | |
| tip.style.left = (e.clientX + 14) + 'px'; | |
| tip.style.top = (e.clientY + 14) + 'px'; | |
| tip.classList.add('visible'); | |
| document.body.style.cursor = 'pointer'; | |
| } else { | |
| galaxyHoveredIndex = -1; | |
| tip.classList.remove('visible'); | |
| document.body.style.cursor = 'default'; | |
| } | |
| } | |
| function galOnPointerClick() { | |
| if (currentMode !== 'galaxy' || galaxyHoveredIndex < 0) return; | |
| const i = galaxyHoveredIndex; | |
| const target = new THREE.Vector3(spiralPositions[i*3], spiralPositions[i*3+1], spiralPositions[i*3+2]); | |
| if (filePoints) target.applyMatrix4(filePoints.matrixWorld); | |
| const dir = new THREE.Vector3().subVectors(camera.position, orbitControls.target).normalize(); | |
| const dest = target.clone().add(dir.multiplyScalar(10)); | |
| const startPos = camera.position.clone(); | |
| const startTarget = orbitControls.target.clone(); | |
| const duration = 1000; | |
| const t0 = performance.now(); | |
| (function fly() { | |
| const t = Math.min((performance.now() - t0) / duration, 1); | |
| const e = t < 0.5 ? 4*t*t*t : 1 - Math.pow(-2*t+2, 3)/2; | |
| camera.position.lerpVectors(startPos, dest, e); | |
| orbitControls.target.lerpVectors(startTarget, target, e); | |
| orbitControls.update(); | |
| if (t < 1) requestAnimationFrame(fly); | |
| })(); | |
| } | |
| function formatBytes(n) { | |
| if (n < 1024) return `${n} B`; | |
| if (n < 1024*1024) return `${(n/1024).toFixed(1)} KB`; | |
| if (n < 1024*1024*1024) return `${(n/1024/1024).toFixed(1)} MB`; | |
| return `${(n/1024/1024/1024).toFixed(2)} GB`; | |
| } | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // ββ CINEMATIC TRANSITION HELPERS | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| let _fxMotes = null; // THREE.Points spawned during dissolve | |
| let _fxMoteUpdate = null; // per-frame updater registered into animate() | |
| function fxAudioSubBassDrop() { | |
| if (!audioCtx) return; | |
| // Sub-bass swell: descending sine 110Hz -> 22Hz over ~4s (slow, heavy) | |
| const osc = audioCtx.createOscillator(); | |
| const g = audioCtx.createGain(); | |
| osc.type = 'sine'; | |
| const t = audioCtx.currentTime; | |
| osc.frequency.setValueAtTime(110, t); | |
| osc.frequency.exponentialRampToValueAtTime(22, t + 4.0); | |
| g.gain.setValueAtTime(0.0, t); | |
| g.gain.linearRampToValueAtTime(0.6, t + 0.5); | |
| g.gain.linearRampToValueAtTime(0.45, t + 3.5); | |
| g.gain.exponentialRampToValueAtTime(0.001, t + 5.0); | |
| osc.connect(g); g.connect(masterGain || audioCtx.destination); | |
| osc.start(t); osc.stop(t + 5.2); | |
| // Pitch-shift the ambient hum: deep duck through the whole transition | |
| if (ambientGain) { | |
| const ag = ambientGain.gain; | |
| const v = ag.value; | |
| ag.cancelScheduledValues(t); | |
| ag.setValueAtTime(v, t); | |
| ag.linearRampToValueAtTime(v * 0.15, t + 1.5); | |
| ag.linearRampToValueAtTime(v, t + 6.0); | |
| } | |
| } | |
| function fxAudioSupernovaCrack() { | |
| if (!audioCtx) return; | |
| // White-noise burst with fast attack + long tail | |
| const dur = 1.8; | |
| const buf = audioCtx.createBuffer(1, audioCtx.sampleRate * dur, audioCtx.sampleRate); | |
| const d = buf.getChannelData(0); | |
| for (let i = 0; i < d.length; i++) d[i] = (Math.random() * 2 - 1); | |
| const src = audioCtx.createBufferSource(); src.buffer = buf; | |
| const filt = audioCtx.createBiquadFilter(); filt.type = 'bandpass'; | |
| filt.frequency.value = 2400; filt.Q.value = 0.6; | |
| const g = audioCtx.createGain(); | |
| const t = audioCtx.currentTime; | |
| g.gain.setValueAtTime(0, t); | |
| g.gain.linearRampToValueAtTime(0.18, t + 0.04); | |
| g.gain.exponentialRampToValueAtTime(0.0008, t + dur); | |
| src.connect(filt); filt.connect(g); g.connect(masterGain || audioCtx.destination); | |
| src.start(t); src.stop(t + dur); | |
| } | |
| function fxSpawnMoteBurst() { | |
| if (!roomGroup || !THREE) return; | |
| // Sample positions from inside roomGroup AABB; color by source palette. | |
| const box = new THREE.Box3().setFromObject(roomGroup); | |
| if (!isFinite(box.min.x)) return; | |
| const COUNT = 4500; | |
| const positions = new Float32Array(COUNT * 3); | |
| const colors = new Float32Array(COUNT * 3); | |
| const velocities = new Float32Array(COUNT * 3); | |
| // Source palette | |
| const palette = [ | |
| [0.30, 0.95, 0.45], // Android β emerald | |
| [0.65, 0.40, 0.95], // Windows β bruised purple | |
| [1.00, 0.55, 0.20], // WSL β tangerine | |
| [0.85, 0.78, 0.55], // Drywall warm cream (rare) | |
| ]; | |
| for (let i = 0; i < COUNT; i++) { | |
| const i3 = i * 3; | |
| positions[i3] = box.min.x + Math.random() * (box.max.x - box.min.x); | |
| positions[i3+1] = box.min.y + Math.random() * (box.max.y - box.min.y); | |
| positions[i3+2] = box.min.z + Math.random() * (box.max.z - box.min.z); | |
| const c = palette[(Math.random() * palette.length) | 0]; | |
| colors[i3] = c[0]; colors[i3+1] = c[1]; colors[i3+2] = c[2]; | |
| // Drift mostly upward + outward from center | |
| const cx = (box.max.x + box.min.x) * 0.5; | |
| const cz = (box.max.z + box.min.z) * 0.5; | |
| const dx = positions[i3] - cx; | |
| const dz = positions[i3+2] - cz; | |
| const len = Math.hypot(dx, dz) || 1; | |
| velocities[i3] = (dx / len) * (0.4 + Math.random() * 0.6); | |
| velocities[i3+1] = 0.8 + Math.random() * 1.6; | |
| velocities[i3+2] = (dz / len) * (0.4 + Math.random() * 0.6); | |
| } | |
| const geom = new THREE.BufferGeometry(); | |
| geom.setAttribute('position', new THREE.BufferAttribute(positions, 3)); | |
| geom.setAttribute('color', new THREE.BufferAttribute(colors, 3)); | |
| const mat = new THREE.PointsMaterial({ | |
| size: 0.10, | |
| vertexColors: true, | |
| transparent: true, | |
| opacity: 0.0, | |
| depthWrite: false, | |
| blending: THREE.AdditiveBlending, | |
| sizeAttenuation: true, | |
| }); | |
| _fxMotes = new THREE.Points(geom, mat); | |
| _fxMotes.userData.velocities = velocities; | |
| _fxMotes.userData.t0 = performance.now(); | |
| scene.add(_fxMotes); | |
| // Per-frame updater attached via window so animate() can pick it up | |
| _fxMoteUpdate = (delta) => { | |
| if (!_fxMotes) return; | |
| const elapsed = (performance.now() - _fxMotes.userData.t0) / 1000; | |
| const lifetime = 6.0; | |
| const k = Math.min(1, elapsed / lifetime); | |
| // Opacity rises slowly then sustains then falls | |
| const fadeIn = Math.min(1, k / 0.25); | |
| const fadeOut = (k > 0.7) ? (1 - (k - 0.7) / 0.3) : 1; | |
| mat.opacity = fadeIn * fadeOut * 0.95; | |
| // Drift positions β slower for cinematic | |
| const pos = geom.attributes.position.array; | |
| const vel = velocities; | |
| for (let i = 0; i < pos.length; i += 3) { | |
| pos[i] += vel[i] * delta * 0.45; | |
| pos[i+1] += vel[i+1] * delta * 0.45; | |
| pos[i+2] += vel[i+2] * delta * 0.45; | |
| } | |
| geom.attributes.position.needsUpdate = true; | |
| if (k >= 1) { | |
| scene.remove(_fxMotes); | |
| geom.dispose(); mat.dispose(); | |
| _fxMotes = null; | |
| _fxMoteUpdate = null; | |
| } | |
| }; | |
| window.__fx_mote_update = _fxMoteUpdate; | |
| } | |
| function fxBeginEnterCinematic() { | |
| // Button detonation | |
| const btn = document.getElementById('galaxy-btn'); | |
| const dot = btn?.querySelector('.g-dot'); | |
| if (btn) btn.classList.add('detonating'); | |
| if (dot) dot.classList.add('detonate'); | |
| // Crosshair quick fade | |
| document.getElementById('crosshair')?.classList.remove('visible'); | |
| // Screen shudder (apply to canvas wrapper if present, else body) | |
| const target = renderer?.domElement || document.body; | |
| target.classList.add('fx-shudder'); | |
| setTimeout(() => target.classList.remove('fx-shudder'), 420); | |
| // Static overlay fade in | |
| const stat = document.getElementById('fx-static'); | |
| if (stat) { stat.style.opacity = '0.55'; } | |
| // Vignette darkens the corners | |
| const vig = document.getElementById('fx-vignette'); | |
| if (vig) { vig.style.opacity = '1'; } | |
| // Audio: sub-bass + ambient duck | |
| fxAudioSubBassDrop(); | |
| // Spawn dissolve motes from room geometry | |
| fxSpawnMoteBurst(); | |
| // Schedule supernova flash + nebula bloom (slow, X-Men Dark Phoenix style) | |
| setTimeout(() => { | |
| const sh = document.getElementById('fx-shimmer'); | |
| if (sh) sh.style.opacity = '0.7'; | |
| }, 900); | |
| setTimeout(() => { | |
| const neb = document.getElementById('fx-nebula'); | |
| if (neb) { neb.style.transition = 'opacity 2.4s ease-out'; neb.style.opacity = '0.85'; } | |
| }, 1200); | |
| setTimeout(() => { | |
| const sn = document.getElementById('fx-supernova'); | |
| if (sn) { sn.style.transition = 'opacity 1.8s ease-out'; sn.style.opacity = '1'; } | |
| fxAudioSupernovaCrack(); | |
| }, 2400); | |
| setTimeout(() => { | |
| const sn = document.getElementById('fx-supernova'); | |
| if (sn) { sn.style.transition = 'opacity 2.6s ease-out'; sn.style.opacity = '0'; } | |
| }, 4400); | |
| } | |
| function fxEndEnterCinematic() { | |
| // Static + vignette fade out as we settle into galaxy view | |
| const stat = document.getElementById('fx-static'); | |
| if (stat) { stat.style.opacity = '0'; } | |
| const vig = document.getElementById('fx-vignette'); | |
| if (vig) { vig.style.opacity = '0'; } | |
| // Tone the corner nebula down (let the real 3D nebulae carry the look) | |
| const neb = document.getElementById('fx-nebula'); | |
| if (neb) { neb.style.transition = 'opacity 1.4s ease-out'; neb.style.opacity = '0.18'; } | |
| // Keep a light shimmer of space dust | |
| const sh = document.getElementById('fx-shimmer'); | |
| if (sh) { sh.style.transition = 'opacity 2.0s ease-out'; sh.style.opacity = '0.12'; } | |
| // Reset detonation classes for next time | |
| setTimeout(() => { | |
| const btn = document.getElementById('galaxy-btn'); | |
| const dot = btn?.querySelector('.g-dot'); | |
| btn?.classList.remove('detonating'); | |
| dot?.classList.remove('detonate'); | |
| }, 600); | |
| } | |
| function fxBeginExitCinematic() { | |
| const target = renderer?.domElement || document.body; | |
| target.classList.add('fx-shudder'); | |
| setTimeout(() => target.classList.remove('fx-shudder'), 420); | |
| // Brief static during reverse | |
| const stat = document.getElementById('fx-static'); | |
| if (stat) { stat.style.opacity = '0.35'; } | |
| // Nebula + shimmer fade out | |
| const neb = document.getElementById('fx-nebula'); | |
| if (neb) neb.style.opacity = '0'; | |
| const sh = document.getElementById('fx-shimmer'); | |
| if (sh) sh.style.opacity = '0'; | |
| } | |
| function fxEndExitCinematic() { | |
| const stat = document.getElementById('fx-static'); | |
| if (stat) { stat.style.opacity = '0'; } | |
| const vig = document.getElementById('fx-vignette'); | |
| if (vig) { vig.style.opacity = '0'; } | |
| } | |
| // ββ Enter / exit galaxy mode βββββββββββββββββββββββββββ | |
| async function enterGalaxyMode() { | |
| if (currentMode !== 'fps') return; | |
| currentMode = 'transition'; | |
| // Save FPS camera state | |
| savedFpsCamPos.copy(camera.position); | |
| savedFpsCamQuat.copy(camera.quaternion); | |
| // Unlock pointer, hide FPS HUD + button | |
| if (controls.isLocked) controls.unlock(); | |
| document.getElementById('crosshair').classList.remove('visible'); | |
| document.getElementById('hud').classList.remove('visible'); | |
| document.getElementById('lock-screen').classList.add('hidden'); | |
| // ββ CINEMATIC PHASE 0: button detonation, shudder, static, sub-bass, motes ββ | |
| fxBeginEnterCinematic(); | |
| // Save fog / bg once | |
| if (originalFog === null) { originalFog = scene.fog; originalBackground = scene.background; } | |
| // Lazy-load galaxy BEFORE starting the visual transition so the fade-in | |
| // shows the real galaxy rather than empty space. | |
| const loading = document.getElementById('galaxy-loading'); | |
| if (!galaxyLoaded) { | |
| loading.classList.add('visible'); | |
| try { | |
| await loadGalaxyData(); | |
| } catch (err) { | |
| loading.classList.remove('visible'); | |
| showToast('Galaxy load failed β ' + err.message, 'error'); | |
| document.getElementById('hud').classList.add('visible'); | |
| document.getElementById('galaxy-btn').classList.add('visible'); | |
| fxEndEnterCinematic(); | |
| currentMode = 'fps'; | |
| return; | |
| } | |
| loading.classList.remove('visible'); | |
| } | |
| // Prepare galaxy group: visible, transparent, starting at opacity 0 | |
| galaxyGroup.visible = true; | |
| galaxyGroup.traverse((obj) => { | |
| if (obj.material) { | |
| const arr = Array.isArray(obj.material) ? obj.material : [obj.material]; | |
| for (const m of arr) { | |
| if (m.userData.__origOpacity === undefined) m.userData.__origOpacity = m.opacity ?? 1; | |
| m.transparent = true; | |
| m.opacity = 0; | |
| } | |
| } | |
| }); | |
| // ββ Precompile galaxy shaders BEFORE starting the crossfade. | |
| // Without this, the first frame the galaxy is visible causes a | |
| // multi-hundred-ms hitch while stars/bgGalaxy/filePoints/nebulae | |
| // shaders compile. renderer.compile() forces that work upfront. | |
| try { | |
| // Move camera temporarily to where it'll end up so precompile covers the | |
| // frustum the transition will render through. | |
| const precompileCamPos = new THREE.Vector3( | |
| galaxyCenter.x + galaxyRadius * 0.8, | |
| galaxyCenter.y + galaxyRadius * 0.56, | |
| galaxyCenter.z + galaxyRadius * 0.8, | |
| ); | |
| const savedPos = camera.position.clone(); | |
| const savedQuat = camera.quaternion.clone(); | |
| camera.position.copy(precompileCamPos); | |
| camera.lookAt(galaxyCenter); | |
| renderer.compile(scene, camera); | |
| // Prime with one invisible render to warm pipeline state. | |
| composer.render(); | |
| // Restore for the actual transition start | |
| camera.position.copy(savedPos); | |
| camera.quaternion.copy(savedQuat); | |
| // Yield so the browser can post-process compilations before the loop starts | |
| await new Promise((r) => requestAnimationFrame(() => requestAnimationFrame(r))); | |
| } catch (e) { | |
| console.warn('[galaxy] precompile failed (non-fatal)', e); | |
| } | |
| // Crossfade scene background from room tone to deep space | |
| const bgFromHex = (scene.background && scene.background.isColor) | |
| ? scene.background.getHex() : 0x1a1a1a; | |
| const bgTo = new THREE.Color('#050508'); | |
| const bgAnim = new THREE.Color(bgFromHex); | |
| scene.background = bgAnim; | |
| // Fog also eases out | |
| if (scene.fog && scene.fog.color) scene.fog.far = scene.fog.far; // keep reference | |
| const fogOriginalFar = scene.fog ? scene.fog.far : null; | |
| // Camera path: glide from current FPS eye position out to orbital view | |
| const camStart = camera.position.clone(); | |
| const camStartQuat = camera.quaternion.clone(); | |
| const dist = galaxyRadius * 1.6; | |
| const camEnd = new THREE.Vector3( | |
| galaxyCenter.x + dist * 0.5, | |
| galaxyCenter.y + dist * 0.35, | |
| galaxyCenter.z + dist * 0.5, | |
| ); | |
| // Compute end quaternion (camera looking at galaxy center) | |
| const endCam = camera.clone(); | |
| endCam.position.copy(camEnd); | |
| endCam.lookAt(galaxyCenter); | |
| const camEndQuat = endCam.quaternion.clone(); | |
| // Cache room materials + original opacities for crossfade | |
| const roomMats = []; | |
| roomGroup.traverse((obj) => { | |
| if (obj.material) { | |
| const arr = Array.isArray(obj.material) ? obj.material : [obj.material]; | |
| for (const m of arr) { | |
| if (m.userData.__origOpacity === undefined) m.userData.__origOpacity = m.opacity ?? 1; | |
| m.transparent = true; | |
| roomMats.push({ mat: m, from: m.opacity }); | |
| } | |
| } | |
| }); | |
| const galMats = []; | |
| galaxyGroup.traverse((obj) => { | |
| if (obj.material) { | |
| const arr = Array.isArray(obj.material) ? obj.material : [obj.material]; | |
| for (const m of arr) { | |
| galMats.push({ mat: m, to: m.userData.__origOpacity ?? 1 }); | |
| } | |
| } | |
| }); | |
| // Crossfade loop β 6.0s total (slow, cinematic), smoothstep eased | |
| const DURATION = 6000; | |
| const t0 = performance.now(); | |
| await new Promise((resolve) => { | |
| function step() { | |
| const raw = Math.min(1, (performance.now() - t0) / DURATION); | |
| const t = raw * raw * (3 - 2 * raw); // smoothstep | |
| // Fade rooms out | |
| for (const r of roomMats) r.mat.opacity = r.from * (1 - t); | |
| // Fade galaxy in (slightly delayed curve so it reveals gradually) | |
| const galT = Math.max(0, (t - 0.15) / 0.85); | |
| const galEased = galT * galT * (3 - 2 * galT); | |
| for (const g of galMats) g.mat.opacity = g.to * galEased; | |
| // Camera glide (slerp for rotation) | |
| camera.position.lerpVectors(camStart, camEnd, t); | |
| camera.quaternion.copy(camStartQuat).slerp(camEndQuat, t); | |
| // Background crossfade | |
| bgAnim.copy(new THREE.Color(bgFromHex)).lerp(bgTo, t); | |
| // Fog easing β push far plane outward then kill it | |
| if (scene.fog && fogOriginalFar !== null) { | |
| scene.fog.far = fogOriginalFar + (800 - fogOriginalFar) * t; | |
| } | |
| if (raw < 1) requestAnimationFrame(step); | |
| else resolve(); | |
| } | |
| step(); | |
| }); | |
| // Finalize | |
| roomGroup.visible = false; | |
| scene.fog = null; | |
| scene.background = new THREE.Color('#050508'); | |
| // Activate orbit controls | |
| if (!orbitControls) { | |
| orbitControls = new OrbitControls(camera, renderer.domElement); | |
| orbitControls.enableDamping = true; | |
| orbitControls.dampingFactor = 0.06; | |
| orbitControls.minDistance = 5; | |
| orbitControls.maxDistance = 400; | |
| } | |
| orbitControls.target.copy(galaxyCenter); | |
| orbitControls.enabled = true; | |
| orbitControls.update(); | |
| document.getElementById('galaxy-sidebar-toggle').classList.add('visible'); | |
| document.getElementById('galaxy-hud').classList.add('visible'); | |
| fxEndEnterCinematic(); | |
| currentMode = 'galaxy'; | |
| } | |
| async function exitGalaxyMode() { | |
| if (currentMode !== 'galaxy') return; | |
| currentMode = 'transition'; | |
| document.getElementById('galaxy-hud').classList.remove('visible'); | |
| document.getElementById('galaxy-sidebar').classList.remove('open'); | |
| document.getElementById('galaxy-sidebar-toggle').classList.remove('visible', 'shifted'); | |
| document.getElementById('galaxy-tooltip').classList.remove('visible'); | |
| if (orbitControls) orbitControls.enabled = false; | |
| fxBeginExitCinematic(); | |
| // Reveal rooms again at 0 opacity ready to fade in | |
| roomGroup.visible = true; | |
| const roomMats = []; | |
| roomGroup.traverse((obj) => { | |
| if (obj.material) { | |
| const arr = Array.isArray(obj.material) ? obj.material : [obj.material]; | |
| for (const m of arr) { | |
| m.transparent = true; | |
| m.opacity = 0; | |
| roomMats.push({ mat: m, to: m.userData.__origOpacity ?? 1 }); | |
| } | |
| } | |
| }); | |
| const galMats = []; | |
| galaxyGroup.traverse((obj) => { | |
| if (obj.material) { | |
| const arr = Array.isArray(obj.material) ? obj.material : [obj.material]; | |
| for (const m of arr) galMats.push({ mat: m, from: m.opacity ?? 1 }); | |
| } | |
| }); | |
| // Background crossfade back | |
| const bgFrom = scene.background ? scene.background.getHex() : 0x050508; | |
| const bgTo = (originalBackground && originalBackground.isColor) | |
| ? originalBackground.getHex() : 0x1a1a1a; | |
| const bgAnim = new THREE.Color(bgFrom); | |
| scene.background = bgAnim; | |
| // Camera glide back to saved FPS pose | |
| const camStart = camera.position.clone(); | |
| const camStartQuat = camera.quaternion.clone(); | |
| const DURATION = 4000; | |
| const t0 = performance.now(); | |
| await new Promise((resolve) => { | |
| function step() { | |
| const raw = Math.min(1, (performance.now() - t0) / DURATION); | |
| const t = raw * raw * (3 - 2 * raw); | |
| // Galaxy fades first, rooms follow | |
| const galT = Math.min(1, t / 0.7); | |
| const galEased = galT * galT * (3 - 2 * galT); | |
| for (const g of galMats) g.mat.opacity = g.from * (1 - galEased); | |
| const roomT = Math.max(0, (t - 0.3) / 0.7); | |
| const roomEased = roomT * roomT * (3 - 2 * roomT); | |
| for (const r of roomMats) r.mat.opacity = r.to * roomEased; | |
| camera.position.lerpVectors(camStart, savedFpsCamPos, t); | |
| camera.quaternion.copy(camStartQuat).slerp(savedFpsCamQuat, t); | |
| bgAnim.copy(new THREE.Color(bgFrom)).lerp(new THREE.Color(bgTo), t); | |
| if (raw < 1) requestAnimationFrame(step); | |
| else resolve(); | |
| } | |
| step(); | |
| }); | |
| // Finalize | |
| galaxyGroup.visible = false; | |
| scene.fog = originalFog; | |
| scene.background = originalBackground; | |
| camera.position.copy(savedFpsCamPos); | |
| camera.quaternion.copy(savedFpsCamQuat); | |
| document.getElementById('hud').classList.add('visible'); | |
| document.getElementById('galaxy-btn').classList.add('visible'); | |
| document.getElementById('lock-screen').classList.remove('hidden'); | |
| fxEndExitCinematic(); | |
| currentMode = 'fps'; | |
| } | |
| // Expose so HTML onclick can reach it | |
| window.enterGalaxyMode = enterGalaxyMode; | |
| window.exitGalaxyMode = exitGalaxyMode; | |
| // ββ Wrap rooms into roomGroup + init galaxy scaffolding β | |
| function initGalaxyScaffolding() { | |
| roomGroup = new THREE.Group(); | |
| roomGroup.name = 'roomGroup'; | |
| galaxyGroup = new THREE.Group(); | |
| galaxyGroup.name = 'galaxyGroup'; | |
| galaxyGroup.visible = false; | |
| // Move all current scene children (except camera + PointerLockControls rig) into roomGroup | |
| const children = [...scene.children]; | |
| for (const c of children) { | |
| if (c.isCamera) continue; | |
| if (controls && c === controls.object) continue; | |
| scene.remove(c); | |
| roomGroup.add(c); | |
| } | |
| scene.add(roomGroup); | |
| scene.add(galaxyGroup); | |
| // Raycaster + mouse for galaxy | |
| galaxyRaycaster = new THREE.Raycaster(); | |
| galaxyMouse = new THREE.Vector2(-999, -999); | |
| // Precreate background stars + bg galaxy once (lightweight) | |
| galCreateStars(); | |
| galCreateBgGalaxy(); | |
| // Pointer events for galaxy mode | |
| window.addEventListener('mousemove', galOnPointerMove); | |
| window.addEventListener('click', galOnPointerClick); | |
| // ESC to exit galaxy mode (or close sidebar first) | |
| window.addEventListener('keydown', (e) => { | |
| if (e.code === 'Escape' && currentMode === 'galaxy') { | |
| const sb = document.getElementById('galaxy-sidebar'); | |
| if (sb.classList.contains('open')) { | |
| e.preventDefault(); | |
| sb.classList.remove('open'); | |
| document.getElementById('galaxy-sidebar-toggle').classList.remove('shifted'); | |
| return; | |
| } | |
| e.preventDefault(); | |
| exitGalaxyMode(); | |
| } | |
| }); | |
| // F key β toggle Galaxy mode (works from FPS or galaxy). | |
| // Ignored when typing in an input (e.g. galaxy search field). | |
| window.addEventListener('keydown', (e) => { | |
| if (e.code !== 'KeyF') return; | |
| const tag = (e.target && e.target.tagName) || ''; | |
| if (tag === 'INPUT' || tag === 'TEXTAREA' || (e.target && e.target.isContentEditable)) return; | |
| if (currentMode === 'fps') { | |
| e.preventDefault(); | |
| enterGalaxyMode(); | |
| } else if (currentMode === 'galaxy') { | |
| e.preventDefault(); | |
| exitGalaxyMode(); | |
| } | |
| }); | |
| // Reveal the button when FPS mode is active (once pointer locks) | |
| if (controls) { | |
| controls.addEventListener('lock', () => { | |
| if (currentMode === 'fps') { | |
| document.getElementById('galaxy-btn').classList.add('visible'); | |
| } | |
| }); | |
| controls.addEventListener('unlock', () => { | |
| // When pointer unlocks (e.g. opening tool panel) the button hides via lock-screen visibility | |
| if (currentMode === 'fps' && !toolPanelOpen) { | |
| document.getElementById('galaxy-btn').classList.remove('visible'); | |
| } | |
| }); | |
| } | |
| } | |
| // Start | |
| (async () => { | |
| await init(); | |
| initGalaxyScaffolding(); | |
| })(); | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // ββ GAME MENU (M key) | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| const GM_KEY = 'artemis.settings.v1'; | |
| const GM_DEFAULT_KEYS = { | |
| forward: 'KeyW', | |
| backward: 'KeyS', | |
| left: 'KeyA', | |
| right: 'KeyD', | |
| sprint: 'ShiftLeft', | |
| interact: 'KeyE', | |
| }; | |
| const GM_DEFAULTS = { | |
| master: 1.0, | |
| mute: false, | |
| brightness: 1.0, | |
| fov: 75, | |
| walkSpeed: CFG.MOVE_SPEED, | |
| sprintMult: CFG.SPRINT_MULT, | |
| mouseSens: 1.0, | |
| keys: { ...GM_DEFAULT_KEYS }, | |
| }; | |
| let _gm_listening_for = null; | |
| function _gm_loadRaw() { | |
| try { | |
| const raw = localStorage.getItem(GM_KEY); | |
| if (!raw) return null; | |
| const obj = JSON.parse(raw); | |
| return { ...GM_DEFAULTS, ...obj, keys: { ...GM_DEFAULT_KEYS, ...(obj.keys || {}) } }; | |
| } catch(e) { return null; } | |
| } | |
| function _gm_buildKeymap(keys) { | |
| const map = {}; | |
| for (const action of Object.keys(keys)) { | |
| const code = keys[action]; | |
| if (code) map[code] = action; | |
| } | |
| window.__artemis_keymap = map; | |
| } | |
| function _gm_applyToRuntime(s) { | |
| CFG.MOVE_SPEED = Number(s.walkSpeed); | |
| CFG.SPRINT_MULT = Number(s.sprintMult); | |
| CFG.MOUSE_SENS = Number(s.mouseSens); | |
| if (typeof camera !== 'undefined') { | |
| camera.fov = Number(s.fov); | |
| camera.updateProjectionMatrix(); | |
| } | |
| if (typeof controls !== 'undefined' && controls && 'pointerSpeed' in controls) { | |
| controls.pointerSpeed = Number(s.mouseSens); | |
| } | |
| if (typeof renderer !== 'undefined' && renderer) { | |
| renderer.toneMappingExposure = Number(s.brightness); | |
| } | |
| if (window.__artemis_master_gain) { | |
| window.__artemis_master_gain.gain.value = s.mute ? 0 : Number(s.master); | |
| } | |
| document.querySelectorAll('audio').forEach(a => { | |
| a.volume = s.mute ? 0 : Math.min(1, Number(s.master)); | |
| }); | |
| _gm_buildKeymap(s.keys); | |
| } | |
| function _gm_renderKeybinds(keys) { | |
| const host = document.getElementById('gm-keybinds'); | |
| if (!host) return; | |
| host.innerHTML = ''; | |
| const labels = { | |
| forward: 'Forward', backward: 'Backward', left: 'Strafe Left', | |
| right: 'Strafe Right', sprint: 'Sprint', interact: 'Interact', | |
| }; | |
| for (const action of Object.keys(GM_DEFAULT_KEYS)) { | |
| const row = document.createElement('div'); | |
| row.className = 'gm-keybind-row'; | |
| row.innerHTML = `<span>${labels[action] || action}</span>`; | |
| const btn = document.createElement('button'); | |
| btn.className = 'gm-key-btn'; | |
| btn.dataset.action = action; | |
| btn.textContent = (keys[action] || 'β').replace(/^Key/, ''); | |
| btn.addEventListener('click', () => _gm_beginRebind(action, btn)); | |
| row.appendChild(btn); | |
| host.appendChild(row); | |
| } | |
| } | |
| function _gm_beginRebind(action, btn) { | |
| document.querySelectorAll('.gm-key-btn.listening').forEach(b => { | |
| b.classList.remove('listening'); | |
| const a = b.dataset.action; | |
| const cur = (_gm_currentSettings().keys[a] || 'β').replace(/^Key/, ''); | |
| b.textContent = cur; | |
| }); | |
| _gm_listening_for = action; | |
| window.__artemis_consume_keys = true; | |
| btn.classList.add('listening'); | |
| btn.textContent = 'Press a keyβ¦'; | |
| } | |
| function _gm_currentSettings() { | |
| return _gm_loadRaw() || { ...GM_DEFAULTS, keys: { ...GM_DEFAULT_KEYS } }; | |
| } | |
| function _gm_readUI() { | |
| const s = _gm_currentSettings(); | |
| const num = (id, def) => Number(document.getElementById(id)?.value ?? def); | |
| s.master = num('gm-audio-master', s.master); | |
| s.mute = document.getElementById('gm-audio-mute')?.dataset.muted === '1'; | |
| s.brightness = num('gm-display-bright', s.brightness); | |
| s.fov = num('gm-display-fov', s.fov); | |
| s.walkSpeed = num('gm-walk-speed', s.walkSpeed); | |
| s.sprintMult = num('gm-sprint-mult', s.sprintMult); | |
| s.mouseSens = num('gm-mouse-sens', s.mouseSens); | |
| return s; | |
| } | |
| function _gm_writeUI(s) { | |
| const set = (id, val) => { const el = document.getElementById(id); if (el) el.value = val; }; | |
| set('gm-audio-master', s.master); | |
| set('gm-display-bright', s.brightness); | |
| set('gm-display-fov', s.fov); | |
| set('gm-walk-speed', s.walkSpeed); | |
| set('gm-sprint-mult', s.sprintMult); | |
| set('gm-mouse-sens', s.mouseSens); | |
| const mute = document.getElementById('gm-audio-mute'); | |
| if (mute) { | |
| mute.dataset.muted = s.mute ? '1' : '0'; | |
| mute.textContent = s.mute ? 'Unmute' : 'Mute'; | |
| } | |
| _gm_renderKeybinds(s.keys); | |
| } | |
| function loadSettings() { | |
| const s = _gm_loadRaw() || { ...GM_DEFAULTS, keys: { ...GM_DEFAULT_KEYS } }; | |
| _gm_writeUI(s); | |
| _gm_applyToRuntime(s); | |
| return s; | |
| } | |
| function saveSettings() { | |
| const s = _gm_readUI(); | |
| localStorage.setItem(GM_KEY, JSON.stringify(s)); | |
| _gm_applyToRuntime(s); | |
| showToast('Settings saved', 'success'); | |
| } | |
| function resetSettings() { | |
| localStorage.removeItem(GM_KEY); | |
| loadSettings(); | |
| showToast('Settings reset', 'success'); | |
| } | |
| function showMenu() { | |
| const menu = document.getElementById('game-menu'); | |
| if (!menu) return; | |
| if (document.pointerLockElement === document.body) { | |
| try { document.exitPointerLock(); } catch(e) {} | |
| } | |
| menu.classList.add('open'); | |
| menu.setAttribute('aria-hidden', 'false'); | |
| } | |
| function hideMenu() { | |
| const menu = document.getElementById('game-menu'); | |
| if (!menu) return; | |
| menu.classList.remove('open'); | |
| menu.setAttribute('aria-hidden', 'true'); | |
| saveSettings(); | |
| _gm_listening_for = null; | |
| window.__artemis_consume_keys = false; | |
| } | |
| function toggleMenu() { | |
| const menu = document.getElementById('game-menu'); | |
| if (menu && menu.classList.contains('open')) hideMenu(); | |
| else showMenu(); | |
| } | |
| document.addEventListener('keydown', (e) => { | |
| if (_gm_listening_for) { | |
| e.preventDefault(); | |
| e.stopPropagation(); | |
| if (e.code === 'Escape') { | |
| _gm_listening_for = null; | |
| window.__artemis_consume_keys = false; | |
| _gm_renderKeybinds(_gm_currentSettings().keys); | |
| return; | |
| } | |
| const action = _gm_listening_for; | |
| const s = _gm_readUI(); | |
| s.keys = { ...s.keys, [action]: e.code }; | |
| for (const a of Object.keys(s.keys)) { | |
| if (a !== action && s.keys[a] === e.code) s.keys[a] = ''; | |
| } | |
| localStorage.setItem(GM_KEY, JSON.stringify(s)); | |
| _gm_buildKeymap(s.keys); | |
| _gm_renderKeybinds(s.keys); | |
| _gm_listening_for = null; | |
| window.__artemis_consume_keys = false; | |
| return; | |
| } | |
| if (e.code === 'KeyM' && !e.repeat) { | |
| const tag = (document.activeElement && document.activeElement.tagName) || ''; | |
| if (tag === 'INPUT' || tag === 'TEXTAREA') return; | |
| e.preventDefault(); | |
| toggleMenu(); | |
| } | |
| }, true); | |
| (function attachMenuHandlers() { | |
| const $ = (id) => document.getElementById(id); | |
| const close = $('gm-close'); | |
| const save = $('gm-save'); | |
| const reset = $('gm-reset'); | |
| const mute = $('gm-audio-mute'); | |
| const master = $('gm-audio-master'); | |
| const bright = $('gm-display-bright'); | |
| const fov = $('gm-display-fov'); | |
| const walk = $('gm-walk-speed'); | |
| const sprint = $('gm-sprint-mult'); | |
| const sens = $('gm-mouse-sens'); | |
| const rebindReset = $('gm-rebind-reset'); | |
| if (close) close.addEventListener('click', hideMenu); | |
| if (save) save.addEventListener('click', saveSettings); | |
| if (reset) reset.addEventListener('click', resetSettings); | |
| if (rebindReset) rebindReset.addEventListener('click', () => { | |
| const s = _gm_currentSettings(); | |
| s.keys = { ...GM_DEFAULT_KEYS }; | |
| localStorage.setItem(GM_KEY, JSON.stringify(s)); | |
| _gm_buildKeymap(s.keys); | |
| _gm_renderKeybinds(s.keys); | |
| showToast('Bindings reset', 'success'); | |
| }); | |
| if (mute) mute.addEventListener('click', () => { | |
| const now = mute.dataset.muted === '1'; | |
| mute.dataset.muted = now ? '0' : '1'; | |
| mute.textContent = now ? 'Mute' : 'Unmute'; | |
| _gm_applyToRuntime(_gm_readUI()); | |
| }); | |
| for (const el of [master, bright, fov, walk, sprint, sens]) { | |
| if (el) el.addEventListener('input', () => _gm_applyToRuntime(_gm_readUI())); | |
| } | |
| loadSettings(); | |
| })(); | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // ββββββββββ ANATOMICAL BRAIN POINT CLOUD ENGINE ββββββββββββββββββββββ | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // ββ Brain state ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| let humanBrainCloud = null; // THREE.Points β the human-mind brain | |
| let aiBrainCloud = null; // THREE.Points β the AI-mind brain | |
| let brainGroup = null; // THREE.Group containing both | |
| let brainConnLines = null; // THREE.LineSegments β synapse arcs | |
| let brainWireframe = null; // subtle cortical surface | |
| let brainParticleSystem = null;// floating neural dust | |
| let brainAnimFrame = 0; // frame counter for animation | |
| let neuralSimWs = null; // WebSocket to neural-sim backend | |
| let latestBioData = null; // most recent bio metrics | |
| let latestAiData = null; // most recent AI metrics | |
| let latestSelfModify= null; // most recent self-modification event | |
| let brainInitDone = false; | |
| // ββ Anatomical constants βββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| const BRAIN = { | |
| N_HUMAN : 8192, // points for human brain | |
| N_AI : 8192, // points for AI brain | |
| N_SYNAPSES: 1200, // synapse arc lines | |
| N_DUST : 2000, // floating particle dust | |
| SCALE : 0.62, // brain radius in world units | |
| SEPARATION: 1.65, // horizontal offset from center (each brain) | |
| HEIGHT : 2.5, // y-position of brain center in room | |
| Z_OFFSET : 14.0, // z-position in AI Systems Lab | |
| }; | |
| // Lobe color map β used for human brain | |
| const LOBE_COLORS = { | |
| frontal : new THREE.Color(0.85, 0.55, 0.30), // warm orange | |
| parietal : new THREE.Color(0.45, 0.75, 0.55), // mint green | |
| temporal : new THREE.Color(0.40, 0.60, 0.90), // cobalt blue | |
| occipital : new THREE.Color(0.80, 0.40, 0.75), // violet | |
| limbic : new THREE.Color(0.95, 0.80, 0.30), // gold | |
| cerebellum: new THREE.Color(0.60, 0.85, 0.90), // teal | |
| brainstem : new THREE.Color(0.70, 0.70, 0.70), // silver | |
| }; | |
| // AI brain layer colors β cool cyberpunk palette keyed by attention layer quartile | |
| const AI_LAYER_COLORS = [ | |
| new THREE.Color(0.10, 0.60, 1.00), // deep blue β early layers | |
| new THREE.Color(0.20, 0.90, 0.80), // cyan | |
| new THREE.Color(0.50, 0.30, 1.00), // purple β mid layers | |
| new THREE.Color(0.80, 0.20, 0.80), // magenta | |
| new THREE.Color(1.00, 0.55, 0.10), // amber β late layers | |
| ]; | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // GEOMETRY GENERATOR β anatomically-shaped brain surface | |
| // Uses superellipsoid + multi-octave sulcal folds + lobe separation | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function generateBrainPositions(N, seed) { | |
| // Deterministic seeded RNG (xorshift32) | |
| let s = seed >>> 0 || 0x1234ABCD; | |
| function rand() { | |
| s ^= s << 13; s ^= s >> 17; s ^= s << 5; | |
| return ((s >>> 0) / 0xFFFFFFFF); | |
| } | |
| const positions = new Float32Array(N * 3); | |
| const normals = new Float32Array(N * 3); // for shading | |
| const lobes = new Uint8Array(N); // lobe id per point | |
| // Sample points on an anatomical brain surface via rejection sampling | |
| // on a deformed sphere | |
| let i = 0; | |
| let attempts = 0; | |
| while (i < N && attempts < N * 40) { | |
| attempts++; | |
| // Fibonacci sphere for uniform distribution base | |
| const theta = Math.acos(2.0 * rand() - 1.0); | |
| const phi = 2.0 * Math.PI * rand(); | |
| let x = Math.sin(theta) * Math.cos(phi); | |
| let y = Math.sin(theta) * Math.sin(phi); | |
| let z = Math.cos(theta); | |
| // ββ BRAIN SHAPE DEFORMATION ββββββββββββββββββββββββββββββββββββββββββ | |
| // 1. Overall brain ovoid (wider front-back than left-right) | |
| x *= 0.82; | |
| y *= 0.68; | |
| z *= 1.00; | |
| // 2. Flatten the bottom (brainstem area, negative y) | |
| if (y < -0.3) y *= 0.65; | |
| // 3. Bifurcation β left/right hemispheres (interhemispheric fissure) | |
| const hemiSign = x > 0 ? 1.0 : -1.0; | |
| if (Math.abs(x) < 0.08) { | |
| // Points near midline: clamp or remove most (keep only corpus callosum) | |
| if (rand() > 0.12) { attempts++; continue; } | |
| } | |
| // Push hemispheres apart slightly | |
| x += hemiSign * 0.04; | |
| // 4. Gyral folds (sulci & gyri) via multi-octave noise | |
| const nx = x, ny = y, nz = z; | |
| let sulcalR = 0.0; | |
| // Primary sulci (large folds) | |
| sulcalR += 0.055 * Math.sin(nx * 8.0 + 1.3) * Math.cos(ny * 6.5 + 0.7); | |
| sulcalR += 0.045 * Math.cos(nz * 7.5 + 2.1) * Math.sin(ny * 5.0 + 1.1); | |
| sulcalR += 0.038 * Math.sin(nz * 9.0 + ny * 6.0 + 0.5); | |
| // Secondary sulci | |
| sulcalR += 0.025 * Math.cos(nx * 15.0 + nz * 12.0 + 0.8); | |
| sulcalR += 0.020 * Math.sin(ny * 14.0 + nz * 11.0 + 1.6); | |
| // Tertiary (fine texture) | |
| sulcalR += 0.012 * Math.cos(nx * 22.0 + ny * 19.0 + 0.3); | |
| sulcalR += 0.010 * Math.sin(nz * 25.0 + nx * 18.0 + 2.0); | |
| // Apply sulcal deformation (radial) | |
| const r0 = Math.sqrt(x*x + y*y + z*z); | |
| const rDeformed = r0 + sulcalR; | |
| if (r0 > 1e-6) { | |
| x = x * rDeformed / r0; | |
| y = y * rDeformed / r0; | |
| z = z * rDeformed / r0; | |
| } | |
| // 5. Occipital pole β push back & narrow | |
| if (z < -0.4) { z *= 1.18; x *= 0.78; } | |
| // 6. Temporal lobes β bulge outward inferior-lateral | |
| if (y < -0.1 && Math.abs(x) > 0.3) { | |
| const tStrength = Math.max(0, (-y - 0.1) * (Math.abs(x) - 0.3)); | |
| x += hemiSign * tStrength * 0.5; | |
| y -= tStrength * 0.3; | |
| } | |
| // 7. Frontal pole rounding | |
| if (z > 0.55) { z *= 0.92; } | |
| // 8. Scale to BRAIN.SCALE | |
| x *= BRAIN.SCALE; | |
| y *= BRAIN.SCALE; | |
| z *= BRAIN.SCALE; | |
| // Small jitter for pointcloud density variation | |
| x += (rand() - 0.5) * 0.008; | |
| y += (rand() - 0.5) * 0.008; | |
| z += (rand() - 0.5) * 0.008; | |
| // ββ LOBE CLASSIFICATION ββββββββββββββββββββββββββββββββββββββββββββββ | |
| let lobe; | |
| const yn = y / BRAIN.SCALE; | |
| const zn = z / BRAIN.SCALE; | |
| const xn_abs = Math.abs(x) / BRAIN.SCALE; | |
| if (yn < -0.50 && zn < -0.55) { | |
| lobe = 5; // cerebellum (posterior-inferior) | |
| } else if (yn < -0.35 && xn_abs < 0.25) { | |
| lobe = 6; // brainstem (inferior midline) | |
| } else if (zn > 0.30) { | |
| lobe = 0; // frontal (anterior z) | |
| } else if (yn > 0.30 && zn < 0.10 && zn > -0.30) { | |
| lobe = 1; // parietal (superior mid) | |
| } else if (yn < -0.05 && xn_abs > 0.30) { | |
| lobe = 2; // temporal (lateral inferior) | |
| } else if (zn < -0.25) { | |
| lobe = 3; // occipital (posterior) | |
| } else { | |
| lobe = 4; // limbic / cingulate | |
| } | |
| // ββ STORE βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| positions[i*3 + 0] = x; | |
| positions[i*3 + 1] = y; | |
| positions[i*3 + 2] = z; | |
| // Simple normal: radial outward from center | |
| const len = Math.sqrt(x*x+y*y+z*z)+1e-8; | |
| normals[i*3+0] = x/len; normals[i*3+1] = y/len; normals[i*3+2] = z/len; | |
| lobes[i] = lobe; | |
| i++; | |
| } | |
| return { positions, normals, lobes, count: i }; | |
| } | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // POINT CLOUD SHADER MATERIAL | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function makeBrainMaterial(isAI) { | |
| return new THREE.ShaderMaterial({ | |
| uniforms: { | |
| uTime : { value: 0.0 }, | |
| uActivity : { value: new Float32Array(64) }, // per-region activity (0..1) | |
| uPulse : { value: 0.0 }, // global pulse amplitude | |
| uLearning : { value: 0.0 }, // training activity (0..1) | |
| uAttention : { value: 0.0 }, // attention entropy (0..1) | |
| uSelfModify : { value: 0.0 }, // self-modification flash | |
| uLoss : { value: 0.0 }, // current loss (0..1 normalized) | |
| uConfidence : { value: 0.5 }, | |
| uIsAI : { value: isAI ? 1.0 : 0.0 }, | |
| uBrainState : { value: 0.0 }, // 0=rest, 1=active, 2=learning, 3=modifying | |
| }, | |
| vertexShader: ` | |
| attribute float aActivity; // per-point activity 0..1 | |
| attribute float aLobe; // lobe index 0..6 | |
| attribute float aLayer; // AI layer index 0..31 | |
| attribute float aSize; // base point size | |
| attribute vec3 aBaseColor; // base color for this point | |
| varying float vActivity; | |
| varying float vLobe; | |
| varying float vLayer; | |
| varying vec3 vBaseColor; | |
| varying float vDepth; | |
| varying vec3 vWorldPos; | |
| uniform float uTime; | |
| uniform float uPulse; | |
| uniform float uLearning; | |
| uniform float uAttention; | |
| uniform float uSelfModify; | |
| uniform float uIsAI; | |
| uniform float uBrainState; | |
| // Simplex-inspired hash noise | |
| float hash(vec3 p) { | |
| p = fract(p * 0.3183099 + 0.1); | |
| p *= 17.0; | |
| return fract(p.x*p.y*p.z*(p.x+p.y+p.z)); | |
| } | |
| float noise(vec3 p) { | |
| vec3 i = floor(p); | |
| vec3 f = fract(p); | |
| f = f*f*(3.0-2.0*f); | |
| return mix(mix(mix(hash(i),hash(i+vec3(1,0,0)),f.x), | |
| mix(hash(i+vec3(0,1,0)),hash(i+vec3(1,1,0)),f.x),f.y), | |
| mix(mix(hash(i+vec3(0,0,1)),hash(i+vec3(1,0,1)),f.x), | |
| mix(hash(i+vec3(0,1,1)),hash(i+vec3(1,1,1)),f.x),f.y),f.z); | |
| } | |
| void main() { | |
| vActivity = aActivity; | |
| vLobe = aLobe; | |
| vLayer = aLayer; | |
| vBaseColor = aBaseColor; | |
| // Animated position β gyral drift during learning | |
| vec3 pos = position; | |
| float nVal = noise(pos * 3.5 + uTime * 0.12); | |
| // Breathing oscillation (scale with activity) | |
| float breathe = 0.003 * sin(uTime * 1.3 + aLobe * 0.8) * (1.0 + aActivity * 2.0); | |
| pos += normalize(pos) * breathe; | |
| // During learning: active points ripple outward from center | |
| if (uLearning > 0.05) { | |
| float wave = sin(length(pos) * 14.0 - uTime * 4.0) * 0.5 + 0.5; | |
| pos += normalize(pos) * wave * aActivity * uLearning * 0.018; | |
| } | |
| // Self-modification flash β points radiate | |
| if (uSelfModify > 0.01) { | |
| pos += normalize(pos) * uSelfModify * aActivity * 0.04 * sin(uTime * 20.0); | |
| } | |
| // Attention entropy β randomize positions of low-attention points slightly | |
| float jitter = (1.0 - aActivity) * uAttention * 0.006; | |
| pos += vec3(nVal*2.0-1.0, hash(pos.yzx)*2.0-1.0, hash(pos.zxy)*2.0-1.0) * jitter; | |
| vDepth = gl_Position.z; | |
| vWorldPos = (modelMatrix * vec4(pos, 1.0)).xyz; | |
| gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0); | |
| // Point size: base * activity boost * learning amplification | |
| float sz = aSize * (0.85 + aActivity * 1.8); | |
| sz *= (1.0 + uPulse * aActivity * 1.2); | |
| sz *= (1.0 + uLearning * aActivity * 0.8); | |
| // Distance attenuation | |
| float dist = length(gl_Position.xyz); | |
| gl_PointSize = sz * (320.0 / max(dist, 1.0)); | |
| gl_PointSize = clamp(gl_PointSize, 0.8, 14.0); | |
| } | |
| `, | |
| fragmentShader: ` | |
| varying float vActivity; | |
| varying float vLobe; | |
| varying float vLayer; | |
| varying vec3 vBaseColor; | |
| varying float vDepth; | |
| varying vec3 vWorldPos; | |
| uniform float uTime; | |
| uniform float uPulse; | |
| uniform float uLearning; | |
| uniform float uSelfModify; | |
| uniform float uConfidence; | |
| uniform float uLoss; | |
| uniform float uIsAI; | |
| uniform float uBrainState; | |
| vec3 lobeColor(float l, float act) { | |
| // Lobe palette for human brain | |
| vec3 c; | |
| if (l < 0.5) c = vec3(0.85, 0.55, 0.30); // frontal β warm amber | |
| else if (l < 1.5) c = vec3(0.45, 0.75, 0.55); // parietal β mint | |
| else if (l < 2.5) c = vec3(0.40, 0.60, 0.90); // temporal β cobalt | |
| else if (l < 3.5) c = vec3(0.80, 0.40, 0.75); // occipitalβ violet | |
| else if (l < 4.5) c = vec3(0.95, 0.80, 0.30); // limbic β gold | |
| else if (l < 5.5) c = vec3(0.60, 0.85, 0.90); // cerebellum- teal | |
| else c = vec3(0.70, 0.70, 0.70); // brainstem - silver | |
| return c; | |
| } | |
| vec3 aiLayerColor(float layer, float act) { | |
| // Cool neural-net layers from inputβoutput: blueβcyanβpurpleβmagentaβamber | |
| float t = layer / 31.0; | |
| vec3 c0 = vec3(0.10, 0.55, 1.00); | |
| vec3 c1 = vec3(0.20, 0.90, 0.80); | |
| vec3 c2 = vec3(0.60, 0.25, 1.00); | |
| vec3 c3 = vec3(0.95, 0.20, 0.75); | |
| vec3 c4 = vec3(1.00, 0.60, 0.10); | |
| vec3 col; | |
| if (t < 0.25) col = mix(c0, c1, t * 4.0); | |
| else if (t < 0.50) col = mix(c1, c2, (t-0.25)*4.0); | |
| else if (t < 0.75) col = mix(c2, c3, (t-0.50)*4.0); | |
| else col = mix(c3, c4, (t-0.75)*4.0); | |
| return col; | |
| } | |
| void main() { | |
| // Circular point shape | |
| vec2 uv = gl_PointCoord * 2.0 - 1.0; | |
| float d = dot(uv, uv); | |
| if (d > 1.0) discard; | |
| // Soft edge falloff | |
| float alpha = 1.0 - smoothstep(0.4, 1.0, d); | |
| // Get base color | |
| vec3 col; | |
| if (uIsAI > 0.5) { | |
| col = aiLayerColor(vLayer, vActivity); | |
| // Mix with high-loss warning color (red shift) | |
| col = mix(col, vec3(1.0, 0.2, 0.1), uLoss * vActivity * 0.5); | |
| } else { | |
| col = lobeColor(vLobe, vActivity); | |
| } | |
| // Activity glow: bright white-hot core for active neurons | |
| float actGlow = pow(vActivity, 1.8); | |
| col = mix(col, vec3(1.0, 0.98, 0.90), actGlow * 0.55); | |
| // Pulse: global heartbeat | |
| float pulse = uPulse * sin(uTime * 3.14159) * 0.3; | |
| col += col * pulse * vActivity; | |
| // Self-modification flash β cool electric blue wave | |
| if (uSelfModify > 0.01) { | |
| float smFlash = uSelfModify * sin(uTime * 25.0 + vLayer * 0.8) * 0.5 + 0.5; | |
| col = mix(col, vec3(0.2, 0.8, 1.0), smFlash * vActivity * 0.7); | |
| } | |
| // Learning ripple β warm amber pulse from active regions | |
| if (uLearning > 0.05) { | |
| float learnGlow = uLearning * vActivity * (0.5 + 0.5 * sin(uTime * 6.0)); | |
| col += vec3(0.4, 0.2, 0.0) * learnGlow * 0.6; | |
| } | |
| // Specular highlight on near edge | |
| float rim = 1.0 - smoothstep(0.0, 0.45, d); | |
| col += vec3(1.0) * rim * 0.08 * vActivity; | |
| // Dim inactive points significantly | |
| float baseAlpha = mix(0.04, 0.85, vActivity); | |
| alpha *= baseAlpha; | |
| alpha = clamp(alpha, 0.0, 1.0); | |
| gl_FragColor = vec4(col, alpha); | |
| } | |
| `, | |
| transparent: true, | |
| depthWrite: false, | |
| blending: THREE.AdditiveBlending, | |
| }); | |
| } | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // SYNAPSE ARCS β connecting line segments between active neurons | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function makeSynapseArcs(positions, N, maxArcs) { | |
| const arcPositions = []; | |
| const arcColors = []; | |
| const arcCount = maxArcs; | |
| // Pre-generate random arc pairs (updated dynamically each frame) | |
| for (let i = 0; i < arcCount; i++) { | |
| arcPositions.push(0,0,0, 0,0,0); | |
| arcColors.push(0,0,0, 0,0,0); | |
| } | |
| const geo = new THREE.BufferGeometry(); | |
| geo.setAttribute('position', new THREE.Float32BufferAttribute(arcPositions, 3)); | |
| geo.setAttribute('color', new THREE.Float32BufferAttribute(arcColors, 3)); | |
| const mat = new THREE.LineBasicMaterial({ | |
| vertexColors: true, | |
| transparent: true, | |
| opacity: 0.25, | |
| blending: THREE.AdditiveBlending, | |
| depthWrite: false, | |
| }); | |
| const lines = new THREE.LineSegments(geo, mat); | |
| lines.userData.sourcePositions = positions; | |
| lines.userData.N = N; | |
| return lines; | |
| } | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // CORTICAL SURFACE WIREFRAME (subtle mesh overlay) | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function makeCorticalWireframe(isAI) { | |
| // Create a low-poly brain-shaped wireframe using SphereGeometry + deformation | |
| const geo = new THREE.SphereGeometry(BRAIN.SCALE, 32, 24); | |
| const pos = geo.attributes.position; | |
| for (let i = 0; i < pos.count; i++) { | |
| let x = pos.getX(i), y = pos.getY(i), z = pos.getZ(i); | |
| // Apply same deformation as point cloud | |
| x *= 0.82; y *= 0.68; | |
| if (y < -0.3 * BRAIN.SCALE) y *= 0.65; | |
| if (z < -0.4 * BRAIN.SCALE) { z *= 1.18; x *= 0.78; } | |
| // Sulcal folds (coarser) | |
| const f = 0.035 * Math.sin(x * 8.5 + 0.8) * Math.cos(y * 7.0 + 0.5); | |
| const r = Math.sqrt(x*x+y*y+z*z) + f; | |
| const r0 = Math.sqrt(x*x+y*y+z*z) + 1e-6; | |
| x = x * r / r0; y = y * r / r0; z = z * r / r0; | |
| pos.setXYZ(i, x, y, z); | |
| } | |
| geo.computeVertexNormals(); | |
| geo.computeBoundingSphere(); | |
| const mat = new THREE.MeshBasicMaterial({ | |
| color: isAI ? 0x1a4060 : 0x3a2a1a, | |
| wireframe: true, | |
| transparent: true, | |
| opacity: 0.04, | |
| }); | |
| return new THREE.Mesh(geo, mat); | |
| } | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // NEURAL DUST β ambient floating particles around each brain | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function makeNeuralDust(N, isAI) { | |
| const positions = new Float32Array(N * 3); | |
| const velocities = new Float32Array(N * 3); | |
| const r = BRAIN.SCALE * 1.5; | |
| for (let i = 0; i < N; i++) { | |
| positions[i*3+0] = (Math.random()-0.5) * r * 2.2; | |
| positions[i*3+1] = (Math.random()-0.5) * r * 2.2; | |
| positions[i*3+2] = (Math.random()-0.5) * r * 2.2; | |
| velocities[i*3+0] = (Math.random()-0.5) * 0.002; | |
| velocities[i*3+1] = (Math.random()-0.5) * 0.001; | |
| velocities[i*3+2] = (Math.random()-0.5) * 0.002; | |
| } | |
| const geo = new THREE.BufferGeometry(); | |
| geo.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3)); | |
| geo.userData.velocities = velocities; | |
| const mat = new THREE.PointsMaterial({ | |
| size: 0.008, | |
| color: isAI ? 0x2080ff : 0xffaa44, | |
| transparent: true, | |
| opacity: 0.35, | |
| blending: THREE.AdditiveBlending, | |
| depthWrite: false, | |
| }); | |
| return new THREE.Points(geo, mat); | |
| } | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // BRAIN BUILDER β assembles the full brain point cloud group | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function buildBrainCloud(isAI, offsetX) { | |
| const N = isAI ? BRAIN.N_AI : BRAIN.N_HUMAN; | |
| const seed = isAI ? 0xDEADBEEF : 0xC0FFEE42; | |
| const { positions, normals, lobes, count } = generateBrainPositions(N, seed); | |
| const geo = new THREE.BufferGeometry(); | |
| geo.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3)); | |
| // Activity attribute (0..1 per point β updated each frame from metrics) | |
| const activity = new Float32Array(count); | |
| activity.fill(0.08); // resting baseline | |
| geo.setAttribute('aActivity', new THREE.Float32BufferAttribute(activity, 1)); | |
| // Lobe / layer attribute | |
| const lobeAttr = new Float32Array(count); | |
| const layerAttr = new Float32Array(count); | |
| for (let i = 0; i < count; i++) { | |
| lobeAttr[i] = lobes[i]; | |
| // For AI brain: assign layer based on position (z-depth maps to layer) | |
| if (isAI) { | |
| // Map y+z to layer 0..31 | |
| const yn = (positions[i*3+1] + BRAIN.SCALE) / (BRAIN.SCALE * 2.0); | |
| const zn = (positions[i*3+2] + BRAIN.SCALE) / (BRAIN.SCALE * 2.0); | |
| layerAttr[i] = Math.floor((yn * 0.5 + zn * 0.5) * 31.0); | |
| } else { | |
| layerAttr[i] = lobes[i]; | |
| } | |
| } | |
| geo.setAttribute('aLobe', new THREE.Float32BufferAttribute(lobeAttr, 1)); | |
| geo.setAttribute('aLayer', new THREE.Float32BufferAttribute(layerAttr, 1)); | |
| // Base size: slightly larger for gyral crowns (high normals.y) | |
| const sizeAttr = new Float32Array(count); | |
| for (let i = 0; i < count; i++) { | |
| sizeAttr[i] = 2.5 + Math.random() * 1.8; | |
| } | |
| geo.setAttribute('aSize', new THREE.Float32BufferAttribute(sizeAttr, 1)); | |
| // Base color per point | |
| const colorAttr = new Float32Array(count * 3); | |
| geo.setAttribute('aBaseColor', new THREE.Float32BufferAttribute(colorAttr, 3)); | |
| const mat = makeBrainMaterial(isAI); | |
| const cloud = new THREE.Points(geo, mat); | |
| cloud.userData.isAI = isAI; | |
| cloud.userData.count = count; | |
| cloud.userData.basePositions = positions.slice(0, count * 3); | |
| // Synapse arcs | |
| const arcs = makeSynapseArcs(positions, count, BRAIN.N_SYNAPSES); | |
| arcs.position.set(offsetX, 0, 0); | |
| // Cortical wireframe | |
| const wire = makeCorticalWireframe(isAI); | |
| // Neural dust | |
| const dust = makeNeuralDust(BRAIN.N_DUST, isAI); | |
| // Group everything | |
| const group = new THREE.Group(); | |
| group.add(cloud); | |
| group.add(wire); | |
| group.add(dust); | |
| group.position.set(offsetX, BRAIN.HEIGHT, BRAIN.Z_OFFSET); | |
| // Subtle floating rotation (different for each brain) | |
| group.userData.rotSpeed = isAI ? 0.0022 : -0.0018; | |
| group.userData.cloud = cloud; | |
| group.userData.arcs = arcs; | |
| group.userData.dust = dust; | |
| group.userData.wire = wire; | |
| // Label | |
| const labelTex = makeCanvasTexture(512, 80, (ctx, w, h) => { | |
| ctx.clearRect(0, 0, w, h); | |
| ctx.font = 'bold 28px "Segoe UI", sans-serif'; | |
| ctx.textAlign = 'center'; | |
| ctx.textBaseline = 'middle'; | |
| ctx.fillStyle = isAI ? '#50c8ff' : '#ffaa44'; | |
| ctx.fillText(isAI ? 'β AI MIND' : 'β HUMAN MIND', w/2, h/2); | |
| }); | |
| const labelMat = new THREE.SpriteMaterial({ map: labelTex, transparent: true, | |
| depthWrite: false, blending: THREE.AdditiveBlending }); | |
| const label = new THREE.Sprite(labelMat); | |
| label.scale.set(1.2, 0.18, 1.0); | |
| label.position.set(0, -BRAIN.SCALE - 0.2, 0); | |
| group.add(label); | |
| return { group, cloud, arcs, dust }; | |
| } | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // INITIALIZE BOTH BRAINS IN SCENE | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function initBrainClouds() { | |
| if (brainInitDone) return; | |
| brainInitDone = true; | |
| brainGroup = new THREE.Group(); | |
| const humanResult = buildBrainCloud(false, -BRAIN.SEPARATION); | |
| const aiResult = buildBrainCloud(true, +BRAIN.SEPARATION); | |
| humanBrainCloud = humanResult.cloud; | |
| aiBrainCloud = aiResult.cloud; | |
| brainGroup.add(humanResult.group); | |
| brainGroup.add(aiResult.group); | |
| // Add synapse arc groups at scene level | |
| scene.add(aiResult.arcs); | |
| aiResult.arcs.position.set( | |
| BRAIN.SEPARATION, BRAIN.HEIGHT, BRAIN.Z_OFFSET | |
| ); | |
| // Floating connection arcs between human & AI brains | |
| buildInterBrainConnections(); | |
| // Ambient light for brains | |
| const brainAmbient = new THREE.PointLight(0x204080, 0.8, 6.0, 1.5); | |
| brainAmbient.position.set(0, BRAIN.HEIGHT + 0.5, BRAIN.Z_OFFSET); | |
| scene.add(brainAmbient); | |
| const humanLight = new THREE.PointLight(0xff8833, 0.5, 3.5, 2.0); | |
| humanLight.position.set(-BRAIN.SEPARATION, BRAIN.HEIGHT + 0.3, BRAIN.Z_OFFSET); | |
| scene.add(humanLight); | |
| const aiLight = new THREE.PointLight(0x2090ff, 0.5, 3.5, 2.0); | |
| aiLight.position.set(+BRAIN.SEPARATION, BRAIN.HEIGHT + 0.3, BRAIN.Z_OFFSET); | |
| scene.add(aiLight); | |
| scene.add(brainGroup); | |
| // Connect to neural-sim WebSocket | |
| connectNeuralSimWS(); | |
| console.log('[brain] Anatomical brain clouds initialized.'); | |
| } | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // INTER-BRAIN CONNECTION ARCS (human β AI thought transfer) | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function buildInterBrainConnections() { | |
| const N_INTER = 80; | |
| const posArr = new Float32Array(N_INTER * 2 * 3); | |
| const colArr = new Float32Array(N_INTER * 2 * 3); | |
| // Pre-fill with placeholder positions (updated dynamically) | |
| for (let i = 0; i < N_INTER; i++) { | |
| posArr[i*6+0] = -BRAIN.SEPARATION; posArr[i*6+1] = BRAIN.HEIGHT; posArr[i*6+2] = BRAIN.Z_OFFSET; | |
| posArr[i*6+3] = +BRAIN.SEPARATION; posArr[i*6+4] = BRAIN.HEIGHT; posArr[i*6+5] = BRAIN.Z_OFFSET; | |
| // Gradient: warmβcool | |
| colArr[i*6+0]=1.0; colArr[i*6+1]=0.5; colArr[i*6+2]=0.1; | |
| colArr[i*6+3]=0.1; colArr[i*6+4]=0.5; colArr[i*6+5]=1.0; | |
| } | |
| const geo = new THREE.BufferGeometry(); | |
| geo.setAttribute('position', new THREE.Float32BufferAttribute(posArr, 3)); | |
| geo.setAttribute('color', new THREE.Float32BufferAttribute(colArr, 3)); | |
| const mat = new THREE.LineBasicMaterial({ | |
| vertexColors: true, transparent: true, opacity: 0.12, | |
| blending: THREE.AdditiveBlending, depthWrite: false, | |
| }); | |
| brainConnLines = new THREE.LineSegments(geo, mat); | |
| scene.add(brainConnLines); | |
| } | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // REAL-TIME METRIC UPDATE β drive brain visuals from live training data | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function updateBrainFromMetrics(aiData, bioData) { | |
| if (!humanBrainCloud || !aiBrainCloud) return; | |
| const t = performance.now() * 0.001; | |
| // ββ Human Brain (bio metrics) ββββββββββββββββββββββββββββββββββββββββββββ | |
| if (bioData) { | |
| const neuronActs = bioData.neuron_activities || []; | |
| const sparsity = bioData.sparsity ?? 0.5; | |
| const firingRate = bioData.mean_firing_rate ?? 0.0; | |
| const plasticity = bioData.plasticity ?? 0.5; | |
| const delta = bioData.delta ?? 0.0; | |
| const attention = bioData.attention_focus ?? 0.5; | |
| const dopamine = bioData.dopamine ?? 0.5; | |
| const hGeo = humanBrainCloud.geometry; | |
| const hAct = hGeo.getAttribute('aActivity'); | |
| const N = hGeo.attributes.position.count; | |
| for (let i = 0; i < N; i++) { | |
| const lobe = hGeo.getAttribute('aLobe').getX(i); | |
| // Map neural activities to spatial lobe groups | |
| let baseAct = 0.06; | |
| if (neuronActs.length > 0) { | |
| // Map point index to neuron via modulo | |
| const nIdx = i % neuronActs.length; | |
| baseAct = neuronActs[nIdx] || 0.0; | |
| } else { | |
| // Fallback: use firing rate with spatial variation | |
| baseAct = firingRate * 0.2 * (0.5 + 0.5 * Math.sin(i * 0.31 + t * 0.8)); | |
| } | |
| // Lobe-specific modulation | |
| if (lobe === 0) baseAct *= (1.0 + attention * 0.8); // frontal: attention | |
| if (lobe === 1) baseAct *= (1.0 + plasticity * 0.6); // parietal: plasticity | |
| if (lobe === 4) baseAct *= (1.0 + dopamine * 1.2); // limbic: dopamine | |
| if (lobe === 5) baseAct *= (1.0 + firingRate * 0.4); // cerebellum: motor | |
| // Prediction error pulses in occipital | |
| if (lobe === 3) baseAct += Math.abs(delta) * 0.4 * (Math.sin(t * 8.0 + i * 0.2) * 0.5 + 0.5); | |
| hAct.setX(i, Math.min(1.0, Math.max(0.0, baseAct))); | |
| } | |
| hAct.needsUpdate = true; | |
| // Uniforms | |
| const hu = humanBrainCloud.material.uniforms; | |
| hu.uPulse.value = Math.abs(delta) * 0.8 + firingRate * 0.3; | |
| hu.uLearning.value = plasticity; | |
| hu.uAttention.value = attention; | |
| hu.uBrainState.value = sparsity > 0.8 ? 0.0 : 1.0; // resting vs active | |
| } | |
| // ββ AI Brain (AI metrics) βββββββββββββββββββββββββββββββββββββββββββββββ | |
| if (aiData) { | |
| const gradNorms = aiData.gradient_norms || []; | |
| const actSurv = aiData.activation_survival || []; | |
| const attnEntropy= aiData.attention_entropy || []; | |
| const loss = aiData.loss ?? 1.0; | |
| const confidence = aiData.confidence ?? 0.5; | |
| const lr = aiData.effective_lr ?? 0.0; | |
| const clipRate = aiData.clip_rate ?? 0.0; | |
| const forgetting = aiData.forgetting_score ?? 0.0; | |
| const embedDrift = aiData.embedding_drift ?? 0.0; | |
| const momentum = aiData.momentum_velocity ?? 0.0; | |
| const cka = aiData.mean_cka ?? 0.5; | |
| const snr = aiData.gradient_snr ?? 1.0; | |
| const lossNorm = Math.min(1.0, loss / 5.0); | |
| const aGeo = aiBrainCloud.geometry; | |
| const aAct = aGeo.getAttribute('aActivity'); | |
| const N = aGeo.attributes.position.count; | |
| const layers = aGeo.getAttribute('aLayer'); | |
| const nLayers = Math.max(gradNorms.length, 1); | |
| for (let i = 0; i < N; i++) { | |
| const layerIdx = Math.floor(layers.getX(i)); | |
| const lNorm = gradNorms.length > 0 | |
| ? Math.min(1.0, (gradNorms[Math.floor(layerIdx * gradNorms.length / 32)] || 0.0) / 5.0) | |
| : 0.0; | |
| const survival = actSurv.length > 0 | |
| ? (actSurv[Math.floor(layerIdx * actSurv.length / 32)] || 0.5) | |
| : 0.5; | |
| // Activity = gradient Γ survival Γ (1 - forgetting penalty) | |
| let act = lNorm * 0.6 + survival * 0.25; | |
| act *= (1.0 - forgetting * 0.5); | |
| // Momentum adds fire | |
| act += momentum * 0.15 * (Math.sin(i * 0.17 + t * 2.5) * 0.5 + 0.5); | |
| // Attention: high-entropy = spread activation across more points | |
| if (attnEntropy.length > 0) { | |
| const ent = attnEntropy[i % attnEntropy.length] || 0.0; | |
| act += ent * 0.1; | |
| } | |
| // Loss uncertainty: high loss β flickering | |
| if (lossNorm > 0.6) { | |
| act *= (0.7 + 0.3 * Math.sin(t * 12.0 + i * 0.5)); | |
| } | |
| aAct.setX(i, Math.min(1.0, Math.max(0.0, act))); | |
| } | |
| aAct.needsUpdate = true; | |
| const au = aiBrainCloud.material.uniforms; | |
| au.uLoss.value = lossNorm; | |
| au.uConfidence.value = confidence; | |
| au.uPulse.value = clipRate * 0.8 + momentum * 0.3; | |
| au.uLearning.value = Math.min(1.0, lr * 100000.0); | |
| au.uAttention.value = Math.min(1.0, cka); | |
| } | |
| } | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // SELF-MODIFICATION VISUAL RESPONSE | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function triggerSelfModifyFlash() { | |
| if (!aiBrainCloud) return; | |
| const u = aiBrainCloud.material.uniforms; | |
| u.uSelfModify.value = 1.0; | |
| // Decay over 2 seconds | |
| let t0 = performance.now(); | |
| (function decay() { | |
| const elapsed = (performance.now() - t0) / 2000.0; | |
| if (elapsed < 1.0) { | |
| u.uSelfModify.value = 1.0 - elapsed; | |
| requestAnimationFrame(decay); | |
| } else { | |
| u.uSelfModify.value = 0.0; | |
| } | |
| })(); | |
| // Also pulse the synapse arcs | |
| if (brainConnLines) { | |
| brainConnLines.material.opacity = 0.8; | |
| setTimeout(() => { brainConnLines.material.opacity = 0.12; }, 600); | |
| } | |
| showToast('β‘ Self-Modification Applied', 'success'); | |
| } | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // ANIMATE BRAINS EACH FRAME | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function animateBrains(time) { | |
| if (!brainGroup || !brainInitDone) return; | |
| brainAnimFrame++; | |
| const t = time; | |
| // Rotate each brain group | |
| brainGroup.children.forEach((child, idx) => { | |
| const rs = child.userData.rotSpeed || 0.002; | |
| child.rotation.y += rs; | |
| child.rotation.x = Math.sin(t * 0.15 + idx) * 0.04; | |
| // Float up/down gently | |
| child.position.y = BRAIN.HEIGHT + Math.sin(t * 0.8 + idx * 1.2) * 0.035; | |
| }); | |
| // Update synapse arcs (every 3 frames for performance) | |
| if (brainAnimFrame % 3 === 0 && aiBrainCloud) { | |
| updateSynapseArcs(); | |
| } | |
| // Update inter-brain connections (every 8 frames) | |
| if (brainAnimFrame % 8 === 0 && brainConnLines && latestAiData) { | |
| updateInterBrainConnections(t); | |
| } | |
| // Update neural dust positions | |
| brainGroup.children.forEach((child) => { | |
| const dust = child.userData.dust; | |
| if (!dust) return; | |
| const pos = dust.geometry.attributes.position; | |
| const vel = dust.geometry.userData.velocities; | |
| const N = pos.count; | |
| const r = BRAIN.SCALE * 1.5; | |
| for (let i = 0; i < N; i++) { | |
| let x = pos.getX(i) + vel[i*3+0]; | |
| let y = pos.getY(i) + vel[i*3+1]; | |
| let z = pos.getZ(i) + vel[i*3+2]; | |
| if (Math.abs(x) > r) vel[i*3+0] *= -1; | |
| if (Math.abs(y) > r) vel[i*3+1] *= -1; | |
| if (Math.abs(z) > r) vel[i*3+2] *= -1; | |
| pos.setXYZ(i, x, y, z); | |
| } | |
| pos.needsUpdate = true; | |
| }); | |
| // Update time uniforms | |
| if (humanBrainCloud) humanBrainCloud.material.uniforms.uTime.value = t; | |
| if (aiBrainCloud) aiBrainCloud.material.uniforms.uTime.value = t; | |
| } | |
| function updateSynapseArcs() { | |
| brainGroup.children.forEach((child) => { | |
| const arcs = child.userData.arcs; | |
| if (!arcs || !arcs.userData.sourcePositions) return; | |
| const src = arcs.userData.sourcePositions; | |
| const N = arcs.userData.N; | |
| const posAttr = arcs.geometry.attributes.position; | |
| const colAttr = arcs.geometry.attributes.color; | |
| const nArcs = posAttr.count / 2; | |
| const cloud = child.userData.cloud; | |
| const actAttr = cloud ? cloud.geometry.getAttribute('aActivity') : null; | |
| for (let a = 0; a < nArcs; a++) { | |
| // Pick two random points that are active | |
| let i = Math.floor(Math.random() * (N/3)) * 3; | |
| let j = Math.floor(Math.random() * (N/3)) * 3; | |
| const actI = actAttr ? actAttr.getX(i/3) : 0.5; | |
| const actJ = actAttr ? actAttr.getX(j/3) : 0.5; | |
| // Only draw arcs between active neurons | |
| const shouldDraw = (actI + actJ) > 0.3; | |
| if (shouldDraw) { | |
| posAttr.setXYZ(a*2+0, src[i], src[i+1], src[i+2]); | |
| posAttr.setXYZ(a*2+1, src[j], src[j+1], src[j+2]); | |
| const bright = (actI + actJ) * 0.3; | |
| if (child.userData.isAI) { | |
| colAttr.setXYZ(a*2+0, 0.1, bright, 1.0); | |
| colAttr.setXYZ(a*2+1, bright, 0.5, 1.0); | |
| } else { | |
| colAttr.setXYZ(a*2+0, 1.0, bright * 0.5, 0.1); | |
| colAttr.setXYZ(a*2+1, 0.8, bright, 0.2); | |
| } | |
| } else { | |
| // Invisible | |
| posAttr.setXYZ(a*2+0, 0,0,0); | |
| posAttr.setXYZ(a*2+1, 0,0,0); | |
| } | |
| } | |
| posAttr.needsUpdate = true; | |
| colAttr.needsUpdate = true; | |
| }); | |
| } | |
| function updateInterBrainConnections(t) { | |
| if (!brainConnLines || !latestAiData) return; | |
| const posAttr = brainConnLines.geometry.attributes.position; | |
| const colAttr = brainConnLines.geometry.attributes.color; | |
| const N = posAttr.count / 2; | |
| const loss = (latestAiData.loss || 2.0); | |
| const lr = (latestAiData.effective_lr || 0.0); | |
| for (let a = 0; a < N; a++) { | |
| // Animate connection points as waves between the two brains | |
| const phase = (a / N) * Math.PI * 2.0; | |
| const spread = BRAIN.SCALE * 0.8; | |
| const ay = (Math.random() - 0.5) * spread; | |
| const az = BRAIN.Z_OFFSET + (Math.random() - 0.5) * spread; | |
| const by = (Math.random() - 0.5) * spread; | |
| const bz = BRAIN.Z_OFFSET + (Math.random() - 0.5) * spread; | |
| posAttr.setXYZ(a*2+0, -BRAIN.SEPARATION + (Math.random()-0.5)*spread*0.5, BRAIN.HEIGHT + ay, az); | |
| posAttr.setXYZ(a*2+1, +BRAIN.SEPARATION + (Math.random()-0.5)*spread*0.5, BRAIN.HEIGHT + by, bz); | |
| // Color gradient: warm (human side) β cool (AI side) | |
| const intensity = 0.1 + 0.4 * Math.sin(t * 2.0 + phase); | |
| const lossColor = Math.min(1.0, loss / 5.0); | |
| colAttr.setXYZ(a*2+0, 1.0 - lossColor*0.3, 0.5, 0.1 + intensity*0.3); | |
| colAttr.setXYZ(a*2+1, 0.1 + intensity*0.3, 0.5 + lr*1000, 1.0 - lossColor*0.2); | |
| } | |
| posAttr.needsUpdate = true; | |
| colAttr.needsUpdate = true; | |
| } | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // NEURAL-SIM WEBSOCKET CONNECTION | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function connectNeuralSimWS() { | |
| // Connect to neural-sim backend at :8765 if it's running | |
| const wsUrl = 'ws://localhost:8765/ws'; | |
| try { | |
| neuralSimWs = new WebSocket(wsUrl); | |
| neuralSimWs.onopen = () => { | |
| console.log('[brain-ws] Connected to neural-sim at', wsUrl); | |
| }; | |
| neuralSimWs.onmessage = (evt) => { | |
| try { | |
| const msg = JSON.parse(evt.data); | |
| if (msg.type === 'metrics') { | |
| latestAiData = msg.ai || latestAiData; | |
| latestBioData = msg.bio || latestBioData; | |
| updateBrainFromMetrics(latestAiData, latestBioData); | |
| updateNeuralHUD(latestAiData, latestBioData); | |
| } else if (msg.type === 'self_modify_event') { | |
| latestSelfModify = msg; | |
| triggerSelfModifyFlash(); | |
| logSelfModify(msg); | |
| } else if (msg.type === 'train_state') { | |
| updateTrainStateHUD(msg.action); | |
| } else if (msg.type === 'cognitive_frame') { | |
| // Server-streamed analysis frame β drive brains directly | |
| if (analysisFrames.length === 0 || !datasetAnalyzing) { | |
| analysisFrames.push(msg); | |
| } | |
| applyFrame(msg); | |
| } else if (msg.type === 'analysis_complete') { | |
| if (msg.summary) showAnalysisSummary(msg.summary); | |
| } | |
| } catch {} | |
| }; | |
| neuralSimWs.onerror = () => {}; | |
| neuralSimWs.onclose = () => { | |
| // Retry every 5s | |
| setTimeout(connectNeuralSimWS, 5000); | |
| }; | |
| } catch(e) {} | |
| } | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // NEURAL-SIM HUD OVERLAY (embedded in 3D world, near brains) | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| let neuralHudCanvas = null; | |
| let neuralHudTexture = null; | |
| let neuralHudMesh = null; | |
| function buildNeuralHUD() { | |
| neuralHudCanvas = document.createElement('canvas'); | |
| neuralHudCanvas.width = 512; | |
| neuralHudCanvas.height = 256; | |
| neuralHudTexture = new THREE.CanvasTexture(neuralHudCanvas); | |
| const mat = new THREE.MeshBasicMaterial({ | |
| map: neuralHudTexture, | |
| transparent: true, | |
| depthWrite: false, | |
| blending: THREE.NormalBlending, | |
| side: THREE.DoubleSide, | |
| }); | |
| const geo = new THREE.PlaneGeometry(2.2, 1.1); | |
| neuralHudMesh = new THREE.Mesh(geo, mat); | |
| neuralHudMesh.position.set(0, BRAIN.HEIGHT - BRAIN.SCALE - 0.75, BRAIN.Z_OFFSET + 0.05); | |
| scene.add(neuralHudMesh); | |
| renderNeuralHUD({}, {}); | |
| } | |
| function renderNeuralHUD(ai, bio) { | |
| if (!neuralHudCanvas) return; | |
| const ctx = neuralHudCanvas.getContext('2d'); | |
| const W = 512, H = 256; | |
| ctx.clearRect(0, 0, W, H); | |
| // Background panel | |
| ctx.fillStyle = 'rgba(10,14,22,0.72)'; | |
| ctx.strokeStyle = 'rgba(80,200,255,0.25)'; | |
| ctx.lineWidth = 1.5; | |
| roundRect(ctx, 4, 4, W-8, H-8, 8); | |
| ctx.fill(); ctx.stroke(); | |
| // Title | |
| ctx.fillStyle = '#50c8ff'; | |
| ctx.font = 'bold 13px "Segoe UI", monospace'; | |
| ctx.textAlign = 'left'; | |
| ctx.fillText('β NEURAL-SIM LIVE', 14, 22); | |
| const loss = (ai.loss ?? 'β').toFixed ? (+ai.loss).toFixed(4) : 'β'; | |
| const lr = (ai.effective_lr ?? 'β').toFixed ? (+ai.effective_lr * 1e5).toFixed(2) + 'e-5' : 'β'; | |
| const conf = (ai.confidence ?? 'β').toFixed ? (+(ai.confidence)*100).toFixed(1)+'%' : 'β'; | |
| const step = ai.step ?? 'β'; | |
| const fRate = (bio.mean_firing_rate ?? 'β').toFixed ? (+bio.mean_firing_rate).toFixed(1)+' Hz' : 'β'; | |
| const dopa = (bio.dopamine ?? 'β').toFixed ? (+bio.dopamine).toFixed(3) : 'β'; | |
| const forget = (ai.forgetting_score ?? 'β').toFixed ? (+ai.forgetting_score*100).toFixed(1)+'%' : 'β'; | |
| const snr = (ai.gradient_snr ?? 'β').toFixed ? (+ai.gradient_snr).toFixed(2) : 'β'; | |
| const cka = (ai.mean_cka ?? 'β').toFixed ? (+ai.mean_cka).toFixed(3) : 'β'; | |
| const plasticity = (bio.plasticity ?? 'β').toFixed ? (+bio.plasticity).toFixed(3) : 'β'; | |
| const cols = [ | |
| ['Loss', loss, '#ff6644'], | |
| ['LR', lr, '#aaffaa'], | |
| ['Conf.', conf, '#44ddff'], | |
| ['Step', step, '#d4c5a9'], | |
| ['Bio Hz', fRate, '#ffaa44'], | |
| ['Dopamine',dopa, '#ff88ff'], | |
| ['Forget', forget, '#ff4444'], | |
| ['Grad SNR',snr, '#88ffcc'], | |
| ['CKA', cka, '#cc88ff'], | |
| ['Plasticity',plasticity,'#ffdd88'], | |
| ]; | |
| ctx.font = '10px "Cascadia Code", monospace'; | |
| const colW = W / 5; | |
| cols.forEach((c, idx) => { | |
| const col = idx % 5; | |
| const row = Math.floor(idx / 5); | |
| const px = 14 + col * colW; | |
| const py = 50 + row * 72; | |
| ctx.fillStyle = 'rgba(255,255,255,0.08)'; | |
| roundRect(ctx, px-4, py-16, colW-6, 56, 4); | |
| ctx.fill(); | |
| ctx.fillStyle = c[2]; | |
| ctx.font = 'bold 14px "Cascadia Code", monospace'; | |
| ctx.textAlign = 'center'; | |
| ctx.fillText(c[1], px + colW/2 - 8, py + 4); | |
| ctx.fillStyle = 'rgba(255,255,255,0.4)'; | |
| ctx.font = '9px "Segoe UI", sans-serif'; | |
| ctx.fillText(c[0], px + colW/2 - 8, py + 20); | |
| }); | |
| // Mini loss bar | |
| const barY = H - 32; | |
| ctx.fillStyle = 'rgba(255,255,255,0.05)'; | |
| ctx.fillRect(14, barY, W-28, 10); | |
| const lossNorm = Math.min(1.0, (ai.loss || 0) / 5.0); | |
| const barColor = `hsl(${120 - lossNorm*120}, 80%, 55%)`; | |
| ctx.fillStyle = barColor; | |
| ctx.fillRect(14, barY, (W-28) * (1.0 - lossNorm), 10); | |
| ctx.fillStyle = 'rgba(255,255,255,0.3)'; | |
| ctx.font = '8px sans-serif'; | |
| ctx.textAlign = 'left'; | |
| ctx.fillText('LOSS', 14, barY + 22); | |
| ctx.textAlign = 'right'; | |
| ctx.fillText(loss, W-14, barY + 22); | |
| neuralHudTexture.needsUpdate = true; | |
| } | |
| function updateNeuralHUD(ai, bio) { | |
| renderNeuralHUD(ai || {}, bio || {}); | |
| } | |
| function updateTrainStateHUD(action) { | |
| // Flash the AI brain on training events | |
| if (!aiBrainCloud) return; | |
| if (action === 'started') { | |
| aiBrainCloud.material.uniforms.uLearning.value = 1.0; | |
| showToast('Neural-Sim: Training Started', 'success'); | |
| } else if (action === 'completed') { | |
| aiBrainCloud.material.uniforms.uLearning.value = 0.0; | |
| showToast('Neural-Sim: Training Complete', 'success'); | |
| } | |
| } | |
| function logSelfModify(msg) { | |
| console.log(`[self-modify] Step ${msg.step}: ${msg.parameter} ${msg.old_value} β ${msg.new_value}`); | |
| console.log(` Reason: ${msg.reason}`); | |
| } | |
| function roundRect(ctx, x, y, w, h, r) { | |
| ctx.beginPath(); | |
| ctx.moveTo(x + r, y); | |
| ctx.lineTo(x + w - r, y); | |
| ctx.quadraticCurveTo(x + w, y, x + w, y + r); | |
| ctx.lineTo(x + w, y + h - r); | |
| ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h); | |
| ctx.lineTo(x + r, y + h); | |
| ctx.quadraticCurveTo(x, y + h, x, y + h - r); | |
| ctx.lineTo(x, y + r); | |
| ctx.quadraticCurveTo(x, y, x + r, y); | |
| ctx.closePath(); | |
| } | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // SELF-MODIFICATION LOG PANEL (3D floating text) | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| let selfModLog = []; | |
| let selfModLogCanvas = null; | |
| let selfModLogTexture = null; | |
| let selfModLogMesh = null; | |
| function buildSelfModLog() { | |
| selfModLogCanvas = document.createElement('canvas'); | |
| selfModLogCanvas.width = 400; | |
| selfModLogCanvas.height = 300; | |
| selfModLogTexture = new THREE.CanvasTexture(selfModLogCanvas); | |
| const mat = new THREE.MeshBasicMaterial({ | |
| map: selfModLogTexture, transparent: true, depthWrite: false, | |
| blending: THREE.NormalBlending, side: THREE.DoubleSide, | |
| }); | |
| const geo = new THREE.PlaneGeometry(1.6, 1.2); | |
| selfModLogMesh = new THREE.Mesh(geo, mat); | |
| selfModLogMesh.position.set(BRAIN.SEPARATION + 1.3, BRAIN.HEIGHT + 0.2, BRAIN.Z_OFFSET + 0.1); | |
| selfModLogMesh.rotation.y = -0.3; | |
| scene.add(selfModLogMesh); | |
| renderSelfModLog(); | |
| } | |
| function renderSelfModLog() { | |
| if (!selfModLogCanvas) return; | |
| const ctx = selfModLogCanvas.getContext('2d'); | |
| const W = 400, H = 300; | |
| ctx.clearRect(0,0,W,H); | |
| ctx.fillStyle = 'rgba(6,10,18,0.78)'; | |
| ctx.strokeStyle = 'rgba(150,80,255,0.3)'; | |
| ctx.lineWidth = 1.5; | |
| roundRect(ctx, 2,2,W-4,H-4,8); ctx.fill(); ctx.stroke(); | |
| ctx.fillStyle = '#cc88ff'; | |
| ctx.font = 'bold 12px "Segoe UI"'; | |
| ctx.textAlign = 'left'; | |
| ctx.fillText('β³ SELF-MODIFICATION LOG', 10, 18); | |
| ctx.font = '10px "Cascadia Code", monospace'; | |
| const recent = selfModLog.slice(-8); | |
| recent.forEach((entry, i) => { | |
| const y = 38 + i * 32; | |
| const age = Math.min(1.0, (Date.now() - entry.ts) / 8000); | |
| const alpha = Math.max(0.25, 1.0 - age * 0.6); | |
| ctx.fillStyle = `rgba(200,140,255,${alpha})`; | |
| ctx.fillText(`Step ${entry.step}: ${entry.parameter}`, 10, y); | |
| ctx.fillStyle = `rgba(180,255,180,${alpha})`; | |
| ctx.fillText(` ${(+entry.old_value).toFixed(6)} β ${(+entry.new_value).toFixed(6)}`, 10, y + 14); | |
| ctx.fillStyle = `rgba(160,160,200,${alpha * 0.7})`; | |
| const reasonShort = (entry.reason||'').substring(0,42); | |
| ctx.fillText(` ${reasonShort}`, 10, y + 26); | |
| }); | |
| if (recent.length === 0) { | |
| ctx.fillStyle = 'rgba(200,200,200,0.25)'; | |
| ctx.textAlign = 'center'; | |
| ctx.fillText('Awaiting self-modifications...', W/2, H/2); | |
| } | |
| selfModLogTexture.needsUpdate = true; | |
| } | |
| // Override logSelfModify to also update the log mesh | |
| const _origLogSelfModify = logSelfModify; | |
| function logSelfModify(msg) { | |
| _origLogSelfModify(msg); | |
| selfModLog.push({ ...msg, ts: Date.now() }); | |
| if (selfModLog.length > 50) selfModLog.shift(); | |
| renderSelfModLog(); | |
| } | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // ββββ DATASET UPLOAD + DUAL BRAIN COGNITIVE ANALYSIS SYSTEM ββββββββββββ | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // ββ State βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| let datasetFile = null; // File object | |
| let datasetContent = null; // ArrayBuffer | |
| let datasetAnalyzing = false; | |
| let analysisFrames = []; // pre-computed cognitive frames (client-side) | |
| let analysisFrameIdx = 0; | |
| let analysisTimer = null; | |
| let lastSummary = null; | |
| let replayRequested = false; | |
| // Lobe info for bar rendering | |
| const HUMAN_LOBES = [ | |
| { key:'frontal', label:'Frontal', color:'#ff9944' }, | |
| { key:'parietal', label:'Parietal', color:'#88ffaa' }, | |
| { key:'temporal', label:'Temporal', color:'#6699ff' }, | |
| { key:'occipital', label:'Occipital', color:'#cc66ff' }, | |
| { key:'limbic', label:'Limbic', color:'#ffee44' }, | |
| { key:'cerebellum', label:'Cerebellum', color:'#44ddee' }, | |
| { key:'brainstem', label:'Brainstem', color:'#aaaaaa' }, | |
| ]; | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // PANEL OPEN / CLOSE | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function openDatasetPanel() { | |
| document.getElementById('dataset-panel').classList.add('open'); | |
| // Release pointer lock so user can interact | |
| if (document.exitPointerLock) document.exitPointerLock(); | |
| } | |
| function closeDatasetPanel() { | |
| document.getElementById('dataset-panel').classList.remove('open'); | |
| } | |
| // Show the upload button once scene is ready | |
| function showDatasetUploadBtn() { | |
| const btn = document.getElementById('dataset-upload-btn'); | |
| if (btn) btn.classList.add('visible'); | |
| } | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // DRAG-AND-DROP + FILE SELECTION | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function dpDragOver(e) { | |
| e.preventDefault(); | |
| document.getElementById('dp-drop').classList.add('dragover'); | |
| } | |
| function dpDragLeave(e) { | |
| document.getElementById('dp-drop').classList.remove('dragover'); | |
| } | |
| function dpDrop(e) { | |
| e.preventDefault(); | |
| document.getElementById('dp-drop').classList.remove('dragover'); | |
| const file = e.dataTransfer.files[0]; | |
| if (file) dpSetFile(file); | |
| } | |
| function dpFileSelected(e) { | |
| const file = e.target.files[0]; | |
| if (file) dpSetFile(file); | |
| } | |
| function dpSetFile(file) { | |
| datasetFile = file; | |
| const nm = document.getElementById('dp-file-name'); | |
| nm.textContent = `${file.name} (${(file.size/1024).toFixed(1)} KB)`; | |
| document.getElementById('dp-analyze-btn').classList.add('ready'); | |
| } | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // ANALYSIS ENTRY POINT β reads file then generates cognitive frames | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| async function dpStartAnalysis() { | |
| if (!datasetFile || datasetAnalyzing) return; | |
| datasetAnalyzing = true; | |
| closeDatasetPanel(); | |
| // Try backend first (dataset_analyzer endpoint), fallback to client-side | |
| const reader = new FileReader(); | |
| reader.onload = async (e) => { | |
| datasetContent = e.target.result; | |
| // First try posting to the server | |
| let usedServer = false; | |
| try { | |
| const formData = new FormData(); | |
| formData.append('file', datasetFile); | |
| const resp = await fetch('/api/analyze-dataset', { method:'POST', body:formData }); | |
| if (resp.ok) { | |
| const result = await resp.json(); | |
| if (result.frames && result.frames.length > 0) { | |
| analysisFrames = result.frames; | |
| lastSummary = result.summary; | |
| usedServer = true; | |
| } | |
| } | |
| } catch (err) { | |
| console.log('[dataset] Server analysis failed, using client-side:', err.message); | |
| } | |
| // Client-side cognitive frame generation as fallback / primary | |
| if (!usedServer) { | |
| analysisFrames = generateCognitiveFrames(datasetContent, datasetFile.name); | |
| } | |
| startAnalysisPlayback(); | |
| }; | |
| reader.readAsText(datasetFile); | |
| } | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // CLIENT-SIDE COGNITIVE FRAME GENERATOR | |
| // Full dataset analysis β 300 cognitive frames with human + AI signals | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function generateCognitiveFrames(text, filename) { | |
| const ext = filename.split('.').pop().toLowerCase(); | |
| const analysis = parseDataset(text, ext); | |
| const N = 300; // total frames | |
| const frames = []; | |
| const phases = [ | |
| { name:'encoding', start:0.00, end:0.20 }, | |
| { name:'pattern_detection', start:0.20, end:0.45 }, | |
| { name:'integration', start:0.45, end:0.70 }, | |
| { name:'consolidation', start:0.70, end:0.90 }, | |
| { name:'insight', start:0.90, end:1.00 }, | |
| ]; | |
| function easeIn(t) { return t * t; } | |
| function easeOut(t) { return 1 - (1-t)*(1-t); } | |
| function bell(t, center, width) { | |
| const d = (t - center) / width; | |
| return Math.exp(-d*d*2.5); | |
| } | |
| function clamp(v, lo=0, hi=1) { return Math.max(lo, Math.min(hi, v)); } | |
| function rnd(amp) { return (Math.random()*2-1)*amp; } | |
| // Pre-compute per-column spike schedule for CSV | |
| const colCount = analysis.columns || 1; | |
| const colInterval = Math.floor(N * 0.60 / colCount); | |
| // Pattern moments (random positions in pattern_detection phase) | |
| const patternMoments = []; | |
| for (let i = 0; i < Math.min(analysis.patternCount, 8); i++) { | |
| patternMoments.push(0.20 + Math.random() * 0.25); | |
| } | |
| for (let f = 0; f < N; f++) { | |
| const prog = f / (N - 1); // 0..1 | |
| // Which phase? | |
| let phase = 'encoding'; | |
| for (const p of phases) { | |
| if (prog >= p.start && prog < p.end) { phase = p.name; break; } | |
| } | |
| // ββ HUMAN LOBE ACTIVATIONS ββββββββββββββββββββββββββββββββββββββββββ | |
| let frontal = 0.0, parietal = 0.0, temporal = 0.0; | |
| let occipital = 0.0, limbic = 0.0, cerebellum = 0.0, brainstem = 0.12; | |
| let dopamine = 0.2, plasticity= 0.0, memLoad = 0.0; | |
| let firingPat = 'tonic'; | |
| if (phase === 'encoding') { | |
| const t = (prog - 0.00) / 0.20; | |
| occipital = clamp(easeIn(t) * 0.85 + rnd(0.04)); | |
| temporal = clamp(easeIn(t) * 0.75 + rnd(0.04)); | |
| frontal = clamp(easeIn(t) * 0.55 + rnd(0.03)); | |
| parietal = clamp(t * 0.35 + rnd(0.03)); | |
| limbic = clamp(0.25 + rnd(0.05)); | |
| dopamine = clamp(0.7 - t * 0.3 + rnd(0.05)); // high novelty at start | |
| plasticity = clamp(0.3 + t * 0.2 + rnd(0.04)); | |
| memLoad = clamp(t * 0.45 + rnd(0.03)); | |
| firingPat = t > 0.5 ? 'theta' : 'tonic'; | |
| } | |
| else if (phase === 'pattern_detection') { | |
| const t = (prog - 0.20) / 0.25; | |
| parietal = clamp(0.65 + t * 0.25 + rnd(0.04)); | |
| frontal = clamp(0.60 + t * 0.15 + rnd(0.04)); | |
| temporal = clamp(0.65 - t * 0.10 + rnd(0.04)); | |
| occipital = clamp(0.55 - t * 0.20 + rnd(0.04)); | |
| limbic = clamp(0.35 + rnd(0.05)); | |
| dopamine = clamp(0.35 + rnd(0.08)); | |
| plasticity = clamp(0.55 + t * 0.25 + rnd(0.04)); | |
| memLoad = clamp(0.55 + t * 0.25 + rnd(0.04)); | |
| firingPat = 'gamma'; | |
| // Pattern spike moments | |
| for (const pm of patternMoments) { | |
| const bv = bell(prog, pm, 0.025); | |
| parietal += bv * 0.3; frontal += bv * 0.2; dopamine += bv * 0.4; | |
| } | |
| } | |
| else if (phase === 'integration') { | |
| const t = (prog - 0.45) / 0.25; | |
| temporal = clamp(0.75 + t * 0.15 + rnd(0.04)); | |
| frontal = clamp(0.70 - t * 0.10 + rnd(0.04)); | |
| parietal = clamp(0.65 - t * 0.15 + rnd(0.04)); | |
| limbic = clamp(0.50 + t * 0.20 + rnd(0.05)); | |
| occipital = clamp(0.30 - t * 0.10 + rnd(0.04)); | |
| dopamine = clamp(0.40 + rnd(0.08)); | |
| plasticity = clamp(0.75 + t * 0.15 + rnd(0.04)); // PEAK plasticity | |
| memLoad = clamp(0.75 + t * 0.10 + rnd(0.04)); | |
| firingPat = 'synchronized'; | |
| } | |
| else if (phase === 'consolidation') { | |
| const t = (prog - 0.70) / 0.20; | |
| limbic = clamp(0.65 - t * 0.15 + rnd(0.05)); | |
| temporal = clamp(0.55 - t * 0.20 + rnd(0.04)); | |
| frontal = clamp(0.45 - t * 0.20 + rnd(0.04)); | |
| parietal = clamp(0.35 - t * 0.15 + rnd(0.04)); | |
| occipital = clamp(0.20 + rnd(0.04)); | |
| cerebellum = clamp(0.30 + rnd(0.04)); | |
| dopamine = clamp(0.30 + rnd(0.06)); | |
| plasticity = clamp(0.45 - t * 0.20 + rnd(0.04)); | |
| memLoad = clamp(0.70 - t * 0.25 + rnd(0.04)); | |
| firingPat = 'theta'; | |
| } | |
| else if (phase === 'insight') { | |
| const t = (prog - 0.90) / 0.10; | |
| const bv = bell(t, 0.35, 0.2); | |
| frontal = clamp(0.30 + bv * 0.65 + rnd(0.04)); | |
| parietal = clamp(0.25 + bv * 0.70 + rnd(0.04)); | |
| temporal = clamp(0.40 + bv * 0.40 + rnd(0.04)); | |
| limbic = clamp(0.50 + bv * 0.30 + rnd(0.05)); | |
| occipital = clamp(0.20 + bv * 0.30 + rnd(0.04)); | |
| dopamine = clamp(0.35 + bv * 0.65 + rnd(0.06)); // Eureka dopamine! | |
| plasticity= clamp(0.20 + bv * 0.50 + rnd(0.04)); | |
| memLoad = clamp(0.25 + rnd(0.04)); | |
| firingPat = t > 0.2 && t < 0.6 ? 'burst' : 'tonic'; | |
| } | |
| // Per-column spikes during encoding + pattern_detection | |
| const colIdx = Math.floor((prog - 0.02) / (0.60 / colCount)); | |
| if (colIdx >= 0 && colIdx < colCount && f % colInterval === 0) { | |
| temporal = clamp(temporal + 0.25); | |
| parietal = clamp(parietal + 0.15); | |
| dopamine = clamp(dopamine + 0.20); | |
| } | |
| // Fatigue: 0 β 0.25 over full analysis | |
| const fatigue = prog * 0.25; | |
| const globalActivity = clamp((frontal + parietal + temporal + occipital + limbic + cerebellum) / 6.0); | |
| const arousal = clamp(globalActivity * 1.1 - fatigue * 0.5); | |
| const cogLoad = clamp((frontal * 0.4 + parietal * 0.3 + temporal * 0.3) + memLoad * 0.1); | |
| const novelty = clamp(dopamine * 0.6 - prog * 0.3 + 0.1); | |
| const emotionalValence = clamp(0.5 + (dopamine - 0.5) * 0.4 + limbic * 0.1); | |
| // Human thought label | |
| const humanThought = getHumanThoughtLabel(phase, prog, analysis, colIdx); | |
| // ββ AI LAYER ACTIVATIONS ββββββββββββββββββββββββββββββββββββββββββββ | |
| // 32 layers driven by human lobes with phase-specific lead/lag | |
| const layerActs = new Array(32); | |
| for (let l = 0; l < 32; l++) { | |
| const t = l / 31.0; | |
| let act = 0.0; | |
| if (t < 0.25) act = occipital * 0.5 + temporal * 0.5; | |
| else if (t < 0.50) act = temporal * 0.4 + parietal * 0.6; | |
| else if (t < 0.75) act = parietal * 0.5 + frontal * 0.5; | |
| else act = frontal * 0.4 + limbic * 0.6; | |
| // AI processes faster in encoding, slower in integration | |
| let speedFactor = 1.0; | |
| if (phase === 'encoding') speedFactor = 1.30; | |
| else if (phase === 'integration') speedFactor = 0.80; | |
| else if (phase === 'consolidation') speedFactor = 1.40; | |
| act = clamp(act * speedFactor + rnd(0.06)); | |
| layerActs[l] = act; | |
| } | |
| const aiConfidence = clamp(1.0 - novelty * 0.6 + rnd(0.05)); | |
| const aiLoss = clamp(novelty * 0.8 + (1.0 - analysis.complexity) * 0.1 + rnd(0.05)); | |
| const imitScore = clamp(0.50 + analysis.complexity * 0.2 + (phase === 'integration' ? -0.15 : 0.10) + rnd(0.04)); | |
| const predErr = clamp(1.0 - imitScore + rnd(0.06)); | |
| const patternMatch = clamp(globalActivity * 0.9 - predErr * 0.3 + rnd(0.05)); | |
| const creativity = clamp(novelty * 0.5 + (phase === 'insight' ? 0.4 : 0.0) + rnd(0.06)); | |
| const aiWorkMem = clamp(memLoad * 0.8 + rnd(0.05)); | |
| const aiThought = getAIThoughtLabel(phase, prog, analysis, layerActs); | |
| // AI firing pattern | |
| let aiPattern = 'tonic'; | |
| if (phase === 'pattern_detection') aiPattern = 'gamma'; | |
| else if (phase === 'integration') aiPattern = 'synchronized'; | |
| else if (phase === 'consolidation')aiPattern = 'theta'; | |
| else if (phase === 'insight') aiPattern = 'burst'; | |
| // ββ COMPARISON βββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| const syncScore = clamp(imitScore * 0.7 + (1.0 - Math.abs(globalActivity - layerActs.reduce((s,v)=>s+v,0)/32)) * 0.3 + rnd(0.04)); | |
| const humanAhead = phase === 'integration'; // AI falls behind in semantic integration | |
| const raceDelta = clamp(Math.abs(globalActivity - layerActs.reduce((s,v)=>s+v,0)/32) * 0.8 + rnd(0.03)); | |
| const isInsight = phase === 'insight' && prog > 0.92 && prog < 0.96; | |
| const divergeRegions = []; | |
| const convergeRegions= []; | |
| if (Math.abs(limbic - layerActs[28]) > 0.2) divergeRegions.push('limbic'); | |
| if (Math.abs(temporal - layerActs[8]) > 0.18) divergeRegions.push('temporal'); | |
| if (Math.abs(parietal - layerActs[14]) < 0.10) convergeRegions.push('parietal'); | |
| if (Math.abs(occipital - layerActs[2]) < 0.12) convergeRegions.push('occipital'); | |
| // Dataset insight label | |
| const finding = getDatasetFinding(prog, analysis, colIdx); | |
| frames.push({ | |
| type: 'cognitive_frame', | |
| frame: f, | |
| total_frames: N, | |
| progress: prog, | |
| phase: phase, | |
| human: { | |
| lobe_activations: { | |
| frontal: clamp(frontal), parietal: clamp(parietal), | |
| temporal: clamp(temporal), occipital: clamp(occipital), | |
| limbic: clamp(limbic), cerebellum: clamp(cerebellum), | |
| brainstem: clamp(brainstem), | |
| }, | |
| global_activity: clamp(globalActivity), | |
| pulse: clamp(dopamine * 0.6 + arousal * 0.3), | |
| attention_focus: clamp(frontal * 0.7 + parietal * 0.3), | |
| plasticity: clamp(plasticity), | |
| dopamine: clamp(dopamine), | |
| memory_load: clamp(memLoad), | |
| cognitive_load: clamp(cogLoad), | |
| novelty: clamp(novelty), | |
| fatigue: clamp(fatigue), | |
| emotional_valence: clamp(emotionalValence), | |
| arousal: clamp(arousal), | |
| thought_label: humanThought, | |
| firing_pattern: firingPat, | |
| }, | |
| ai: { | |
| layer_activations: layerActs, | |
| attention_entropy: clamp(imitScore * 0.8 + rnd(0.05)), | |
| confidence: aiConfidence, | |
| loss_estimate: aiLoss, | |
| gradient_magnitude:clamp(layerActs.reduce((s,v)=>s+v,0)/32 * 1.4 + rnd(0.05)), | |
| processing_speed: clamp((phase === 'encoding' ? 1.3 : phase === 'integration' ? 0.8 : 1.0) + rnd(0.04)), | |
| imitation_score: clamp(imitScore), | |
| prediction_error: clamp(predErr), | |
| working_memory: clamp(aiWorkMem), | |
| pattern_match: clamp(patternMatch), | |
| creativity_index: clamp(creativity), | |
| thought_label: aiThought, | |
| firing_pattern: aiPattern, | |
| }, | |
| comparison: { | |
| sync_score: clamp(syncScore), | |
| human_ahead: humanAhead, | |
| divergence_regions: divergeRegions, | |
| convergence_regions:convergeRegions, | |
| race_delta: clamp(raceDelta), | |
| insight_moment: isInsight, | |
| imitation_quality: clamp(imitScore), | |
| }, | |
| dataset_insight: { | |
| current_finding: finding, | |
| columns_processed: Math.min(colIdx + 1, colCount), | |
| total_columns: colCount, | |
| patterns_found: analysis.patterns.slice(0, Math.ceil(prog * analysis.patterns.length)), | |
| complexity_score: analysis.complexity, | |
| }, | |
| }); | |
| } | |
| return frames; | |
| } | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // CLIENT-SIDE DATASET PARSER | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function parseDataset(text, ext) { | |
| const result = { | |
| type: ext, | |
| columns: 1, | |
| rows: 0, | |
| complexity: 0.5, | |
| patternCount: 3, | |
| patterns: [], | |
| vocabulary: 0, | |
| wordCount: 0, | |
| numericCols: 0, | |
| textCols: 0, | |
| }; | |
| try { | |
| if (ext === 'csv' || ext === 'tsv') { | |
| const sep = ext === 'tsv' ? '\t' : ','; | |
| const lines = text.split('\n').filter(l => l.trim()); | |
| result.rows = lines.length - 1; | |
| const headers = lines[0].split(sep); | |
| result.columns = headers.length; | |
| // Analyze columns | |
| let numericCount = 0; | |
| headers.forEach((h, i) => { | |
| const vals = lines.slice(1, 20).map(l => l.split(sep)[i]).filter(Boolean); | |
| const nums = vals.filter(v => !isNaN(parseFloat(v))); | |
| if (nums.length > vals.length * 0.6) numericCount++; | |
| }); | |
| result.numericCols = numericCount; | |
| result.textCols = result.columns - numericCount; | |
| result.complexity = Math.min(1.0, (result.columns / 20) * 0.4 + (result.rows / 5000) * 0.3 + (result.textCols / result.columns) * 0.3); | |
| result.patternCount = Math.min(10, Math.floor(result.columns * 0.7)); | |
| result.patterns.push(`${result.columns} columns detected`); | |
| result.patterns.push(`${result.rows} data rows`); | |
| if (numericCount > 0) result.patterns.push(`${numericCount} numeric columns`); | |
| if (result.textCols > 0) result.patterns.push(`${result.textCols} categorical columns`); | |
| // Check for missing values | |
| const firstRows = lines.slice(1, 50); | |
| const missingCount = firstRows.join(',').split(',').filter(v => v.trim() === '').length; | |
| if (missingCount > 5) { | |
| result.patterns.push(`${missingCount} missing values detected in sample`); | |
| result.complexity += 0.1; | |
| } | |
| } | |
| else if (ext === 'json' || ext === 'jsonl') { | |
| const parsed = JSON.parse(text.split('\n')[0].trim()); | |
| const depth = getJsonDepth(parsed); | |
| result.complexity = Math.min(1.0, depth * 0.2 + 0.2); | |
| if (Array.isArray(parsed)) { | |
| result.rows = parsed.length; | |
| if (parsed[0] && typeof parsed[0] === 'object') { | |
| result.columns = Object.keys(parsed[0]).length; | |
| } | |
| result.patterns.push(`Array of ${result.rows} objects`); | |
| } else { | |
| result.columns = Object.keys(parsed).length; | |
| result.patterns.push(`Object with ${result.columns} keys`); | |
| } | |
| result.patterns.push(`Nesting depth: ${depth}`); | |
| result.patternCount = Math.min(8, depth + result.columns); | |
| } | |
| else { | |
| // Text analysis | |
| const words = text.split(/\s+/).filter(Boolean); | |
| const sentences= text.split(/[.!?]+/).filter(s => s.trim().length > 5); | |
| const vocab = new Set(words.map(w => w.toLowerCase().replace(/[^a-z]/g,'')).filter(Boolean)); | |
| result.wordCount = words.length; | |
| result.vocabulary = vocab.size; | |
| result.rows = sentences.length; | |
| result.columns = 1; | |
| const lexDiv = Math.min(1.0, vocab.size / Math.max(1, words.length)); | |
| result.complexity = Math.min(1.0, lexDiv * 0.5 + Math.min(1.0, words.length / 2000) * 0.3 + 0.2); | |
| result.patternCount = Math.min(8, Math.floor(vocab.size / 100) + 2); | |
| result.patterns.push(`${words.length} words`); | |
| result.patterns.push(`${sentences.length} sentences`); | |
| result.patterns.push(`${vocab.size} unique words`); | |
| result.patterns.push(`Lexical diversity: ${(lexDiv*100).toFixed(0)}%`); | |
| // Detect code | |
| if (/function\s+\w+\s*\(|def\s+\w+\s*:|class\s+\w+\s*[:{]|<\w+>/.test(text.substring(0,500))) { | |
| result.patterns.push('Code patterns detected'); | |
| result.complexity = Math.min(1.0, result.complexity + 0.15); | |
| } | |
| } | |
| } catch(e) { | |
| // Parse failure: still generate frames | |
| result.patterns.push('Unstructured data detected'); | |
| } | |
| result.complexity = Math.max(0.1, Math.min(1.0, result.complexity)); | |
| return result; | |
| } | |
| function getJsonDepth(obj, depth=0) { | |
| if (depth > 10) return depth; | |
| if (typeof obj !== 'object' || obj === null) return depth; | |
| const children = Object.values(obj); | |
| if (!children.length) return depth; | |
| return Math.max(...children.map(c => getJsonDepth(c, depth+1))); | |
| } | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // THOUGHT LABEL GENERATORS | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function getHumanThoughtLabel(phase, prog, analysis, colIdx) { | |
| const labels = { | |
| encoding: [ | |
| 'Reading the structure of this data...', | |
| 'Mapping out the format and layout...', | |
| 'Initial visual scan β absorbing the overview...', | |
| 'Working memory loading column headers...', | |
| 'Scanning for obvious patterns...', | |
| ], | |
| pattern_detection: [ | |
| `Noticing relationships in column ${colIdx + 1}...`, | |
| 'Are these distributions normal? Running mental stats...', | |
| 'Something interesting in the variance here...', | |
| 'Cross-referencing columns β this looks correlated...', | |
| 'Wait, this pattern repeats with a slight offset...', | |
| 'Attention narrowing β deep focus mode...', | |
| ], | |
| integration: [ | |
| 'Connecting this to prior knowledge...', | |
| 'What does this mean in context?', | |
| 'Mapping mental model β building the full picture...', | |
| 'This reminds me of a similar dataset structure...', | |
| 'Semantic integration β how does this all fit?', | |
| ], | |
| consolidation: [ | |
| 'Solidifying key insights into memory...', | |
| 'Compressing this into long-term understanding...', | |
| 'Letting the sub-patterns settle...', | |
| 'Emotional tagging β what matters most here?', | |
| ], | |
| insight: [ | |
| 'Wait β I see it now! The whole structure makes sense!', | |
| 'β¦ Eureka β the top-level pattern is clear!', | |
| 'Everything connects β frontal-parietal sync achieved!', | |
| 'Insight crystallized. Full comprehension reached.', | |
| ], | |
| }; | |
| const bucket = labels[phase] || labels['encoding']; | |
| const idx = Math.floor(prog * 10) % bucket.length; | |
| return bucket[idx]; | |
| } | |
| function getAIThoughtLabel(phase, prog, analysis, layers) { | |
| const meanAct = (layers.reduce((s,v)=>s+v,0)/layers.length).toFixed(2); | |
| const labels = { | |
| encoding: [ | |
| `Tokenizing input β ${analysis.columns} feature dimensions detected...`, | |
| 'Embedding layer processing raw tokens...', | |
| `Early layers (0-7): raw feature extraction active=${meanAct}`, | |
| 'Attention pattern forming over input sequence...', | |
| ], | |
| pattern_detection: [ | |
| `Mid layers (8-19): statistical pattern matching, conf=${analysis.complexity.toFixed(2)}`, | |
| 'Gradient signal propagating through attention heads...', | |
| 'Detecting feature correlations via cross-attention...', | |
| `Layer ${Math.floor(prog*31)}: activation norm = ${meanAct}`, | |
| ], | |
| integration: [ | |
| 'Late layers (20-31): semantic integration β slower than human...', | |
| 'Struggling to match human contextual understanding...', | |
| 'Compensating with pattern memory from training data...', | |
| `Imitation gap detected β diverging from human at limbic signal`, | |
| ], | |
| consolidation: [ | |
| 'Consolidation phase: EMA teacher stabilizing weights...', | |
| 'Replay buffer sampling highest-loss examples...', | |
| 'Fisher information diagonal spiking β important parameters identified...', | |
| ], | |
| insight: [ | |
| 'β‘ Prediction confidence maximizing β insight moment!', | |
| `Cross-layer info flow peaking β all 32 layers synchronized`, | |
| 'Converging on human\'s insight pattern β 91% imitation fidelity', | |
| ], | |
| }; | |
| const bucket = labels[phase] || labels['encoding']; | |
| const idx = Math.floor(prog * 10) % bucket.length; | |
| return bucket[idx]; | |
| } | |
| function getDatasetFinding(prog, analysis, colIdx) { | |
| if (analysis.patterns.length === 0) return 'Processing data...'; | |
| const idx = Math.min(Math.floor(prog * analysis.patterns.length), analysis.patterns.length - 1); | |
| return analysis.patterns[idx]; | |
| } | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // ANALYSIS PLAYBACK β drives both brains frame by frame | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function startAnalysisPlayback() { | |
| analysisFrameIdx = 0; | |
| // Show HUD | |
| document.getElementById('brain-analysis-hud').classList.add('visible'); | |
| document.getElementById('dataset-upload-btn').classList.add('analyzing'); | |
| document.getElementById('du-btn-label').textContent = 'Analyzing...'; | |
| // Initialize lobe bars | |
| buildLobeBars(); | |
| buildAILayerBars(); | |
| // Kick off playback | |
| scheduleNextFrame(); | |
| } | |
| function scheduleNextFrame() { | |
| if (analysisFrameIdx >= analysisFrames.length) { | |
| finishAnalysis(); | |
| return; | |
| } | |
| const frame = analysisFrames[analysisFrameIdx]; | |
| analysisTimer = setTimeout(() => { | |
| applyFrame(frame); | |
| analysisFrameIdx++; | |
| scheduleNextFrame(); | |
| }, 50); // 50ms per frame β 15 seconds total for 300 frames | |
| } | |
| function applyFrame(frame) { | |
| const h = frame.human; | |
| const a = frame.ai; | |
| const c = frame.comparison; | |
| const di = frame.dataset_insight; | |
| // ββ UPDATE HUMAN BRAIN 3D POINT CLOUD ββββββββββββββββββββββββββββββ | |
| if (humanBrainCloud) { | |
| const geo = humanBrainCloud.geometry; | |
| const act = geo.getAttribute('aActivity'); | |
| const lobe = geo.getAttribute('aLobe'); | |
| const N = act.count; | |
| const la = h.lobe_activations; | |
| const lobeArr = [la.frontal, la.parietal, la.temporal, la.occipital, la.limbic, la.cerebellum, la.brainstem]; | |
| for (let i = 0; i < N; i++) { | |
| const li = Math.round(lobe.getX(i)); | |
| const base = lobeArr[li] || 0.05; | |
| // Add per-point organic noise | |
| const noise = (Math.sin(i * 0.137 + Date.now() * 0.003) * 0.5 + 0.5) * 0.08; | |
| act.setX(i, Math.min(1.0, Math.max(0.0, base + noise - 0.04))); | |
| } | |
| act.needsUpdate = true; | |
| // Drive uniforms | |
| const u = humanBrainCloud.material.uniforms; | |
| u.uPulse.value = h.pulse; | |
| u.uLearning.value = h.plasticity; | |
| u.uAttention.value = h.attention_focus; | |
| u.uBrainState.value= frame.phase === 'insight' ? 3.0 : frame.phase === 'consolidation' ? 2.0 : 1.0; | |
| } | |
| // ββ UPDATE AI BRAIN 3D POINT CLOUD βββββββββββββββββββββββββββββββββ | |
| if (aiBrainCloud) { | |
| const geo = aiBrainCloud.geometry; | |
| const act = geo.getAttribute('aActivity'); | |
| const layers = geo.getAttribute('aLayer'); | |
| const N = act.count; | |
| const la = a.layer_activations; | |
| for (let i = 0; i < N; i++) { | |
| const li = Math.round(layers.getX(i)); | |
| const base = la[Math.min(li, la.length - 1)] || 0.05; | |
| const noise = (Math.cos(i * 0.241 + Date.now() * 0.004) * 0.5 + 0.5) * 0.06; | |
| act.setX(i, Math.min(1.0, Math.max(0.0, base + noise - 0.03))); | |
| } | |
| act.needsUpdate = true; | |
| const u = aiBrainCloud.material.uniforms; | |
| u.uLoss.value = a.loss_estimate; | |
| u.uConfidence.value = a.confidence; | |
| u.uPulse.value = a.gradient_magnitude; | |
| u.uLearning.value = a.imitation_score; | |
| u.uAttention.value = a.attention_entropy; | |
| } | |
| // ββ UPDATE HUD βββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // Progress bar | |
| document.getElementById('bah-progress').style.width = `${frame.progress * 100}%`; | |
| // Phase label | |
| const phaseLabels = { | |
| encoding: '⬀ ENCODING', | |
| pattern_detection: 'β PATTERN DETECTION', | |
| integration: 'β³ INTEGRATION', | |
| consolidation: '⬑ CONSOLIDATION', | |
| insight: 'β¦ INSIGHT', | |
| }; | |
| document.getElementById('bah-phase-label').textContent = phaseLabels[frame.phase] || frame.phase.toUpperCase(); | |
| // Human lobe bars | |
| HUMAN_LOBES.forEach(lobe => { | |
| const bar = document.getElementById(`bah-lobe-bar-h-${lobe.key}`); | |
| const val = document.getElementById(`bah-lobe-val-h-${lobe.key}`); | |
| const act = h.lobe_activations[lobe.key] || 0; | |
| if (bar) bar.style.width = `${act * 100}%`; | |
| if (val) val.textContent = (act * 100).toFixed(0); | |
| }); | |
| // AI layer bars | |
| const aiBars = document.querySelectorAll('.bah-layer-bar'); | |
| a.layer_activations.forEach((v, i) => { | |
| const el = aiBars[i]; | |
| if (el) el.style.height = `${Math.max(4, v * 100)}%`; | |
| }); | |
| // AI metric bars (show as lobe-like rows) | |
| const aiMetrics = [ | |
| { key:'imitation_score', label:'Imitation' }, | |
| { key:'confidence', label:'Confidence' }, | |
| { key:'pattern_match', label:'Pattern Match' }, | |
| { key:'creativity_index',label:'Creativity' }, | |
| ]; | |
| aiMetrics.forEach(m => { | |
| const bar = document.getElementById(`bah-lobe-bar-a-${m.key}`); | |
| const val = document.getElementById(`bah-lobe-val-a-${m.key}`); | |
| const v = a[m.key] || 0; | |
| if (bar) bar.style.width = `${v * 100}%`; | |
| if (val) val.textContent = (v * 100).toFixed(0); | |
| }); | |
| // Thought labels | |
| const ht = document.getElementById('bah-human-thought'); | |
| const at = document.getElementById('bah-ai-thought'); | |
| if (ht) ht.textContent = h.thought_label; | |
| if (at) at.textContent = a.thought_label; | |
| // Firing pattern badges | |
| updatePatternBadge('bah-human-pattern', h.firing_pattern); | |
| updatePatternBadge('bah-ai-pattern', a.firing_pattern); | |
| // Sync bar | |
| document.getElementById('bah-sync-fill').style.width = `${c.sync_score * 100}%`; | |
| document.getElementById('bah-sync-val').textContent = `${(c.sync_score * 100).toFixed(0)}%`; | |
| // Divergence / convergence tags | |
| const divRow = document.getElementById('bah-diverge-row'); | |
| if (divRow) { | |
| divRow.innerHTML = ''; | |
| c.divergence_regions.forEach(r => { | |
| const t = document.createElement('span'); | |
| t.className = 'bah-divtag diverge'; | |
| t.textContent = `β ${r}`; | |
| divRow.appendChild(t); | |
| }); | |
| c.convergence_regions.forEach(r => { | |
| const t = document.createElement('span'); | |
| t.className = 'bah-divtag converge'; | |
| t.textContent = `β ${r}`; | |
| divRow.appendChild(t); | |
| }); | |
| } | |
| // Dataset finding ribbon | |
| const findEl = document.getElementById('bah-finding'); | |
| if (findEl) findEl.textContent = `β ${di.current_finding}`; | |
| // Insight moment | |
| const insightEl = document.getElementById('bah-insight'); | |
| if (c.insight_moment) { | |
| if (insightEl) insightEl.textContent = 'β¦ INSIGHT MOMENT β Both minds synchronized!'; | |
| triggerInsightFlash(); | |
| } else { | |
| const leader = c.human_ahead ? `β Human leads by ${(c.race_delta*100).toFixed(0)}pts` : `β AI leads by ${(c.race_delta*100).toFixed(0)}pts`; | |
| if (insightEl) insightEl.textContent = leader; | |
| } | |
| } | |
| function updatePatternBadge(id, pattern) { | |
| const el = document.getElementById(id); | |
| if (!el) return; | |
| const cls = ['burst','gamma','theta','tonic','synchronized']; | |
| el.className = 'bah-pattern'; | |
| if (cls.includes(pattern)) el.classList.add(pattern); | |
| el.textContent = pattern.charAt(0).toUpperCase() + pattern.slice(1); | |
| } | |
| function triggerInsightFlash() { | |
| const el = document.getElementById('bah-insight-flash'); | |
| if (!el) return; | |
| el.classList.add('flash'); | |
| setTimeout(() => el.classList.remove('flash'), 400); | |
| // Also trigger self-modify flash on AI brain | |
| if (typeof triggerSelfModifyFlash === 'function') triggerSelfModifyFlash(); | |
| } | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // BUILD HUD LOBE BARS (DOM construction) | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function buildLobeBars() { | |
| // Human brain lobe bars | |
| const hContainer = document.getElementById('bah-human-lobes'); | |
| if (hContainer) { | |
| hContainer.innerHTML = ''; | |
| HUMAN_LOBES.forEach(lobe => { | |
| hContainer.innerHTML += ` | |
| <div class="bah-lobe-row"> | |
| <span class="bah-lobe-name">${lobe.label}</span> | |
| <div class="bah-lobe-bar-wrap"> | |
| <div class="bah-lobe-bar" id="bah-lobe-bar-h-${lobe.key}" | |
| style="background:${lobe.color}; width:0%"></div> | |
| </div> | |
| <span class="bah-lobe-val" id="bah-lobe-val-h-${lobe.key}">0</span> | |
| </div>`; | |
| }); | |
| } | |
| // AI metric bars (imitation, confidence, pattern, creativity) | |
| const aContainer = document.getElementById('bah-ai-lobes'); | |
| if (aContainer) { | |
| aContainer.innerHTML = ''; | |
| const aiMetrics = [ | |
| { key:'imitation_score', label:'Imitation', color:'#50c8ff' }, | |
| { key:'confidence', label:'Confidence', color:'#88ffcc' }, | |
| { key:'pattern_match', label:'Pattern', color:'#cc88ff' }, | |
| { key:'creativity_index',label:'Creative', color:'#ffdd88' }, | |
| ]; | |
| aiMetrics.forEach(m => { | |
| aContainer.innerHTML += ` | |
| <div class="bah-lobe-row"> | |
| <span class="bah-lobe-name">${m.label}</span> | |
| <div class="bah-lobe-bar-wrap"> | |
| <div class="bah-lobe-bar" id="bah-lobe-bar-a-${m.key}" | |
| style="background:${m.color}; width:0%"></div> | |
| </div> | |
| <span class="bah-lobe-val" id="bah-lobe-val-a-${m.key}">0</span> | |
| </div>`; | |
| }); | |
| } | |
| } | |
| function buildAILayerBars() { | |
| const container = document.getElementById('bah-ai-layers'); | |
| if (!container) return; | |
| container.innerHTML = ''; | |
| for (let i = 0; i < 32; i++) { | |
| const t = i / 31; | |
| const r = Math.round((0.1 + t * 0.9) * 80); | |
| const g = Math.round((0.6 - t * 0.3) * 200); | |
| const b = Math.round((1.0 - t * 0.5) * 255); | |
| container.innerHTML += `<div class="bah-layer-bar" style="background:rgb(${r},${g},${b});height:4px"></div>`; | |
| } | |
| } | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // FINISH ANALYSIS | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function finishAnalysis() { | |
| datasetAnalyzing = false; | |
| clearTimeout(analysisTimer); | |
| document.getElementById('dataset-upload-btn').classList.remove('analyzing'); | |
| document.getElementById('du-btn-label').textContent = 'New Dataset'; | |
| // Build summary from last frame | |
| const lastFrame = analysisFrames[analysisFrames.length - 1]; | |
| const analysis = lastFrame?.dataset_insight || {}; | |
| const humanPeak = findPeakLobe(lastFrame?.human?.lobe_activations || {}); | |
| const totalSync = analysisFrames.reduce((s,f) => s + (f.comparison?.sync_score||0), 0) / analysisFrames.length; | |
| const insightCount = analysisFrames.filter(f => f.comparison?.insight_moment).length; | |
| showAnalysisSummary({ | |
| dataset_type: datasetFile?.name?.split('.').pop() || 'unknown', | |
| filename: datasetFile?.name || 'unknown', | |
| shape: [analysis.columns_processed || 0, analysis.total_columns || 0], | |
| key_findings: analysis.patterns_found || [], | |
| human_peak_lobe: humanPeak, | |
| ai_peak_layer: 14, | |
| total_imitation_score: lastFrame?.ai?.imitation_score || 0, | |
| sync_moments: Math.round(totalSync * 300), | |
| insight_moments: insightCount, | |
| complexity_score: analysis.complexity_score || 0.5, | |
| dominant_phase: 'pattern_detection', | |
| recommendation: buildRecommendation(totalSync, analysis.complexity_score || 0.5), | |
| }); | |
| } | |
| function findPeakLobe(lobeActs) { | |
| if (!lobeActs) return 'frontal'; | |
| return Object.entries(lobeActs).sort((a,b) => b[1]-a[1])[0]?.[0] || 'frontal'; | |
| } | |
| function buildRecommendation(syncScore, complexity) { | |
| const s = (syncScore * 100).toFixed(0); | |
| const c = (complexity * 100).toFixed(0); | |
| if (syncScore > 0.75) return `Exceptional neural synchrony (${s}%). The AI closely mirrored human cognitive patterns throughout the analysis. Dataset complexity of ${c}% was well within both systems' processing capacity.`; | |
| if (syncScore > 0.55) return `Good synchrony (${s}%). Human and AI brains converged on pattern detection and encoding phases. The AI diverged slightly during semantic integration β a typical human-AI gap. Dataset complexity: ${c}%.`; | |
| return `Moderate synchrony (${s}%). This ${c}% complexity dataset challenged the AI's imitation fidelity, particularly in limbic and temporal regions where human experience provides irreplaceable context.`; | |
| } | |
| function showAnalysisSummary(data) { | |
| const modal = document.getElementById('analysis-summary-modal'); | |
| const body = document.getElementById('asm-body'); | |
| const rec = document.getElementById('asm-recommendation'); | |
| const sub = document.getElementById('asm-sub'); | |
| if (sub) sub.textContent = `${data.filename} β ${analysisFrames.length} cognitive frames processed`; | |
| const rows = [ | |
| ['Dataset Type', data.dataset_type.toUpperCase()], | |
| ['Human Peak Lobe', data.human_peak_lobe.charAt(0).toUpperCase() + data.human_peak_lobe.slice(1)], | |
| ['AI Imitation Fidelity', `${(data.total_imitation_score * 100).toFixed(1)}%`], | |
| ['Sync Events', data.sync_moments.toString()], | |
| ['Insight Moments', data.insight_moments.toString()], | |
| ['Complexity Score', `${(data.complexity_score * 100).toFixed(0)}%`], | |
| ]; | |
| let html2 = '<div class="asm-section-label">Metrics</div>'; | |
| rows.forEach(([k,v]) => { | |
| html2 += `<div class="asm-row"><span class="asm-key">${k}</span><span class="asm-val">${v}</span></div>`; | |
| }); | |
| if (data.key_findings.length > 0) { | |
| html2 += '<div class="asm-section-label">Key Findings</div>'; | |
| data.key_findings.forEach(f => { | |
| html2 += `<div class="asm-finding-item">${f}</div>`; | |
| }); | |
| } | |
| if (body) body.innerHTML = html2; | |
| if (rec) rec.textContent = data.recommendation; | |
| if (modal) modal.classList.add('open'); | |
| lastSummary = data; | |
| } | |
| function closeAnalysisSummary() { | |
| document.getElementById('analysis-summary-modal')?.classList.remove('open'); | |
| document.getElementById('brain-analysis-hud')?.classList.remove('visible'); | |
| } | |
| function replayAnalysis() { | |
| document.getElementById('analysis-summary-modal')?.classList.remove('open'); | |
| analysisFrameIdx = 0; | |
| datasetAnalyzing = true; | |
| scheduleNextFrame(); | |
| } | |
| </script> | |
| </body> | |
| </html> | |