neural-sim / index.html
ftrtemis's picture
Deploy: Neural Simulation Dashboard v1.0
6154e7f verified
Raw
History Blame Contribute Delete
254 kB
<!DOCTYPE html>
<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 &middot; Mouse to look &middot; E to interact &middot; 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) => ({ '&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;' }[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>