Download index.html from DavidVivancos/Neuraxon2MiniControl: direct link, hf CLI and curl.
- Browser
- Download file 89.8 kB
-
https://huggingface.co/spaces/DavidVivancos/Neuraxon2MiniControl/resolve/main/index.html
- Command line
-
hf download hf://spaces/DavidVivancos/Neuraxon2MiniControl/index.html
-
curl -L -o index.html https://huggingface.co/spaces/DavidVivancos/Neuraxon2MiniControl/resolve/main/index.html
89.8 kB
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Sphero Γ Neuraxon 2.0 β Bio-Inspired Neural Control</title> | |
| <style> | |
| @import url('https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap'); | |
| :root { | |
| --bg-abyss: #030709; | |
| --bg-deep: #060d14; | |
| --bg-card: #0a1520; | |
| --bg-raised: #0e1a28; | |
| --bg-glass: rgba(10,21,32,0.75); | |
| --border-faint: rgba(34,211,238,0.08); | |
| --border-glow: rgba(34,211,238,0.2); | |
| --border-active: rgba(34,211,238,0.5); | |
| --txt-primary: #e0f2fe; | |
| --txt-secondary: #7aa3c0; | |
| --txt-dim: #3a5f7a; | |
| --cyan: #22d3ee; | |
| --cyan-bright: #67e8f9; | |
| --green: #34d399; | |
| --green-dim: rgba(52,211,153,0.15); | |
| --red: #fb7185; | |
| --red-dim: rgba(251,113,133,0.15); | |
| --amber: #fbbf24; | |
| --purple: #a78bfa; | |
| --pink: #f472b6; | |
| --blue: #60a5fa; | |
| --excite: #22d3ee; | |
| --neutral: #475569; | |
| --inhibit: #f472b6; | |
| --da-color: #fbbf24; | |
| --sht-color: #a78bfa; | |
| --ach-color: #34d399; | |
| --na-color: #fb7185; | |
| --mono: 'JetBrains Mono', monospace; | |
| --sans: 'Outfit', system-ui, sans-serif; | |
| } | |
| * { box-sizing: border-box; margin: 0; padding: 0; } | |
| html { height: 100%; } | |
| body { | |
| min-height: 100%; background: var(--bg-abyss); color: var(--txt-primary); | |
| font-family: var(--sans); overflow-x: hidden; | |
| background-image: | |
| radial-gradient(ellipse 80% 50% at 20% 80%, rgba(34,211,238,0.03) 0%, transparent 60%), | |
| radial-gradient(ellipse 60% 40% at 80% 20%, rgba(164,139,250,0.03) 0%, transparent 60%); | |
| } | |
| /* === HEADER === */ | |
| header { | |
| position: sticky; top: 0; z-index: 100; | |
| display: flex; justify-content: space-between; align-items: center; | |
| padding: 12px 24px; | |
| background: rgba(3,7,9,0.9); backdrop-filter: blur(20px) saturate(1.5); | |
| border-bottom: 1px solid var(--border-faint); | |
| } | |
| .logo-area { display: flex; align-items: center; gap: 12px; } | |
| .logo-area h1 { | |
| font-size: 17px; font-weight: 700; letter-spacing: -0.02em; | |
| background: linear-gradient(135deg, var(--cyan), var(--purple)); | |
| -webkit-background-clip: text; -webkit-text-fill-color: transparent; | |
| } | |
| .logo-area .subtitle { font-size: 10px; color: var(--txt-dim); font-family: var(--mono); letter-spacing: 0.08em; text-transform: uppercase; } | |
| .header-right { display: flex; align-items: center; gap: 14px; } | |
| .status-badge { | |
| font-family: var(--mono); font-size: 10px; font-weight: 600; letter-spacing: 0.06em; | |
| padding: 5px 14px; border-radius: 20px; border: 1px solid; | |
| display: flex; align-items: center; gap: 6px; | |
| } | |
| .status-badge::before { content: ''; width: 6px; height: 6px; border-radius: 50%; } | |
| .status-badge.on { color: var(--green); border-color: rgba(52,211,153,0.3); } | |
| .status-badge.on::before { background: var(--green); box-shadow: 0 0 8px var(--green); } | |
| .status-badge.off { color: var(--red); border-color: rgba(251,113,133,0.2); } | |
| .status-badge.off::before { background: var(--red); } | |
| .mode-indicator { | |
| font-family: var(--mono); font-size: 10px; font-weight: 500; | |
| padding: 5px 12px; border-radius: 6px; letter-spacing: 0.05em; | |
| background: rgba(34,211,238,0.08); color: var(--cyan); border: 1px solid rgba(34,211,238,0.15); | |
| } | |
| /* === LAYOUT === */ | |
| .app-grid { | |
| display: grid; | |
| grid-template-columns: 280px 1fr 260px; | |
| grid-template-rows: auto 1fr auto; | |
| gap: 0; | |
| height: calc(100vh - 49px); | |
| max-height: calc(100vh - 49px); | |
| } | |
| @media (max-width: 1024px) { | |
| .app-grid { grid-template-columns: 1fr; grid-template-rows: auto; height: auto; } | |
| .panel-left, .panel-right { border: none ; } | |
| } | |
| /* === PANELS === */ | |
| .panel-left { | |
| grid-row: 1 / -1; | |
| border-right: 1px solid var(--border-faint); | |
| background: var(--bg-deep); | |
| overflow-y: auto; padding: 16px; | |
| display: flex; flex-direction: column; gap: 14px; | |
| } | |
| .panel-center { | |
| grid-row: 1 / -1; | |
| display: flex; flex-direction: column; | |
| position: relative; overflow: hidden; | |
| cursor: grab; | |
| } | |
| .panel-center:active { cursor: grabbing; } | |
| .panel-right { | |
| grid-row: 1 / -1; | |
| border-left: 1px solid var(--border-faint); | |
| background: var(--bg-deep); | |
| overflow-y: auto; padding: 16px; | |
| display: flex; flex-direction: column; gap: 14px; | |
| } | |
| /* === CARDS === */ | |
| .card { | |
| background: var(--bg-card); border: 1px solid var(--border-faint); | |
| border-radius: 10px; padding: 14px; position: relative; | |
| } | |
| .card-title { | |
| font-size: 10px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.1em; | |
| color: var(--txt-dim); margin-bottom: 12px; display: flex; align-items: center; gap: 6px; | |
| } | |
| .card-title .dot { width: 5px; height: 5px; border-radius: 50%; } | |
| /* === CONNECTION === */ | |
| .connect-row { display: flex; gap: 8px; } | |
| .btn { | |
| flex: 1; padding: 10px 16px; border: none; border-radius: 8px; | |
| font-family: var(--sans); font-size: 12px; font-weight: 600; | |
| cursor: pointer; transition: all 0.2s; | |
| } | |
| .btn:disabled { opacity: 0.35; cursor: not-allowed; } | |
| .btn-primary { | |
| background: linear-gradient(135deg, var(--cyan), #3b82f6); | |
| color: #fff; box-shadow: 0 2px 12px rgba(34,211,238,0.2); | |
| } | |
| .btn-primary:hover:not(:disabled) { transform: translateY(-1px); box-shadow: 0 4px 20px rgba(34,211,238,0.3); } | |
| .btn-secondary { background: var(--bg-raised); color: var(--txt-secondary); border: 1px solid var(--border-faint); } | |
| .btn-secondary:hover:not(:disabled) { border-color: var(--border-glow); } | |
| /* === MODE SELECTOR === */ | |
| .mode-group { display: flex; gap: 4px; background: var(--bg-abyss); border-radius: 8px; padding: 3px; } | |
| .mode-btn { | |
| flex: 1; padding: 8px 6px; border: none; border-radius: 6px; cursor: pointer; | |
| font-family: var(--mono); font-size: 10px; font-weight: 500; letter-spacing: 0.03em; | |
| background: transparent; color: var(--txt-dim); transition: all 0.2s; | |
| } | |
| .mode-btn.active { background: var(--bg-raised); color: var(--cyan); box-shadow: 0 0 12px rgba(34,211,238,0.1); } | |
| .mode-btn:hover:not(.active) { color: var(--txt-secondary); } | |
| /* === NEUROMOD SLIDERS === */ | |
| .neuromod-row { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; } | |
| .neuromod-label { font-family: var(--mono); font-size: 10px; font-weight: 600; width: 28px; text-align: right; } | |
| .neuromod-slider { | |
| flex: 1; -webkit-appearance: none; appearance: none; | |
| height: 4px; border-radius: 2px; outline: none; background: var(--bg-abyss); | |
| } | |
| .neuromod-slider::-webkit-slider-thumb { | |
| -webkit-appearance: none; width: 14px; height: 14px; | |
| border-radius: 50%; cursor: pointer; border: 2px solid; | |
| } | |
| .neuromod-val { font-family: var(--mono); font-size: 10px; width: 32px; color: var(--txt-dim); } | |
| .nm-da .neuromod-label { color: var(--da-color); } | |
| .nm-da .neuromod-slider::-webkit-slider-thumb { background: var(--da-color); border-color: var(--da-color); } | |
| .nm-sht .neuromod-label { color: var(--sht-color); } | |
| .nm-sht .neuromod-slider::-webkit-slider-thumb { background: var(--sht-color); border-color: var(--sht-color); } | |
| .nm-ach .neuromod-label { color: var(--ach-color); } | |
| .nm-ach .neuromod-slider::-webkit-slider-thumb { background: var(--ach-color); border-color: var(--ach-color); } | |
| .nm-na .neuromod-label { color: var(--na-color); } | |
| .nm-na .neuromod-slider::-webkit-slider-thumb { background: var(--na-color); border-color: var(--na-color); } | |
| /* === METRICS === */ | |
| .metrics-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; } | |
| .metric-box { | |
| background: var(--bg-abyss); border-radius: 6px; padding: 8px 10px; | |
| border: 1px solid var(--border-faint); | |
| } | |
| .metric-label { font-size: 9px; color: var(--txt-dim); font-family: var(--mono); text-transform: uppercase; letter-spacing: 0.08em; } | |
| .metric-value { font-size: 16px; font-weight: 700; margin-top: 2px; } | |
| .metric-value.exc { color: var(--excite); } | |
| .metric-value.inh { color: var(--inhibit); } | |
| .metric-value.neu { color: var(--txt-dim); } | |
| /* === D-PAD === */ | |
| .dpad-container { display: flex; justify-content: center; } | |
| .dpad-grid { | |
| display: grid; grid-template-columns: 54px 54px 54px; grid-template-rows: 54px 54px 54px; | |
| gap: 5px; | |
| } | |
| .dpad-btn { | |
| width: 54px; height: 54px; border-radius: 10px; border: 1.5px solid var(--border-glow); | |
| background: var(--bg-raised); color: var(--txt-dim); font-size: 18px; font-weight: 700; | |
| display: grid; place-items: center; cursor: pointer; transition: all 0.1s; | |
| user-select: none; -webkit-user-select: none; font-family: var(--sans); | |
| } | |
| .dpad-btn:active, .dpad-btn.active { | |
| background: rgba(34,211,238,0.15); border-color: var(--cyan); color: var(--cyan); | |
| box-shadow: 0 0 14px rgba(34,211,238,0.25); transform: scale(0.94); | |
| } | |
| .dpad-btn.stop-btn { font-size: 9px; font-family: var(--mono); border-color: rgba(251,113,133,0.3); color: var(--red); font-weight: 600; } | |
| .dpad-btn.stop-btn:active, .dpad-btn.stop-btn.active { background: rgba(251,113,133,0.15); box-shadow: 0 0 14px rgba(251,113,133,0.2); } | |
| .dpad-hidden { visibility: hidden; } | |
| .dpad-hint { text-align: center; font-size: 9px; color: var(--txt-dim); font-family: var(--mono); margin-top: 8px; } | |
| /* === CANVAS === */ | |
| #neuralCanvas { | |
| width: 100%; height: 100%; display: block; | |
| background: transparent; | |
| } | |
| .canvas-overlay { | |
| position: absolute; bottom: 12px; left: 12px; right: 12px; | |
| display: flex; justify-content: space-between; align-items: flex-end; | |
| pointer-events: none; | |
| } | |
| .overlay-chip { | |
| font-family: var(--mono); font-size: 9px; letter-spacing: 0.05em; | |
| padding: 4px 10px; border-radius: 4px; | |
| background: rgba(3,7,9,0.8); color: var(--txt-dim); | |
| border: 1px solid var(--border-faint); backdrop-filter: blur(8px); | |
| } | |
| .overlay-chip span { color: var(--cyan); } | |
| /* === FOCUSED NODE HUD === */ | |
| .focus-hud { | |
| position: absolute; top: 14px; left: 50%; transform: translateX(-50%); | |
| pointer-events: none; z-index: 10; | |
| background: rgba(3,7,9,0.88); backdrop-filter: blur(16px) saturate(1.4); | |
| border: 1px solid rgba(251,191,36,0.3); border-radius: 10px; | |
| padding: 10px 18px; min-width: 220px; | |
| font-family: var(--mono); font-size: 10px; | |
| display: flex; gap: 14px; align-items: center; | |
| opacity: 0; transition: opacity 0.25s ease; | |
| box-shadow: 0 4px 30px rgba(0,0,0,0.5), 0 0 20px rgba(251,191,36,0.05); | |
| } | |
| .focus-hud.visible { opacity: 1; } | |
| .focus-hud .node-id { font-size: 16px; font-weight: 800; min-width: 30px; text-align: center; } | |
| .focus-hud .node-meta { display: flex; flex-direction: column; gap: 2px; } | |
| .focus-hud .meta-row { display: flex; gap: 8px; } | |
| .focus-hud .meta-label { color: var(--txt-dim); font-size: 9px; width: 50px; } | |
| .focus-hud .meta-val { font-weight: 600; font-size: 10px; } | |
| /* === INTERACTION HINT === */ | |
| .pan-hint { | |
| position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); | |
| pointer-events: none; z-index: 5; | |
| font-family: var(--mono); font-size: 11px; color: rgba(224,242,254,0.25); | |
| letter-spacing: 0.05em; text-align: center; | |
| transition: opacity 1.2s ease; | |
| } | |
| .pan-hint.hidden { opacity: 0; } | |
| /* === LEGEND === */ | |
| .legend-row { display: flex; gap: 14px; justify-content: center; flex-wrap: wrap; } | |
| .legend-item { display: flex; align-items: center; gap: 5px; font-size: 9px; font-family: var(--mono); color: var(--txt-dim); } | |
| .legend-dot { width: 8px; height: 8px; border-radius: 50%; } | |
| .legend-dot.exc { background: var(--excite); box-shadow: 0 0 6px var(--excite); } | |
| .legend-dot.inh { background: var(--inhibit); box-shadow: 0 0 6px var(--inhibit); } | |
| .legend-dot.neu { background: var(--neutral); } | |
| /* === LOG === */ | |
| .log-panel { | |
| font-family: var(--mono); font-size: 10px; line-height: 1.6; | |
| overflow-y: auto; padding: 10px; max-height: 160px; | |
| background: rgba(0,0,0,0.4); border: 1px solid var(--border-faint); | |
| border-radius: 8px; color: var(--txt-dim); white-space: pre-wrap; | |
| } | |
| .log-panel::-webkit-scrollbar { width: 4px; } | |
| .log-panel::-webkit-scrollbar-thumb { background: var(--border-faint); border-radius: 2px; } | |
| /* === MOTOR OUTPUT === */ | |
| .motor-display { display: flex; gap: 10px; align-items: center; justify-content: center; } | |
| .motor-dial { | |
| width: 80px; height: 80px; border-radius: 50%; | |
| border: 2px solid var(--border-glow); background: var(--bg-abyss); | |
| position: relative; display: flex; align-items: center; justify-content: center; | |
| } | |
| .motor-dial .needle { | |
| width: 2px; height: 30px; background: var(--cyan); | |
| position: absolute; bottom: 50%; left: calc(50% - 1px); | |
| transform-origin: bottom center; transition: transform 0.15s; | |
| border-radius: 1px; box-shadow: 0 0 6px var(--cyan); | |
| } | |
| .motor-dial .center-dot { | |
| width: 8px; height: 8px; border-radius: 50%; | |
| background: var(--cyan); position: absolute; | |
| box-shadow: 0 0 10px var(--cyan); | |
| } | |
| .motor-label { font-family: var(--mono); font-size: 9px; color: var(--txt-dim); text-align: center; margin-top: 4px; } | |
| .motor-val { font-family: var(--mono); font-size: 14px; font-weight: 700; color: var(--cyan); text-align: center; } | |
| /* === ACTIVITY BAR === */ | |
| .activity-bar { | |
| height: 40px; background: var(--bg-abyss); border-radius: 6px; | |
| border: 1px solid var(--border-faint); overflow: hidden; | |
| display: flex; position: relative; | |
| } | |
| .activity-bar canvas { width: 100%; height: 100%; } | |
| /* === SCROLLBAR === */ | |
| .panel-left::-webkit-scrollbar, .panel-right::-webkit-scrollbar { width: 4px; } | |
| .panel-left::-webkit-scrollbar-thumb, .panel-right::-webkit-scrollbar-thumb { background: var(--border-faint); border-radius: 2px; } | |
| .controls-disabled { opacity: 0.3; pointer-events: none; } | |
| /* === LEARNING PROGRESS BAR === */ | |
| .progress-track { | |
| width: 100%; height: 18px; background: var(--bg-abyss); | |
| border-radius: 9px; border: 1px solid var(--border-faint); | |
| overflow: hidden; position: relative; | |
| } | |
| .progress-fill { | |
| height: 100%; border-radius: 9px; transition: width 0.4s ease; | |
| background: linear-gradient(90deg, rgba(251,191,36,0.4), rgba(52,211,153,0.8)); | |
| position: relative; | |
| } | |
| .progress-fill.learned { | |
| background: linear-gradient(90deg, rgba(52,211,153,0.6), rgba(34,211,238,0.9)); | |
| } | |
| .progress-text { | |
| position: absolute; top: 0; left: 0; right: 0; bottom: 0; | |
| display: flex; align-items: center; justify-content: center; | |
| font-family: var(--mono); font-size: 9px; font-weight: 600; | |
| color: var(--txt-primary); text-shadow: 0 1px 3px rgba(0,0,0,0.6); | |
| pointer-events: none; z-index: 1; | |
| } | |
| .nxon-source-tag { | |
| font-family: var(--mono); font-size: 9px; font-weight: 600; | |
| padding: 3px 8px; border-radius: 4px; letter-spacing: 0.04em; | |
| } | |
| .nxon-source-tag.human { background: rgba(52,211,153,0.15); color: var(--green); } | |
| .nxon-source-tag.nxon { background: rgba(164,139,250,0.15); color: var(--purple); } | |
| .nxon-source-tag.learning { background: rgba(251,191,36,0.15); color: var(--amber); } | |
| </style> | |
| </head> | |
| <body> | |
| <header> | |
| <div class="logo-area"> | |
| <div> | |
| <h1> <a href="https://github.com/DavidVivancos/Neuraxon"> Neuraxon 2.0</a> <a href="https://sphero.com/collections/mini"> Sphero Mini<a> Control</h1> | |
| <div class="subtitle">Bio-Inspired Neural Control By <a href="https://www.vivancos.com/">David Vivancos</a> & <a href="https://josesanchezgarcia.com/">Jose Sanchez</a> for <a href="https://qubic.org/">Qubic</a> Open Science </div> | |
| </div> | |
| </div> | |
| <div class="header-right"> | |
| <div class="mode-indicator" id="modeLabel">MANUAL</div> | |
| <div class="status-badge off" id="connStatus">OFFLINE</div> | |
| </div> | |
| </header> | |
| <div class="app-grid"> | |
| <!-- =================== LEFT PANEL =================== --> | |
| <div class="panel-left"> | |
| <div class="card"> | |
| <div class="card-title"><div class="dot" style="background:var(--cyan)"></div> SPHERO CONNECTION</div> | |
| <div class="connect-row"> | |
| <button class="btn btn-primary" id="btnConnect">Connect</button> | |
| <button class="btn btn-secondary" id="btnDisconnect" disabled>Disconnect</button> | |
| </div> | |
| </div> | |
| <div class="card"> | |
| <div class="card-title"><div class="dot" style="background:var(--purple)"></div> CONTROL MODE</div> | |
| <div class="mode-group"> | |
| <button class="mode-btn active" data-mode="manual" onclick="setMode('manual')">Manual</button> | |
| <button class="mode-btn" data-mode="hybrid" onclick="setMode('hybrid')">Hybrid</button> | |
| </div> | |
| </div> | |
| <div class="card" id="driveCard"> | |
| <div class="card-title"><div class="dot" style="background:var(--green)"></div> HUMAN DRIVE</div> | |
| <div class="dpad-container"> | |
| <div class="dpad-grid"> | |
| <div class="dpad-hidden"></div> | |
| <div class="dpad-btn" id="btnUp">W</div> | |
| <div class="dpad-hidden"></div> | |
| <div class="dpad-btn" id="btnLeft">A</div> | |
| <div class="dpad-btn stop-btn" id="btnBrake">STOP</div> | |
| <div class="dpad-btn" id="btnRight">D</div> | |
| <div class="dpad-hidden"></div> | |
| <div class="dpad-btn" id="btnDown">S</div> | |
| <div class="dpad-hidden"></div> | |
| </div> | |
| </div> | |
| <div class="dpad-hint">WASD / Arrows / Space</div> | |
| </div> | |
| <div class="card"> | |
| <div class="card-title"><div class="dot" style="background:var(--amber)"></div> MOTOR OUTPUT</div> | |
| <div class="motor-display"> | |
| <div> | |
| <div class="motor-dial" id="headingDial"> | |
| <div class="needle" id="headingNeedle"></div> | |
| <div class="center-dot"></div> | |
| </div> | |
| <div class="motor-label">HEADING</div> | |
| <div class="motor-val" id="headingVal">0Β°</div> | |
| </div> | |
| <div style="text-align:center;"> | |
| <div class="metric-box" style="width:80px;padding:12px;"> | |
| <div class="metric-label">SPEED</div> | |
| <div class="metric-value exc" id="speedVal" style="font-size:22px;">0</div> | |
| </div> | |
| <div style="margin-top:6px;"> | |
| <div class="metric-label" style="font-size:8px;">BLEND</div> | |
| <div class="motor-val" id="blendVal" style="font-size:11px;color:var(--purple);">H:100% N:0%</div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="card" style="flex:1;min-height:100px;display:flex;flex-direction:column;"> | |
| <div class="card-title"><div class="dot" style="background:var(--txt-dim)"></div> SYSTEM LOG</div> | |
| <div class="log-panel" id="logPanel" style="flex:1;max-height:none;"></div> | |
| </div> | |
| </div> | |
| <!-- =================== CENTER - NEURAL VIS =================== --> | |
| <div class="panel-center" id="panelCenter"> | |
| <canvas id="neuralCanvas"></canvas> | |
| <!-- Focus HUD (shows details of nearest node) --> | |
| <div class="focus-hud" id="focusHud"> | |
| <div class="node-id" id="focusNodeId">β</div> | |
| <div class="node-meta"> | |
| <div class="meta-row"><span class="meta-label">TYPE</span><span class="meta-val" id="focusType">β</span></div> | |
| <div class="meta-row"><span class="meta-label">STATE</span><span class="meta-val" id="focusState">β</span></div> | |
| <div class="meta-row"><span class="meta-label">s(t)</span><span class="meta-val" id="focusS">β</span></div> | |
| <div class="meta-row"><span class="meta-label">ADAPT</span><span class="meta-val" id="focusAdapt">β</span></div> | |
| <div class="meta-row"><span class="meta-label">HEALTH</span><span class="meta-val" id="focusHealth">β</span></div> | |
| <div class="meta-row"><span class="meta-label">SYNAPSES</span><span class="meta-val" id="focusSyn">β</span></div> | |
| </div> | |
| </div> | |
| <!-- Interaction hint --> | |
| <div class="pan-hint" id="panHint">β CLICK & DRAG TO EXPLORE THE NEURAL GRAPH β<br><span style="font-size:9px;opacity:0.5;">scroll to zoom Β· nearest node auto-focuses</span></div> | |
| <div class="canvas-overlay"> | |
| <div class="overlay-chip">STEP <span id="stepCount">0</span></div> | |
| <div class="legend-row"> | |
| <div class="legend-item"><div class="legend-dot exc"></div>Excite (+1)</div> | |
| <div class="legend-item"><div class="legend-dot neu"></div>Neutral (0)</div> | |
| <div class="legend-item"><div class="legend-dot inh"></div>Inhibit (-1)</div> | |
| <div class="legend-item" style="opacity:0.6;">β Input</div> | |
| <div class="legend-item" style="opacity:0.6;">β Hidden</div> | |
| <div class="legend-item" style="opacity:0.6;">β‘ Output</div> | |
| </div> | |
| <div class="overlay-chip">ENERGY <span id="energyVal">0.00</span></div> | |
| </div> | |
| </div> | |
| <!-- =================== RIGHT PANEL =================== --> | |
| <div class="panel-right"> | |
| <div class="card"> | |
| <div class="card-title"><div class="dot" style="background:var(--da-color)"></div> NEUROMODULATORS</div> | |
| <div class="neuromod-row nm-da"> | |
| <span class="neuromod-label">DA</span> | |
| <input type="range" class="neuromod-slider" id="sliderDA" min="0" max="100" value="50"> | |
| <span class="neuromod-val" id="valDA">0.50</span> | |
| </div> | |
| <div class="neuromod-row nm-sht"> | |
| <span class="neuromod-label">5HT</span> | |
| <input type="range" class="neuromod-slider" id="slider5HT" min="0" max="100" value="50"> | |
| <span class="neuromod-val" id="val5HT">0.50</span> | |
| </div> | |
| <div class="neuromod-row nm-ach"> | |
| <span class="neuromod-label">ACh</span> | |
| <input type="range" class="neuromod-slider" id="sliderACh" min="0" max="100" value="50"> | |
| <span class="neuromod-val" id="valACh">0.50</span> | |
| </div> | |
| <div class="neuromod-row nm-na"> | |
| <span class="neuromod-label">NA</span> | |
| <input type="range" class="neuromod-slider" id="sliderNA" min="0" max="100" value="50"> | |
| <span class="neuromod-val" id="valNA">0.50</span> | |
| </div> | |
| </div> | |
| <div class="card"> | |
| <div class="card-title"><div class="dot" style="background:var(--cyan)"></div> NETWORK STATS</div> | |
| <div class="metrics-grid"> | |
| <div class="metric-box"> | |
| <div class="metric-label">Excitatory</div> | |
| <div class="metric-value exc" id="statExc">0</div> | |
| </div> | |
| <div class="metric-box"> | |
| <div class="metric-label">Inhibitory</div> | |
| <div class="metric-value inh" id="statInh">0</div> | |
| </div> | |
| <div class="metric-box"> | |
| <div class="metric-label">Neutral</div> | |
| <div class="metric-value neu" id="statNeu">0</div> | |
| </div> | |
| <div class="metric-box"> | |
| <div class="metric-label">Synapses</div> | |
| <div class="metric-value" style="color:var(--blue)" id="statSyn">0</div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="card"> | |
| <div class="card-title"><div class="dot" style="background:var(--sht-color)"></div> OSCILLATOR BANK</div> | |
| <canvas id="oscCanvas" width="228" height="90" style="width:100%;height:90px;border-radius:6px;background:var(--bg-abyss);"></canvas> | |
| </div> | |
| <div class="card"> | |
| <div class="card-title"><div class="dot" style="background:var(--green)"></div> ACTIVITY TRACE</div> | |
| <div class="activity-bar"> | |
| <canvas id="activityCanvas" width="228" height="40" style="width:100%;height:40px;"></canvas> | |
| </div> | |
| </div> | |
| <div class="card"> | |
| <div class="card-title"><div class="dot" style="background:var(--pink)"></div> RECEPTOR ACTIVATIONS</div> | |
| <div id="receptorBars" style="display:flex;flex-direction:column;gap:4px;"></div> | |
| </div> | |
| <div class="card"> | |
| <div class="card-title"><div class="dot" style="background:var(--amber)"></div> LEARNING PROGRESS</div> | |
| <div style="margin-bottom:8px;"> | |
| <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:4px;"> | |
| <span style="font-family:var(--mono);font-size:9px;color:var(--txt-dim);">ACCURACY</span> | |
| <span style="font-family:var(--mono);font-size:11px;font-weight:700;color:var(--green);" id="learnPct">0%</span> | |
| </div> | |
| <div class="progress-track"> | |
| <div class="progress-fill" id="learnBar" style="width:0%;"></div> | |
| <div class="progress-text" id="learnBarText">Drive in MANUAL to teach</div> | |
| </div> | |
| </div> | |
| <div class="metrics-grid"> | |
| <div class="metric-box"> | |
| <div class="metric-label">Teach Steps</div> | |
| <div class="metric-value" style="color:var(--da-color);font-size:13px;" id="statLearn">0</div> | |
| </div> | |
| <div class="metric-box"> | |
| <div class="metric-label">Mean |Ξw|</div> | |
| <div class="metric-value" style="color:var(--amber);font-size:13px;" id="statDw">0.000</div> | |
| </div> | |
| <div class="metric-box"> | |
| <div class="metric-label">Status</div> | |
| <div class="metric-value" style="color:var(--green);font-size:11px;" id="statStruct">stable</div> | |
| </div> | |
| <div class="metric-box"> | |
| <div class="metric-label">Out Accum</div> | |
| <div class="metric-value" style="color:var(--cyan);font-size:10px;font-family:var(--mono);" id="statAccum">0 0 0 0</div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="card"> | |
| <div class="card-title"><div class="dot" style="background:var(--purple)"></div> MOTOR SOURCE</div> | |
| <div style="display:flex;gap:6px;justify-content:center;flex-wrap:wrap;" id="sourceTagArea"> | |
| <span class="nxon-source-tag human" id="tagHuman">HUMAN</span> | |
| <span class="nxon-source-tag nxon" id="tagNxon" style="opacity:0.3;">NEURAXON</span> | |
| <span class="nxon-source-tag learning" id="tagLearn" style="opacity:0.3;">LEARNING</span> | |
| </div> | |
| <div style="margin-top:8px;text-align:center;"> | |
| <span style="font-family:var(--mono);font-size:10px;color:var(--txt-dim);" id="sourceDetail">Manual control active</span> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // SPHERO MINI BLE PROTOCOL | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| const UUID_SPHERO_SERVICE = '00010001-574f-4f20-5370-6865726f2121'; | |
| const UUID_SPHERO_SERVICE_INIT = '00020001-574f-4f20-5370-6865726f2121'; | |
| const UUID_CHAR_HANDLE = '00010002-574f-4f20-5370-6865726f2121'; | |
| const UUID_CHAR_USETHEFORCE = '00020005-574f-4f20-5370-6865726f2121'; | |
| const UseTheForceBytes = new Uint8Array([0x75,0x73,0x65,0x74,0x68,0x65,0x66,0x6f,0x72,0x63,0x65,0x2e,0x2e,0x2e,0x62,0x61,0x6e,0x64]); | |
| const API = { ESC: 0xAB, SOP: 0x8D, EOP: 0xD8, ESC_MASK: 0x88 }; | |
| const DeviceId = { powerInfo: 0x13, driving: 0x16, userIO: 0x1A }; | |
| const PowerCmd = { wake: 0x0D, sleep: 0x01 }; | |
| const DrivingCmd = { resetYaw: 0x06, driveWithHeading: 0x07 }; | |
| const UserIOCmd = { allLEDs: 0x0E }; | |
| const Flags = { requestsResponse: 2, requestsOnlyErrorResponse: 4, resetsInactivityTimeout: 8 }; | |
| class SpheroMiniBLE { | |
| constructor() { | |
| this.device = null; this.server = null; this.ch = new Map(); this.seq = 0; | |
| this._chain = Promise.resolve(); this._qDepth = 0; this._closed = false; | |
| this.onDisconnect = null; | |
| } | |
| _pushEscaped(out, b) { | |
| if (b === API.SOP || b === API.EOP || b === API.ESC) { out.push(API.ESC); out.push(b & (~API.ESC_MASK)); } | |
| else out.push(b); | |
| } | |
| _buildPacket(did, cid, dataBytes, cmdFlags) { | |
| this.seq = (this.seq + 1) & 0xFF; let sum = 0; const out = []; | |
| out.push(API.SOP); out.push(cmdFlags); sum += cmdFlags; | |
| this._pushEscaped(out, did); sum += did; | |
| this._pushEscaped(out, cid); sum += cid; | |
| this._pushEscaped(out, this.seq); sum += this.seq; | |
| for (const b of dataBytes) { this._pushEscaped(out, b); sum += b; } | |
| const chk = (~sum) & 0xFF; this._pushEscaped(out, chk); out.push(API.EOP); | |
| return new Uint8Array(out); | |
| } | |
| _enqueueWrite(fn) { | |
| if (this._closed) return Promise.reject(new Error('BLE closed')); | |
| this._qDepth++; | |
| const run = async () => { try { return await fn(); } finally { this._qDepth = Math.max(0, this._qDepth - 1); } }; | |
| this._chain = this._chain.then(run, run); return this._chain; | |
| } | |
| async connect() { | |
| sysLog('Requesting Bluetooth device...'); | |
| this.device = await navigator.bluetooth.requestDevice({ | |
| filters: [{ services: [UUID_SPHERO_SERVICE] }], | |
| optionalServices: [UUID_SPHERO_SERVICE_INIT] | |
| }); | |
| this.device.addEventListener('gattserverdisconnected', () => { | |
| sysLog('BLE disconnected'); | |
| if (this.onDisconnect) this.onDisconnect(); | |
| }); | |
| sysLog(`Connecting to: ${this.device.name || 'Sphero Mini'}`); | |
| this.server = await this.device.gatt.connect(); | |
| const svcCmd = await this.server.getPrimaryService(UUID_SPHERO_SERVICE); | |
| const svcInit = await this.server.getPrimaryService(UUID_SPHERO_SERVICE_INIT); | |
| const chHandle = await svcCmd.getCharacteristic(UUID_CHAR_HANDLE); | |
| const chForce = await svcInit.getCharacteristic(UUID_CHAR_USETHEFORCE); | |
| this.ch.set('handle', chHandle); | |
| sysLog('Waking Sphero...'); | |
| await this._enqueueWrite(() => chForce.writeValue(UseTheForceBytes)); | |
| await this.send(DeviceId.powerInfo, PowerCmd.wake, [], { response: 'full' }); | |
| await sleep(200); | |
| await this.send(DeviceId.powerInfo, PowerCmd.wake, [], { response: 'full' }); | |
| } | |
| async disconnect() { | |
| this._closed = true; | |
| if (this.device && this.device.gatt.connected) this.device.gatt.disconnect(); | |
| } | |
| async send(did, cid, data, opts = { response: 'errorOnly' }) { | |
| const chHandle = this.ch.get('handle'); if (!chHandle) throw new Error('Handle missing'); | |
| let cmdFlags = Flags.resetsInactivityTimeout; | |
| if (opts.response === 'full') cmdFlags |= Flags.requestsResponse; | |
| else if (opts.response === 'errorOnly') cmdFlags |= Flags.requestsOnlyErrorResponse; | |
| const pkt = this._buildPacket(did, cid, data, cmdFlags); | |
| return this._enqueueWrite(async () => { | |
| if (chHandle.writeValueWithoutResponse && opts.response !== 'full') await chHandle.writeValueWithoutResponse(pkt); | |
| else await chHandle.writeValue(pkt); | |
| return this.seq; | |
| }); | |
| } | |
| async setMainLED(r, g, b) { return this.send(DeviceId.userIO, UserIOCmd.allLEDs, [0x00, 0x70, r & 255, g & 255, b & 255], { response: 'none' }); } | |
| async roll(speed, headingDeg) { | |
| const head = ((headingDeg % 360) + 360) % 360; | |
| return this.send(DeviceId.driving, DrivingCmd.driveWithHeading, [speed & 255, (head >> 8) & 0xFF, head & 0xFF, 0x01], { response: 'none' }); | |
| } | |
| async resetYaw() { return this.send(DeviceId.driving, DrivingCmd.resetYaw, [], { response: 'full' }); } | |
| async stop() { return this.roll(0, 0); } | |
| } | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // NEURAXON 2.0 β JavaScript Implementation | |
| // Based on the architecture by David Vivancos & Jose Sanchez | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class ReceptorSubtype { | |
| constructor(name, threshold, gain, isTonic) { | |
| this.name = name; this.threshold = threshold; | |
| this.gain = gain; this.isTonic = isTonic; this.activation = 0; | |
| } | |
| computeActivation(concentration) { | |
| const k = this.isTonic ? 20 : 10; | |
| this.activation = this.gain / (1 + Math.exp(-k * (concentration - this.threshold))); | |
| return this.activation; | |
| } | |
| } | |
| class OscillatorBank { | |
| constructor() { | |
| this.bands = [ | |
| { name: 'infraslow', freq: 0.05, phase: Math.random() * Math.PI * 2, amplitude: 0.15 }, | |
| { name: 'slow', freq: 0.5, phase: Math.random() * Math.PI * 2, amplitude: 0.2 }, | |
| { name: 'theta', freq: 6, phase: Math.random() * Math.PI * 2, amplitude: 0.3 }, | |
| { name: 'alpha', freq: 10, phase: Math.random() * Math.PI * 2, amplitude: 0.15 }, | |
| { name: 'gamma', freq: 40, phase: Math.random() * Math.PI * 2, amplitude: 0.25 }, | |
| ]; | |
| this.coupling = 0.3; | |
| } | |
| update(dt) { | |
| for (const b of this.bands) { | |
| b.phase += 2 * Math.PI * b.freq * dt / 1000; | |
| b.phase %= (2 * Math.PI); | |
| } | |
| } | |
| getDrive(neuronId, N) { | |
| const phi = 2 * Math.PI * neuronId / N; | |
| const theta = this.bands[2]; | |
| const gamma = this.bands[4]; | |
| const slow = this.bands[1]; | |
| const infra = this.bands[0]; | |
| const gateTheta = Math.max(0, Math.cos(theta.phase + phi)); | |
| const gammaSig = gamma.amplitude * gateTheta * Math.sin(gamma.phase + 2 * phi); | |
| const slowSig = slow.amplitude * Math.sin(slow.phase + 0.3 * phi); | |
| const infraSig = infra.amplitude * Math.sin(infra.phase); | |
| return this.coupling * (gammaSig + 0.5 * slowSig + 0.3 * infraSig); | |
| } | |
| } | |
| class NeuromodulatorSystem { | |
| constructor() { | |
| this.modulators = { | |
| DA: { tonic: 0.5, phasic: 0, tauTonic: 5000, tauPhasic: 200, releaseRate: 0.3 }, | |
| SHT: { tonic: 0.5, phasic: 0, tauTonic: 8000, tauPhasic: 500, releaseRate: 0.2 }, | |
| ACh: { tonic: 0.5, phasic: 0, tauTonic: 4000, tauPhasic: 150, releaseRate: 0.25 }, | |
| NA: { tonic: 0.5, phasic: 0, tauTonic: 6000, tauPhasic: 300, releaseRate: 0.2 }, | |
| }; | |
| this.receptors = { | |
| D1: new ReceptorSubtype('D1', 0.4, 1.0, false), | |
| D2: new ReceptorSubtype('D2', 0.6, 0.8, true), | |
| SHT1A: new ReceptorSubtype('5HT1A', 0.3, 1.0, true), | |
| SHT2A: new ReceptorSubtype('5HT2A', 0.5, 0.9, false), | |
| SHT4: new ReceptorSubtype('5HT4', 0.4, 0.7, false), | |
| M1: new ReceptorSubtype('M1', 0.35, 1.0, false), | |
| M2: new ReceptorSubtype('M2', 0.5, 0.6, true), | |
| B1: new ReceptorSubtype('Ξ²1', 0.4, 0.9, false), | |
| A2: new ReceptorSubtype('Ξ±2', 0.3, 0.7, true), | |
| }; | |
| this.externalOverrides = { DA: null, SHT: null, ACh: null, NA: null }; | |
| } | |
| update(activity, dt) { | |
| const { excFrac, meanAct, changeRate } = activity; | |
| for (const [key, m] of Object.entries(this.modulators)) { | |
| m.tonic += (0.5 - m.tonic) * dt / m.tauTonic; | |
| m.phasic *= Math.exp(-dt / m.tauPhasic); | |
| } | |
| this.modulators.DA.phasic += this.modulators.DA.releaseRate * changeRate * dt / 1000; | |
| this.modulators.SHT.tonic += this.modulators.SHT.releaseRate * meanAct * dt / 5000; | |
| this.modulators.ACh.phasic += this.modulators.ACh.releaseRate * excFrac * dt / 1000; | |
| this.modulators.NA.phasic += this.modulators.NA.releaseRate * changeRate * dt / 1000; | |
| this.modulators.ACh.phasic *= (1 - 0.1 * this.modulators.DA.phasic); | |
| this.modulators.SHT.tonic += 0.02 * (this.modulators.NA.tonic + this.modulators.NA.phasic) * dt / 1000; | |
| for (const m of Object.values(this.modulators)) { | |
| m.tonic = clamp(m.tonic, 0, 1); | |
| m.phasic = clamp(m.phasic, 0, 1); | |
| } | |
| for (const [key, val] of Object.entries(this.externalOverrides)) { | |
| if (val !== null) this.modulators[key].tonic = val; | |
| } | |
| } | |
| computeReceptorActivations() { | |
| const R = {}; | |
| const getConc = (modKey, isTonic) => { | |
| const m = this.modulators[modKey]; | |
| return isTonic ? m.tonic : m.tonic + m.phasic; | |
| }; | |
| R.D1 = this.receptors.D1.computeActivation(getConc('DA', false)); | |
| R.D2 = this.receptors.D2.computeActivation(getConc('DA', true)); | |
| R.SHT1A = this.receptors.SHT1A.computeActivation(getConc('SHT', true)); | |
| R.SHT2A = this.receptors.SHT2A.computeActivation(getConc('SHT', false)); | |
| R.SHT4 = this.receptors.SHT4.computeActivation(getConc('SHT', false)); | |
| R.M1 = this.receptors.M1.computeActivation(getConc('ACh', false)); | |
| R.M2 = this.receptors.M2.computeActivation(getConc('ACh', true)); | |
| R.B1 = this.receptors.B1.computeActivation(getConc('NA', false)); | |
| R.A2 = this.receptors.A2.computeActivation(getConc('NA', true)); | |
| return R; | |
| } | |
| } | |
| class NeuraxonSynapse { | |
| constructor(preId, postId, branchId) { | |
| this.preId = preId; this.postId = postId; this.branchId = branchId; | |
| this.wFast = (Math.random() - 0.5) * 0.6; | |
| this.wSlow = (Math.random() - 0.5) * 0.3; | |
| this.wMeta = (Math.random() - 0.5) * 0.1; | |
| this.tauFast = 50; this.tauSlow = 500; this.tauMeta = 5000; | |
| this.preTrace = 0; this.postTrace = 0; | |
| this.recentDw = 0; this.integrity = 1.0; | |
| this.silent = Math.random() < 0.05; | |
| } | |
| computeInput(preState) { | |
| if (this.silent) return 0; | |
| return (this.wFast + this.wSlow) * preState; | |
| } | |
| getModulatoryEffect() { return this.wMeta; } | |
| update(preState, postState, R, neighborDws, dt) { | |
| const tauSTDP = 150; | |
| this.preTrace += (-this.preTrace / tauSTDP + (preState === 1 ? 1 : 0)) * dt / 1000; | |
| this.postTrace += (-this.postTrace / tauSTDP + (postState === 1 ? 1 : 0)) * dt / 1000; | |
| const Aplus = this.preTrace * (postState === 1 ? 1 : 0); | |
| const Aminus = this.postTrace * (preState === 1 ? 1 : 0); | |
| // Base learning rate β slight boost for turn output neurons to compensate for rarity | |
| const isTurnTarget = (this.postId === 32 || this.postId === 33); | |
| const eta = isTurnTarget ? 0.06 : 0.05; | |
| const d1 = R.D1 || 0.5; | |
| const d2 = R.D2 || 0.5; | |
| let dw = eta * Aplus * d1 - eta * 0.6 * Aminus * d2; | |
| if (preState === 1 && postState === 1) dw += eta * 0.3 * d1; | |
| if (preState === 1 && postState === -1) dw -= eta * 0.2 * d2; | |
| if (postState === 0) dw *= 0.1; | |
| if (neighborDws.length > 0) { | |
| let assoc = 0; | |
| for (const nd of neighborDws) assoc += nd / (1 + Math.random()); | |
| dw += 0.01 * assoc; | |
| } | |
| this.recentDw = dw; | |
| this.wFast += (dt / this.tauFast) * (-this.wFast * 0.0005 + 0.5 * dw); | |
| const slowRate = isTurnTarget ? 0.18 : 0.15; | |
| this.wSlow += (dt / this.tauSlow) * (-this.wSlow * 0.0005 + slowRate * dw); | |
| this.wFast = clamp(this.wFast, -1, 1); | |
| this.wSlow = clamp(this.wSlow, -1, 1); | |
| const shtFactor = 0.5 * (R.SHT2A || 0.5) + 0.1 * (1 - (R.SHT1A || 0.5)); | |
| this.wMeta += (dt / this.tauMeta) * (-this.wMeta * 0.0005 + 0.05 * dw * shtFactor); | |
| this.wMeta = clamp(this.wMeta, -0.5, 0.5); | |
| const activityBonus = (Math.abs(preState) + Math.abs(postState)) * 0.00005; | |
| this.integrity += (-0.00001 + activityBonus) * dt; | |
| this.integrity = clamp(this.integrity, 0, 1); | |
| if (this.silent && Math.abs(preState) === 1 && Math.abs(postState) === 1 && Math.random() < 0.02) { | |
| this.silent = false; | |
| } | |
| } | |
| } | |
| class Neuraxon { | |
| constructor(id, type) { | |
| this.id = id; this.type = type; | |
| this.s = 0; this.state = 0; | |
| this.theta1 = 0.5; this.theta2 = -0.5; | |
| this.adaptation = 0; this.autoReceptor = 0; | |
| this.rBar = 0; this.targetRate = 0.3; | |
| this.health = 1.0; this.baseRate = 0.05; | |
| this.tau = 80 + Math.random() * 60; | |
| this.branches = 3; | |
| this.x = 0; this.y = 0; | |
| this.prevState = 0; | |
| } | |
| update(branchInputs, modInputs, Iext, oscDrive, R, dt) { | |
| const alpha = 0.001; | |
| this.rBar += alpha * (Math.abs(this.state) - this.rBar) * dt / 1000; | |
| const thetaDend = 0.8; | |
| let D = 0; | |
| for (let b = 0; b < this.branches; b++) { | |
| const inputs = branchInputs[b] || []; | |
| let sigma = 0; | |
| for (const inp of inputs) sigma += inp; | |
| if (Math.abs(sigma) > thetaDend) { | |
| D += Math.sign(sigma) * Math.pow(Math.abs(sigma), 1.3); | |
| } else { | |
| D += sigma; | |
| } | |
| } | |
| const gNA = 1 + 0.5 * (R.B1 || 0.5) + 0.2 * (R.A2 || 0.5); | |
| const spont = (this.baseRate + 0.3 * (R.A2 || 0.3)) * (Math.random() - 0.4); | |
| this.s += (dt / this.tau) * (-this.s + gNA * D + Iext + oscDrive - this.adaptation + spont); | |
| let rawMod = 0; | |
| for (const m of (modInputs || [])) rawMod += m; | |
| rawMod += 0.3 * (R.M1 || 0.5) - 0.2 * (R.M2 || 0.3); | |
| const dThetaMeta = 0.3 * Math.tanh(rawMod); | |
| const etaH = 0.01; | |
| const dThetaHomeo = etaH * (this.rBar - this.targetRate); | |
| const theta1Eff = this.theta1 - dThetaMeta + dThetaHomeo - 0.1 * this.autoReceptor; | |
| const theta2Eff = this.theta2 - dThetaMeta + dThetaHomeo + 0.1 * this.autoReceptor; | |
| this.prevState = this.state; | |
| if (this.s > theta1Eff) this.state = 1; | |
| else if (this.s < theta2Eff) this.state = -1; | |
| else this.state = 0; | |
| const tauA = 300, tauR = 1000; | |
| this.adaptation += (dt / tauA) * (-this.adaptation + 0.1 * Math.abs(this.state)); | |
| this.autoReceptor += (dt / tauR) * (-this.autoReceptor + 0.2 * this.state); | |
| this.health -= 0.00001 * (1 - Math.abs(this.state)) * dt / 1000; | |
| this.health = clamp(this.health, 0.1, 1); | |
| } | |
| } | |
| class NeuraxonNetwork { | |
| constructor(nInput, nHidden, nOutput) { | |
| this.neurons = []; | |
| this.synapses = []; | |
| this.neuromod = new NeuromodulatorSystem(); | |
| this.oscillators = new OscillatorBank(); | |
| this.energy = 0; | |
| this.step = 0; | |
| this.meanDw = 0; | |
| this.structEvent = ''; | |
| for (let i = 0; i < nInput; i++) this.neurons.push(new Neuraxon(i, 'input')); | |
| for (let i = 0; i < nHidden; i++) this.neurons.push(new Neuraxon(nInput + i, 'hidden')); | |
| for (let i = 0; i < nOutput; i++) this.neurons.push(new Neuraxon(nInput + nHidden + i, 'output')); | |
| this.N = this.neurons.length; | |
| this.nInput = nInput; this.nHidden = nHidden; this.nOutput = nOutput; | |
| this._buildSmallWorld(6, 0.2); | |
| this._addMotorPathways(); | |
| this._layoutNeurons(); | |
| } | |
| _addMotorPathways() { | |
| const outStart = this.nInput + this.nHidden; | |
| // Direct inputβoutput: turn pathways get a slight head start | |
| const directMap = [[0, 0], [1, 2], [2, 3], [3, 1]]; | |
| const turnOutputs = new Set([2, 3]); | |
| for (const [inp, out] of directMap) { | |
| const syn = new NeuraxonSynapse(inp, outStart + out, 0); | |
| const isTurn = turnOutputs.has(out); | |
| syn.wFast = isTurn ? 0.7 + Math.random() * 0.15 : 0.6 + Math.random() * 0.2; | |
| syn.wSlow = isTurn ? 0.35 + Math.random() * 0.1 : 0.3 + Math.random() * 0.1; | |
| syn.integrity = 1.0; | |
| syn.silent = false; | |
| this.synapses.push(syn); | |
| } | |
| for (let h = this.nInput; h < outStart; h++) { | |
| for (let o = outStart; o < this.N; o++) { | |
| const isTurnOut = (o === outStart + 2 || o === outStart + 3); | |
| const connProb = isTurnOut ? 0.45 : 0.4; | |
| if (Math.random() < connProb) { | |
| const syn = new NeuraxonSynapse(h, o, Math.floor(Math.random() * 3)); | |
| syn.wFast = (Math.random() - 0.5) * 0.4; | |
| syn.wSlow = (Math.random() - 0.5) * 0.2; | |
| this.synapses.push(syn); | |
| } | |
| } | |
| } | |
| for (let i = 0; i < this.nInput; i++) { | |
| for (let h = this.nInput; h < outStart; h++) { | |
| if (Math.random() < 0.35) { | |
| const syn = new NeuraxonSynapse(i, h, Math.floor(Math.random() * 3)); | |
| syn.wFast = (Math.random() - 0.5) * 0.5; | |
| syn.wSlow = (Math.random() - 0.5) * 0.2; | |
| this.synapses.push(syn); | |
| } | |
| } | |
| } | |
| for (let i = 4; i <= 5; i++) { | |
| for (let h = this.nInput; h < outStart; h++) { | |
| if (Math.random() < 0.5) { | |
| const syn = new NeuraxonSynapse(i, h, Math.floor(Math.random() * 3)); | |
| syn.wFast = (Math.random() - 0.3) * 0.3; | |
| this.synapses.push(syn); | |
| } | |
| } | |
| } | |
| } | |
| _buildSmallWorld(k, beta) { | |
| const N = this.N; | |
| const halfK = Math.floor(k / 2); | |
| const edges = new Set(); | |
| for (let i = 0; i < N; i++) { | |
| for (let j = 1; j <= halfK; j++) { | |
| const target = (i + j) % N; | |
| const key = Math.min(i, target) + '-' + Math.max(i, target); | |
| if (!edges.has(key)) { | |
| edges.add(key); | |
| const branchId = Math.floor(Math.random() * 3); | |
| this.synapses.push(new NeuraxonSynapse(i, target, branchId)); | |
| } | |
| } | |
| } | |
| const synCopy = [...this.synapses]; | |
| for (const syn of synCopy) { | |
| if (Math.random() < beta) { | |
| let newTarget; | |
| do { newTarget = Math.floor(Math.random() * N); } while (newTarget === syn.preId); | |
| syn.postId = newTarget; | |
| syn.branchId = Math.floor(Math.random() * 3); | |
| } | |
| } | |
| } | |
| _layoutNeurons() { | |
| // βββ ENHANCED POLAR / RADIAL LAYOUT βββ | |
| // Spread neurons in polar coordinates with staggered radii for visual depth | |
| const cx = 0, cy = 0; // world-space center | |
| // Input neurons: inner ring with slight jitter | |
| for (let i = 0; i < this.nInput; i++) { | |
| const angle = (2 * Math.PI * i / this.nInput) - Math.PI / 2; | |
| const r = 80 + (i % 2) * 12; | |
| this.neurons[i].x = cx + r * Math.cos(angle); | |
| this.neurons[i].y = cy + r * Math.sin(angle); | |
| } | |
| // Hidden neurons: two staggered concentric bands | |
| const hidStart = this.nInput; | |
| for (let i = 0; i < this.nHidden; i++) { | |
| const angle = (2 * Math.PI * i / this.nHidden) - Math.PI / 2; | |
| const band = (i % 3 === 0) ? 190 : (i % 3 === 1) ? 230 : 260; | |
| const jitter = (Math.sin(i * 2.7) * 15); | |
| this.neurons[hidStart + i].x = cx + (band + jitter) * Math.cos(angle); | |
| this.neurons[hidStart + i].y = cy + (band + jitter) * Math.sin(angle); | |
| } | |
| // Output neurons: outer ring | |
| const outStart = this.nInput + this.nHidden; | |
| for (let i = 0; i < this.nOutput; i++) { | |
| const angle = (2 * Math.PI * i / this.nOutput) - Math.PI / 2; | |
| const r = 350; | |
| this.neurons[outStart + i].x = cx + r * Math.cos(angle); | |
| this.neurons[outStart + i].y = cy + r * Math.sin(angle); | |
| } | |
| } | |
| setInputs(values) { | |
| for (let i = 0; i < Math.min(values.length, this.nInput); i++) { | |
| const n = this.neurons[i]; | |
| n.s = values[i] * 2.0; | |
| n.state = values[i] > 0.2 ? 1 : values[i] < -0.2 ? -1 : 0; | |
| n.adaptation = 0; | |
| n.autoReceptor = 0; | |
| } | |
| } | |
| getOutputStates() { | |
| const start = this.nInput + this.nHidden; | |
| return this.neurons.slice(start).map(n => n.state); | |
| } | |
| getOutputValues() { | |
| const start = this.nInput + this.nHidden; | |
| return this.neurons.slice(start).map(n => n.s); | |
| } | |
| simulateStep(dt) { | |
| this.step++; | |
| let excCount = 0, inhCount = 0, neuCount = 0, totalAbs = 0, changes = 0; | |
| for (const n of this.neurons) { | |
| if (n.state === 1) excCount++; | |
| else if (n.state === -1) inhCount++; | |
| else neuCount++; | |
| totalAbs += Math.abs(n.state); | |
| if (n.state !== n.prevState) changes++; | |
| } | |
| const activity = { | |
| excFrac: excCount / this.N, | |
| meanAct: totalAbs / this.N, | |
| changeRate: changes / this.N, | |
| }; | |
| this.neuromod.update(activity, dt); | |
| this.oscillators.update(dt); | |
| const R = this.neuromod.computeReceptorActivations(); | |
| const branchInputs = new Array(this.N); | |
| const modInputs = new Array(this.N); | |
| for (let i = 0; i < this.N; i++) { | |
| branchInputs[i] = [[], [], []]; | |
| modInputs[i] = []; | |
| } | |
| for (const syn of this.synapses) { | |
| const preState = this.neurons[syn.preId].state; | |
| const input = syn.computeInput(preState); | |
| if (branchInputs[syn.postId] && branchInputs[syn.postId][syn.branchId]) { | |
| branchInputs[syn.postId][syn.branchId].push(input); | |
| } | |
| if (modInputs[syn.postId]) { | |
| modInputs[syn.postId].push(syn.getModulatoryEffect()); | |
| } | |
| } | |
| for (const n of this.neurons) { | |
| if (n.type === 'input') continue; | |
| const osc = this.oscillators.getDrive(n.id, this.N); | |
| n.update(branchInputs[n.id], modInputs[n.id], 0, osc, R, dt); | |
| } | |
| let totalDw = 0; | |
| for (const syn of this.synapses) { | |
| const pre = this.neurons[syn.preId]; | |
| const post = this.neurons[syn.postId]; | |
| const neighbors = this.synapses | |
| .filter(s => s.postId === syn.postId && s !== syn && s.branchId === syn.branchId) | |
| .slice(0, 3) | |
| .map(s => s.recentDw); | |
| syn.update(pre.state, post.state, R, neighbors, dt); | |
| totalDw += Math.abs(syn.recentDw); | |
| } | |
| this.meanDw = this.synapses.length > 0 ? totalDw / this.synapses.length : 0; | |
| this.structEvent = ''; | |
| const toRemove = []; | |
| for (let i = this.synapses.length - 1; i >= 0; i--) { | |
| if (this.synapses[i].integrity < 0.1) { toRemove.push(i); } | |
| } | |
| for (const idx of toRemove) { this.synapses.splice(idx, 1); this.structEvent = 'pruned'; } | |
| if (Math.random() < 0.002 && this.synapses.length < this.N * 8) { | |
| const a = Math.floor(Math.random() * this.N); | |
| const b = Math.floor(Math.random() * this.N); | |
| if (a !== b) { | |
| this.synapses.push(new NeuraxonSynapse(a, b, Math.floor(Math.random() * 3))); | |
| this.structEvent = 'formed'; | |
| } | |
| } | |
| this.energy += 0.01 * excCount * dt / 1000; | |
| return { excCount, inhCount, neuCount, R, activity }; | |
| } | |
| } | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // APPLICATION LAYER | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| const $ = id => document.getElementById(id); | |
| const sleep = ms => new Promise(r => setTimeout(r, ms)); | |
| const clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v)); | |
| function sysLog(msg) { | |
| const el = $('logPanel'); | |
| const ts = new Date().toLocaleTimeString('en-US', { hour12: false }); | |
| el.textContent += `[${ts}] ${msg}\n`; | |
| el.scrollTop = el.scrollHeight; | |
| } | |
| let sphero = null; | |
| let isConnected = false; | |
| let controlMode = 'manual'; | |
| let heading = 0; | |
| const BASE_SPEED = 90; | |
| const TURN_DEG = 8; | |
| const activeKeys = new Set(); | |
| let simRunning = true; | |
| const outputAccum = [0, 0, 0, 0]; | |
| const OUTPUT_DECAY = 0.92; | |
| const OUTPUT_GAIN = 0.5; | |
| let teachingActive = false; | |
| let teachingTarget = [0, 0, 0, 0]; | |
| let learnCount = 0; | |
| let lastLearnLog = 0; | |
| // ββ STOP learning: track idle to teach "all-off" as a real behavior ββ | |
| let idleTicks = 0; | |
| const IDLE_TEACH_DELAY = 25; // ~0.4s no keys β teach "stop" | |
| // ββ Movement replay: remember human sequences for Neuraxon mode ββ | |
| const replayBuffer = []; // { inputs: [...], target: [...] } | |
| const REPLAY_MAX = 200; | |
| let replayIdx = 0; | |
| let isReplaying = false; | |
| let learnHits = 0; | |
| let learnTrials = 0; | |
| let learnAccuracy = 0; | |
| const LEARN_WINDOW = 300; | |
| const learnHistory = []; | |
| let nxonContributing = false; | |
| let explorePhase = 0; | |
| let exploreBehavior = 0; | |
| let exploreTimer = 0; | |
| const EXPLORE_DURATION = 160; // ticks per behavior (~2.6s) | |
| const network = new NeuraxonNetwork(6, 24, 4); | |
| const activityHistory = []; | |
| const MAX_HIST = 200; | |
| // --- Build receptor bars UI --- | |
| function buildReceptorUI() { | |
| const container = $('receptorBars'); | |
| const receptorNames = ['D1','D2','5HT1A','5HT2A','5HT4','M1','M2','Ξ²1','Ξ±2']; | |
| const colors = { | |
| D1: 'var(--da-color)', D2: 'var(--da-color)', | |
| '5HT1A': 'var(--sht-color)', '5HT2A': 'var(--sht-color)', '5HT4': 'var(--sht-color)', | |
| M1: 'var(--ach-color)', M2: 'var(--ach-color)', | |
| 'Ξ²1': 'var(--na-color)', 'Ξ±2': 'var(--na-color)', | |
| }; | |
| container.innerHTML = ''; | |
| for (const name of receptorNames) { | |
| const row = document.createElement('div'); | |
| row.style.cssText = 'display:flex;align-items:center;gap:6px;'; | |
| row.innerHTML = ` | |
| <span style="font-family:var(--mono);font-size:9px;width:38px;text-align:right;color:${colors[name]};font-weight:600;">${name}</span> | |
| <div style="flex:1;height:4px;background:var(--bg-abyss);border-radius:2px;overflow:hidden;"> | |
| <div id="rbar_${name.replace(/[^a-zA-Z0-9]/g,'')}" style="height:100%;width:0%;background:${colors[name]};border-radius:2px;transition:width 0.15s;"></div> | |
| </div> | |
| `; | |
| container.appendChild(row); | |
| } | |
| } | |
| buildReceptorUI(); | |
| ['DA','5HT','ACh','NA'].forEach(key => { | |
| const sliderId = key === '5HT' ? 'slider5HT' : `slider${key}`; | |
| const valId = key === '5HT' ? 'val5HT' : `val${key}`; | |
| const modKey = key === '5HT' ? 'SHT' : key; | |
| $(sliderId).addEventListener('input', e => { | |
| const v = e.target.value / 100; | |
| $(valId).textContent = v.toFixed(2); | |
| network.neuromod.externalOverrides[modKey] = v; | |
| }); | |
| }); | |
| function setMode(mode) { | |
| controlMode = mode; | |
| document.querySelectorAll('.mode-btn').forEach(b => b.classList.toggle('active', b.dataset.mode === mode)); | |
| $('modeLabel').textContent = mode.toUpperCase(); | |
| const driveCard = $('driveCard'); | |
| if (mode === 'neuraxon') { | |
| driveCard.classList.add('controls-disabled'); | |
| } else { | |
| driveCard.classList.remove('controls-disabled'); | |
| } | |
| sysLog(`Mode: ${mode.toUpperCase()}`); | |
| } | |
| function getHumanAction() { | |
| let speed = 0, turn = 0; | |
| if (activeKeys.has('up')) speed = BASE_SPEED; | |
| if (activeKeys.has('down')) speed = -BASE_SPEED; | |
| if (activeKeys.has('left')) turn = -TURN_DEG; | |
| if (activeKeys.has('right')) turn = TURN_DEG; | |
| if (activeKeys.has('up') && activeKeys.has('left')) { speed = BASE_SPEED * 0.7; turn = -TURN_DEG; } | |
| if (activeKeys.has('up') && activeKeys.has('right')) { speed = BASE_SPEED * 0.7; turn = TURN_DEG; } | |
| return { speed, turn }; | |
| } | |
| function applyTeachingSignal() { | |
| const outStart = network.nInput + network.nHidden; | |
| teachingTarget = [0, 0, 0, 0]; | |
| teachingActive = false; | |
| if (activeKeys.has('up')) { teachingTarget[0] = 1; teachingActive = true; } | |
| if (activeKeys.has('down')) { teachingTarget[1] = 1; teachingActive = true; } | |
| if (activeKeys.has('left')) { teachingTarget[2] = 1; teachingActive = true; } | |
| if (activeKeys.has('right')) { teachingTarget[3] = 1; teachingActive = true; } | |
| // ββ STOP TEACHING: when human releases keys, teach "no output" ββ | |
| if (!teachingActive && (controlMode === 'manual' || controlMode === 'hybrid')) { | |
| idleTicks++; | |
| // After a brief pause, actively teach the network to be quiet | |
| if (idleTicks > IDLE_TEACH_DELAY && idleTicks < IDLE_TEACH_DELAY + 60) { | |
| teachingActive = true; | |
| // All targets stay 0 β this IS the stop signal | |
| for (let i = 0; i < 4; i++) { | |
| const n = network.neurons[outStart + i]; | |
| n.s = -0.3; | |
| n.state = 0; | |
| } | |
| // Mild DA for the stop-learning (smaller than move-learning) | |
| network.neuromod.modulators.DA.phasic = clamp( | |
| network.neuromod.modulators.DA.phasic + 0.08, 0, 1 | |
| ); | |
| learnCount++; | |
| return; // don't also run the movement clamp below | |
| } | |
| } else { | |
| idleTicks = 0; | |
| } | |
| if (teachingActive && (controlMode === 'manual' || controlMode === 'hybrid')) { | |
| const isTurnTeach = (teachingTarget[2] === 1 || teachingTarget[3] === 1); | |
| // ββ Record to replay buffer (raw inputβtarget pair) ββ | |
| const currentInputs = computeSensoryInputs(); | |
| replayBuffer.push({ inputs: [...currentInputs], target: [...teachingTarget] }); | |
| if (replayBuffer.length > REPLAY_MAX) replayBuffer.shift(); | |
| // ββ MEASURE ACCURACY BEFORE CLAMPING ββ | |
| let hits = 0, evaluated = 0; | |
| for (let i = 0; i < 4; i++) { | |
| const n = network.neurons[outStart + i]; | |
| const naturalState = n.state; | |
| const desired = teachingTarget[i]; | |
| if (desired === 1) { | |
| evaluated++; | |
| if (naturalState === 1) hits++; | |
| } else { | |
| evaluated++; | |
| if (naturalState <= 0) hits++; | |
| } | |
| } | |
| const hitRatio = evaluated > 0 ? hits / evaluated : 0; | |
| learnHistory.push(hitRatio); | |
| if (learnHistory.length > LEARN_WINDOW) learnHistory.shift(); | |
| if (learnHistory.length > 10) { | |
| const sum = learnHistory.reduce((a, b) => a + b, 0); | |
| learnAccuracy = Math.round((sum / learnHistory.length) * 100); | |
| } | |
| // ββ CLAMP OUTPUT NEURONS ββ | |
| for (let i = 0; i < 4; i++) { | |
| const n = network.neurons[outStart + i]; | |
| if (teachingTarget[i] > 0) { | |
| n.s = n.theta1 + 0.8; | |
| n.state = 1; | |
| } else { | |
| n.s = -0.2; | |
| n.state = 0; | |
| } | |
| } | |
| // ββ NEUROMODULATOR BURSTS β moderate turn bonus ββ | |
| const daBoost = isTurnTeach ? 0.25 : 0.2; | |
| network.neuromod.modulators.DA.phasic = clamp( | |
| network.neuromod.modulators.DA.phasic + daBoost, 0, 1 | |
| ); | |
| const achBase = 0.12 * (1 - learnAccuracy / 150); | |
| const achBoost = isTurnTeach ? Math.max(0.05, achBase) : Math.max(0.03, achBase); | |
| network.neuromod.modulators.ACh.phasic = clamp( | |
| network.neuromod.modulators.ACh.phasic + achBoost, 0, 1 | |
| ); | |
| network.neuromod.modulators.NA.phasic = clamp( | |
| network.neuromod.modulators.NA.phasic + 0.05, 0, 1 | |
| ); | |
| learnCount++; | |
| if (learnCount - lastLearnLog >= 200) { | |
| sysLog(`Learn: ${learnCount} steps | acc: ${learnAccuracy}% | ${isTurnTeach ? 'β» turn' : 'β fwd'}`); | |
| lastLearnLog = learnCount; | |
| } | |
| } | |
| } | |
| function getNeuraxonAction() { | |
| const outStart = network.nInput + network.nHidden; | |
| for (let i = 0; i < 4; i++) { | |
| const state = network.neurons[outStart + i].state; | |
| outputAccum[i] = outputAccum[i] * OUTPUT_DECAY + state * OUTPUT_GAIN; | |
| outputAccum[i] = clamp(outputAccum[i], -2.0, 2.0); | |
| } | |
| const fwd = Math.max(0, outputAccum[0]); | |
| const back = Math.max(0, outputAccum[1]); | |
| const left = Math.max(0, outputAccum[2]); | |
| const right = Math.max(0, outputAccum[3]); | |
| const rawSpeed = fwd - back * 0.5; | |
| const speed = clamp(Math.round(rawSpeed * BASE_SPEED * 0.9), 0, 180); | |
| const turnRaw = (right - left); | |
| const turn = turnRaw * TURN_DEG * 2.8; | |
| nxonContributing = (speed > 12 || Math.abs(turn) > 2); | |
| return { speed, turn }; | |
| } | |
| function computeSensoryInputs() { | |
| const inputs = new Array(6).fill(0); | |
| if (controlMode === 'manual' || controlMode === 'hybrid') { | |
| if (activeKeys.has('up')) inputs[0] = 1.0; | |
| if (activeKeys.has('left')) inputs[1] = 1.0; | |
| if (activeKeys.has('right')) inputs[2] = 1.0; | |
| if (activeKeys.has('down')) inputs[3] = 1.0; | |
| inputs[4] = Math.sin(Date.now() / 4000) * 0.2; | |
| inputs[5] = Math.cos(Date.now() / 5000) * 0.15; | |
| } | |
| if (controlMode === 'hybrid') { | |
| inputs[4] += Math.sin(Date.now() / 2000) * 0.25; | |
| inputs[5] += Math.cos(Date.now() / 2500) * 0.2; | |
| if (nxonContributing) { | |
| inputs[4] += outputAccum[0] * 0.15; | |
| inputs[5] += (outputAccum[3] - outputAccum[2]) * 0.1; | |
| } | |
| } | |
| if (controlMode === 'neuraxon') { | |
| // ββ REPLAY-FIRST STRATEGY: replay what the human actually did ββ | |
| // If we have recorded moves, prefer replaying them over random exploration | |
| if (replayBuffer.length > 20 && Math.random() < 0.6) { | |
| // Replay a stored input pattern β cycles through the buffer | |
| isReplaying = true; | |
| const frame = replayBuffer[replayIdx % replayBuffer.length]; | |
| replayIdx++; | |
| // Feed the stored sensory inputs (first 4 channels = directional) | |
| for (let i = 0; i < 4; i++) { | |
| inputs[i] = frame.inputs[i] * 0.8; // slightly attenuated replay | |
| } | |
| } else { | |
| // ββ EXPLORATION: balanced patterns including STOP ββ | |
| isReplaying = false; | |
| exploreTimer++; | |
| if (exploreTimer >= EXPLORE_DURATION) { | |
| exploreTimer = 0; | |
| exploreBehavior = (exploreBehavior + 1) % 8; | |
| } | |
| const t = exploreTimer / EXPLORE_DURATION; | |
| const ramp = t < 0.15 ? t / 0.15 : t > 0.85 ? (1 - t) / 0.15 : 1.0; | |
| const intensity = (0.85 + 0.15 * Math.sin(Date.now() / 800)) * ramp; | |
| switch (exploreBehavior) { | |
| case 0: inputs[0] = intensity; break; // forward | |
| case 1: inputs[1] = intensity; break; // left | |
| case 2: inputs[2] = intensity; break; // right | |
| case 3: inputs[0] = intensity; inputs[2] = intensity * 0.6; break; // fwd + right | |
| case 4: inputs[0] = intensity; inputs[1] = intensity * 0.6; break; // fwd + left | |
| case 5: inputs[3] = intensity * 0.5; break; // back | |
| case 6: break; // STOP β all zero | |
| case 7: break; // STOP β all zero (2nd pause) | |
| } | |
| } | |
| // Self-reinforcement: feed output accumulators back evenly | |
| const selfGain = 0.25; | |
| inputs[0] += clamp(outputAccum[0] * selfGain, 0, 0.3); | |
| inputs[1] += clamp(outputAccum[2] * selfGain, 0, 0.3); | |
| inputs[2] += clamp(outputAccum[3] * selfGain, 0, 0.3); | |
| inputs[3] += clamp(outputAccum[1] * selfGain, 0, 0.3); | |
| // Rhythmic drive | |
| inputs[4] = Math.sin(Date.now() / 1000) * 0.5 + Math.sin(Date.now() / 3300) * 0.2; | |
| inputs[5] = Math.cos(Date.now() / 1400) * 0.4 + Math.cos(Date.now() / 4500) * 0.2; | |
| } | |
| for (let i = 0; i < 6; i++) inputs[i] = clamp(inputs[i], -1.5, 1.5); | |
| return inputs; | |
| } | |
| // --- Drive loop --- | |
| let driveInterval = null; | |
| function startDriveLoop() { | |
| if (driveInterval) clearInterval(driveInterval); | |
| driveInterval = setInterval(() => { | |
| if (!isConnected || !sphero) return; | |
| const human = getHumanAction(); | |
| const ai = getNeuraxonAction(); | |
| let finalSpeed, finalTurn; | |
| if (controlMode === 'manual') { | |
| finalSpeed = Math.abs(human.speed); | |
| finalTurn = human.turn; | |
| if (human.speed < 0) finalTurn = 180; | |
| } else if (controlMode === 'hybrid') { | |
| const hSpd = Math.abs(human.speed); | |
| finalSpeed = Math.round(hSpd * 0.75 + ai.speed * 0.25); | |
| finalTurn = human.turn * 0.75 + ai.turn * 0.25; | |
| } else { | |
| finalSpeed = ai.speed; | |
| finalTurn = ai.turn; | |
| } | |
| heading = (heading + finalTurn + 360) % 360; | |
| sphero.roll(clamp(Math.round(finalSpeed), 0, 255), Math.round(heading)).catch(() => {}); | |
| if (controlMode === 'manual') { | |
| if (teachingActive && idleTicks > IDLE_TEACH_DELAY) { | |
| // Red-orange = teaching STOP | |
| sphero.setMainLED(200, 80, 50).catch(() => {}); | |
| } else if (teachingActive) { | |
| sphero.setMainLED(250, 180, 30).catch(() => {}); | |
| } else { | |
| sphero.setMainLED(40, 200, 140).catch(() => {}); | |
| } | |
| } else if (controlMode === 'hybrid') { | |
| if (teachingActive && nxonContributing) sphero.setMainLED(220, 180, 255).catch(() => {}); | |
| else if (teachingActive) sphero.setMainLED(250, 180, 30).catch(() => {}); | |
| else if (nxonContributing) sphero.setMainLED(160, 100, 255).catch(() => {}); | |
| else sphero.setMainLED(40, 140, 160).catch(() => {}); | |
| } else { | |
| if (nxonContributing) sphero.setMainLED(140, 80, 255).catch(() => {}); | |
| else sphero.setMainLED(30, 180, 220).catch(() => {}); | |
| } | |
| $('speedVal').textContent = Math.round(finalSpeed); | |
| $('headingVal').textContent = Math.round(heading) + 'Β°'; | |
| $('headingNeedle').style.transform = `rotate(${heading}deg)`; | |
| }, 60); | |
| } | |
| function stopDriveLoop() { | |
| if (driveInterval) { clearInterval(driveInterval); driveInterval = null; } | |
| if (sphero && isConnected) sphero.stop().catch(() => {}); | |
| } | |
| $('btnConnect').onclick = async () => { | |
| try { | |
| $('btnConnect').disabled = true; | |
| sphero = new SpheroMiniBLE(); | |
| sphero.onDisconnect = () => { | |
| sysLog('Disconnected from Sphero.'); | |
| stopDriveLoop(); | |
| updateConnectionUI(false); | |
| sphero = null; | |
| }; | |
| await sphero.connect(); | |
| sysLog('Setting up...'); | |
| await sphero.setMainLED(34, 211, 238); | |
| await sphero.resetYaw(); | |
| heading = 0; | |
| updateConnectionUI(true); | |
| startDriveLoop(); | |
| sysLog('Connected! Neuraxon brain online.'); | |
| } catch (e) { | |
| sysLog(`Connection failed: ${e.message}`); | |
| updateConnectionUI(false); | |
| } | |
| }; | |
| $('btnDisconnect').onclick = async () => { | |
| sysLog('Disconnecting...'); | |
| stopDriveLoop(); | |
| if (sphero) { | |
| try { await sphero.setMainLED(0, 0, 0); } catch (e) {} | |
| await sphero.disconnect(); | |
| } | |
| }; | |
| function updateConnectionUI(connected) { | |
| isConnected = connected; | |
| const s = $('connStatus'); | |
| s.textContent = connected ? 'CONNECTED' : 'OFFLINE'; | |
| s.className = 'status-badge ' + (connected ? 'on' : 'off'); | |
| $('btnConnect').disabled = connected; | |
| $('btnDisconnect').disabled = !connected; | |
| } | |
| // --- Controls --- | |
| function bindControls() { | |
| const dirs = { Up: 'up', Down: 'down', Left: 'left', Right: 'right' }; | |
| for (const [btnSuffix, dir] of Object.entries(dirs)) { | |
| const btn = $('btn' + btnSuffix); | |
| const down = () => { activeKeys.add(dir); btn.classList.add('active'); }; | |
| const up = () => { activeKeys.delete(dir); btn.classList.remove('active'); }; | |
| btn.addEventListener('mousedown', down); | |
| btn.addEventListener('mouseup', up); | |
| btn.addEventListener('mouseleave', up); | |
| btn.addEventListener('touchstart', e => { e.preventDefault(); down(); }); | |
| btn.addEventListener('touchend', e => { e.preventDefault(); up(); }); | |
| } | |
| $('btnBrake').addEventListener('mousedown', () => activeKeys.clear()); | |
| $('btnBrake').addEventListener('touchstart', e => { e.preventDefault(); activeKeys.clear(); }); | |
| document.addEventListener('keydown', e => { | |
| if (['ArrowUp','ArrowDown','ArrowLeft','ArrowRight',' ','w','a','s','d','W','A','S','D'].includes(e.key)) e.preventDefault(); | |
| let dir = null; | |
| if (e.key === 'ArrowUp' || e.key.toLowerCase() === 'w') dir = 'up'; | |
| else if (e.key === 'ArrowDown' || e.key.toLowerCase() === 's') dir = 'down'; | |
| else if (e.key === 'ArrowLeft' || e.key.toLowerCase() === 'a') dir = 'left'; | |
| else if (e.key === 'ArrowRight' || e.key.toLowerCase() === 'd') dir = 'right'; | |
| else if (e.key === ' ') { activeKeys.clear(); return; } | |
| if (dir && !activeKeys.has(dir)) { | |
| activeKeys.add(dir); | |
| const btn = $('btn' + dir.charAt(0).toUpperCase() + dir.slice(1)); | |
| if (btn) btn.classList.add('active'); | |
| } | |
| }); | |
| document.addEventListener('keyup', e => { | |
| let dir = null; | |
| if (e.key === 'ArrowUp' || e.key.toLowerCase() === 'w') dir = 'up'; | |
| else if (e.key === 'ArrowDown' || e.key.toLowerCase() === 's') dir = 'down'; | |
| else if (e.key === 'ArrowLeft' || e.key.toLowerCase() === 'a') dir = 'left'; | |
| else if (e.key === 'ArrowRight' || e.key.toLowerCase() === 'd') dir = 'right'; | |
| if (dir) { | |
| activeKeys.delete(dir); | |
| const btn = $('btn' + dir.charAt(0).toUpperCase() + dir.slice(1)); | |
| if (btn) btn.classList.remove('active'); | |
| } | |
| }); | |
| } | |
| bindControls(); | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // ENHANCED INTERACTIVE VISUALIZATION | |
| // β Continuous Panning, Proximity Focus, Polar Layout | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| const canvas = $('neuralCanvas'); | |
| const ctx = canvas.getContext('2d'); | |
| const oscCanvas = $('oscCanvas'); | |
| const oscCtx = oscCanvas.getContext('2d'); | |
| const actCanvas = $('activityCanvas'); | |
| const actCtx = actCanvas.getContext('2d'); | |
| // ββ Camera / Pan State ββ | |
| let camX = 0, camY = 0; // world offset (panning) | |
| let camZoom = 1.0; | |
| let isDragging = false; | |
| let dragStartX = 0, dragStartY = 0; | |
| let dragStartCamX = 0, dragStartCamY = 0; | |
| let hasDragged = false; // track if user has interacted | |
| // ββ Focused Node State ββ | |
| let focusedNodeId = -1; | |
| let focusAnimT = 0; // animation interpolant | |
| // ββ Synapse Pulse Particles ββ | |
| const pulseParticles = []; | |
| const MAX_PARTICLES = 80; | |
| function resizeCanvas() { | |
| const rect = canvas.parentElement.getBoundingClientRect(); | |
| canvas.width = rect.width * window.devicePixelRatio; | |
| canvas.height = rect.height * window.devicePixelRatio; | |
| ctx.setTransform(window.devicePixelRatio, 0, 0, window.devicePixelRatio, 0, 0); | |
| } | |
| window.addEventListener('resize', resizeCanvas); | |
| resizeCanvas(); | |
| // ββ Mouse / Touch Handlers for Panning ββ | |
| const panelCenter = $('panelCenter'); | |
| panelCenter.addEventListener('mousedown', e => { | |
| if (e.target !== canvas) return; | |
| isDragging = true; | |
| dragStartX = e.clientX; | |
| dragStartY = e.clientY; | |
| dragStartCamX = camX; | |
| dragStartCamY = camY; | |
| }); | |
| window.addEventListener('mousemove', e => { | |
| if (!isDragging) return; | |
| const dx = e.clientX - dragStartX; | |
| const dy = e.clientY - dragStartY; | |
| camX = dragStartCamX + dx / camZoom; | |
| camY = dragStartCamY + dy / camZoom; | |
| if (!hasDragged && (Math.abs(dx) > 5 || Math.abs(dy) > 5)) { | |
| hasDragged = true; | |
| $('panHint').classList.add('hidden'); | |
| } | |
| }); | |
| window.addEventListener('mouseup', () => { isDragging = false; }); | |
| // Zoom with scroll wheel | |
| panelCenter.addEventListener('wheel', e => { | |
| e.preventDefault(); | |
| const delta = e.deltaY > 0 ? 0.92 : 1.08; | |
| camZoom = clamp(camZoom * delta, 0.3, 4.0); | |
| if (!hasDragged) { hasDragged = true; $('panHint').classList.add('hidden'); } | |
| }, { passive: false }); | |
| // Touch support for mobile | |
| let touchStartX = 0, touchStartY = 0, touchStartCamX = 0, touchStartCamY = 0; | |
| panelCenter.addEventListener('touchstart', e => { | |
| if (e.touches.length === 1) { | |
| isDragging = true; | |
| touchStartX = e.touches[0].clientX; | |
| touchStartY = e.touches[0].clientY; | |
| touchStartCamX = camX; | |
| touchStartCamY = camY; | |
| } | |
| }, { passive: true }); | |
| panelCenter.addEventListener('touchmove', e => { | |
| if (!isDragging || e.touches.length !== 1) return; | |
| const dx = e.touches[0].clientX - touchStartX; | |
| const dy = e.touches[0].clientY - touchStartY; | |
| camX = touchStartCamX + dx / camZoom; | |
| camY = touchStartCamY + dy / camZoom; | |
| if (!hasDragged) { hasDragged = true; $('panHint').classList.add('hidden'); } | |
| }, { passive: true }); | |
| panelCenter.addEventListener('touchend', () => { isDragging = false; }); | |
| // ββ World-to-Screen coordinate transforms ββ | |
| function worldToScreen(wx, wy) { | |
| const w = canvas.width / window.devicePixelRatio; | |
| const h = canvas.height / window.devicePixelRatio; | |
| return { | |
| x: w / 2 + (wx + camX) * camZoom, | |
| y: h / 2 + (wy + camY) * camZoom, | |
| }; | |
| } | |
| function screenToWorld(sx, sy) { | |
| const w = canvas.width / window.devicePixelRatio; | |
| const h = canvas.height / window.devicePixelRatio; | |
| return { | |
| x: (sx - w / 2) / camZoom - camX, | |
| y: (sy - h / 2) / camZoom - camY, | |
| }; | |
| } | |
| // ββ Find the nearest node to the screen center (focal point) ββ | |
| function findFocalNode() { | |
| const w = canvas.width / window.devicePixelRatio; | |
| const h = canvas.height / window.devicePixelRatio; | |
| // Focal point is screen center | |
| const focal = screenToWorld(w / 2, h / 2); | |
| let minDist = Infinity; | |
| let nearestId = -1; | |
| for (const n of network.neurons) { | |
| const dx = n.x - focal.x; | |
| const dy = n.y - focal.y; | |
| const d = dx * dx + dy * dy; | |
| if (d < minDist) { minDist = d; nearestId = n.id; } | |
| } | |
| return nearestId; | |
| } | |
| // ββ Update Focus HUD ββ | |
| function updateFocusHUD(nodeId) { | |
| const hud = $('focusHud'); | |
| if (nodeId < 0) { hud.classList.remove('visible'); return; } | |
| hud.classList.add('visible'); | |
| const n = network.neurons[nodeId]; | |
| $('focusNodeId').textContent = '#' + n.id; | |
| $('focusNodeId').style.color = n.state === 1 ? '#22d3ee' : n.state === -1 ? '#f472b6' : '#475569'; | |
| $('focusType').textContent = n.type.toUpperCase(); | |
| $('focusType').style.color = n.type === 'input' ? '#fbbf24' : n.type === 'output' ? '#34d399' : '#7aa3c0'; | |
| const stateLabel = n.state === 1 ? '+1 EXCITE' : n.state === -1 ? '-1 INHIBIT' : ' 0 NEUTRAL'; | |
| $('focusState').textContent = stateLabel; | |
| $('focusState').style.color = n.state === 1 ? '#22d3ee' : n.state === -1 ? '#f472b6' : '#475569'; | |
| $('focusS').textContent = n.s.toFixed(3); | |
| $('focusAdapt').textContent = n.adaptation.toFixed(4); | |
| $('focusHealth').textContent = (n.health * 100).toFixed(1) + '%'; | |
| // Count synapses | |
| let synIn = 0, synOut = 0; | |
| for (const syn of network.synapses) { | |
| if (syn.postId === nodeId) synIn++; | |
| if (syn.preId === nodeId) synOut++; | |
| } | |
| $('focusSyn').textContent = `in:${synIn} out:${synOut}`; | |
| } | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // DRAW NETWORK β Enhanced Interactive Polar Visualization | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| const INPUT_LABELS = ['FWD', 'LEFT', 'RIGHT', 'BACK', 'EXPLORE', 'RHYTHM']; | |
| const OUTPUT_LABELS = ['SPD+', 'SPD-', 'TRNβ', 'TRNβ']; | |
| function drawNetwork(stats) { | |
| const w = canvas.width / window.devicePixelRatio; | |
| const h = canvas.height / window.devicePixelRatio; | |
| ctx.clearRect(0, 0, w, h); | |
| const time = Date.now() / 1000; | |
| // ββ Background: Concentric polar grid rings ββ | |
| ctx.save(); | |
| const center = worldToScreen(0, 0); | |
| const ringRadii = [80, 190, 230, 260, 350]; | |
| for (const r of ringRadii) { | |
| const sr = r * camZoom; | |
| ctx.beginPath(); | |
| ctx.arc(center.x, center.y, sr, 0, Math.PI * 2); | |
| ctx.strokeStyle = 'rgba(34,211,238,0.04)'; | |
| ctx.lineWidth = 1; | |
| ctx.stroke(); | |
| } | |
| // Radial spokes | |
| for (let i = 0; i < 12; i++) { | |
| const angle = (Math.PI * 2 * i) / 12; | |
| const outer = 400 * camZoom; | |
| ctx.beginPath(); | |
| ctx.moveTo(center.x, center.y); | |
| ctx.lineTo(center.x + outer * Math.cos(angle), center.y + outer * Math.sin(angle)); | |
| ctx.strokeStyle = 'rgba(34,211,238,0.02)'; | |
| ctx.lineWidth = 0.5; | |
| ctx.stroke(); | |
| } | |
| ctx.restore(); | |
| // ββ Find and update focal node ββ | |
| const newFocusId = findFocalNode(); | |
| if (newFocusId !== focusedNodeId) { | |
| focusedNodeId = newFocusId; | |
| focusAnimT = 0; | |
| } | |
| focusAnimT = Math.min(1, focusAnimT + 0.08); | |
| updateFocusHUD(focusedNodeId); | |
| // ββ Spawn pulse particles on active synapses ββ | |
| if (pulseParticles.length < MAX_PARTICLES && Math.random() < 0.3) { | |
| for (const syn of network.synapses) { | |
| if (syn.silent) continue; | |
| const pre = network.neurons[syn.preId]; | |
| if (pre.state !== 0 && Math.random() < 0.008) { | |
| pulseParticles.push({ | |
| preId: syn.preId, postId: syn.postId, | |
| t: 0, speed: 0.015 + Math.random() * 0.02, | |
| color: pre.state === 1 ? [34,211,238] : [244,114,182], | |
| }); | |
| if (pulseParticles.length >= MAX_PARTICLES) break; | |
| } | |
| } | |
| } | |
| // ββ Draw Synapses ββ | |
| ctx.lineWidth = 0.5; | |
| for (const syn of network.synapses) { | |
| if (syn.silent) continue; | |
| const pre = network.neurons[syn.preId]; | |
| const post = network.neurons[syn.postId]; | |
| const p1 = worldToScreen(pre.x, pre.y); | |
| const p2 = worldToScreen(post.x, post.y); | |
| const strength = Math.abs(syn.wFast + syn.wSlow); | |
| // Highlight synapses connected to focused node | |
| const isFocusConn = (syn.preId === focusedNodeId || syn.postId === focusedNodeId); | |
| const alpha = isFocusConn ? 0.15 + strength * 0.4 : 0.02 + strength * 0.08; | |
| const lw = isFocusConn ? 1.2 : 0.5; | |
| if (syn.wFast + syn.wSlow > 0) { | |
| ctx.strokeStyle = `rgba(34,211,238,${alpha})`; | |
| } else { | |
| ctx.strokeStyle = `rgba(244,114,182,${alpha})`; | |
| } | |
| ctx.lineWidth = lw; | |
| const mx = (p1.x + p2.x) / 2 + (p2.y - p1.y) * 0.12; | |
| const my = (p1.y + p2.y) / 2 - (p2.x - p1.x) * 0.12; | |
| ctx.beginPath(); | |
| ctx.moveTo(p1.x, p1.y); | |
| ctx.quadraticCurveTo(mx, my, p2.x, p2.y); | |
| ctx.stroke(); | |
| } | |
| // ββ Draw Pulse Particles ββ | |
| for (let i = pulseParticles.length - 1; i >= 0; i--) { | |
| const p = pulseParticles[i]; | |
| p.t += p.speed; | |
| if (p.t >= 1) { pulseParticles.splice(i, 1); continue; } | |
| const pre = network.neurons[p.preId]; | |
| const post = network.neurons[p.postId]; | |
| const p1 = worldToScreen(pre.x, pre.y); | |
| const p2 = worldToScreen(post.x, post.y); | |
| const mx = (p1.x + p2.x) / 2 + (p2.y - p1.y) * 0.12; | |
| const my = (p1.y + p2.y) / 2 - (p2.x - p1.x) * 0.12; | |
| // Quadratic bezier interpolation | |
| const t = p.t; | |
| const u = 1 - t; | |
| const px = u * u * p1.x + 2 * u * t * mx + t * t * p2.x; | |
| const py = u * u * p1.y + 2 * u * t * my + t * t * p2.y; | |
| const [cr, cg, cb] = p.color; | |
| const pAlpha = Math.sin(t * Math.PI) * 0.8; | |
| ctx.beginPath(); | |
| ctx.arc(px, py, 2 * camZoom, 0, Math.PI * 2); | |
| ctx.fillStyle = `rgba(${cr},${cg},${cb},${pAlpha})`; | |
| ctx.fill(); | |
| } | |
| // ββ Draw Neurons ββ | |
| for (const n of network.neurons) { | |
| const pos = worldToScreen(n.x, n.y); | |
| const isFocused = (n.id === focusedNodeId); | |
| const easeT = isFocused ? focusAnimT : 0; | |
| // Base styling | |
| let color, glowColor; | |
| if (n.state === 1) { | |
| color = '#22d3ee'; glowColor = 'rgba(34,211,238,0.5)'; | |
| } else if (n.state === -1) { | |
| color = '#f472b6'; glowColor = 'rgba(244,114,182,0.5)'; | |
| } else { | |
| color = '#475569'; glowColor = 'rgba(71,85,105,0.15)'; | |
| } | |
| // Radius: focused nodes are much larger | |
| let baseR = n.type === 'input' ? 6 : n.type === 'output' ? 8 : 4.5; | |
| if (n.state !== 0) baseR += 1.5; | |
| const focusR = baseR * 2.5; | |
| const radius = (baseR + (focusR - baseR) * easeT) * camZoom; | |
| // Glow effect | |
| if (n.state !== 0 || isFocused) { | |
| const glowR = radius * (isFocused ? 5 : 3.5); | |
| ctx.beginPath(); | |
| const grad = ctx.createRadialGradient(pos.x, pos.y, 0, pos.x, pos.y, glowR); | |
| grad.addColorStop(0, isFocused ? 'rgba(251,191,36,0.2)' : glowColor); | |
| grad.addColorStop(1, 'transparent'); | |
| ctx.fillStyle = grad; | |
| ctx.arc(pos.x, pos.y, glowR, 0, Math.PI * 2); | |
| ctx.fill(); | |
| } | |
| // Pulsing ring for active nodes | |
| if (n.state !== 0) { | |
| const pulseR = radius + Math.sin(time * 4 + n.id) * 2 * camZoom; | |
| ctx.beginPath(); | |
| ctx.arc(pos.x, pos.y, pulseR + 3 * camZoom, 0, Math.PI * 2); | |
| ctx.strokeStyle = n.state === 1 ? 'rgba(34,211,238,0.15)' : 'rgba(244,114,182,0.15)'; | |
| ctx.lineWidth = 1; | |
| ctx.stroke(); | |
| } | |
| // Body | |
| ctx.beginPath(); | |
| ctx.arc(pos.x, pos.y, radius, 0, Math.PI * 2); | |
| ctx.fillStyle = color; | |
| ctx.fill(); | |
| // ββ FOCUSED STATE: yellow dashed border + metadata ββ | |
| if (isFocused) { | |
| ctx.save(); | |
| ctx.beginPath(); | |
| ctx.arc(pos.x, pos.y, radius + 4 * camZoom, 0, Math.PI * 2); | |
| ctx.setLineDash([4, 4]); | |
| ctx.strokeStyle = `rgba(251,191,36,${0.6 + 0.3 * Math.sin(time * 3)})`; | |
| ctx.lineWidth = 2; | |
| ctx.stroke(); | |
| ctx.setLineDash([]); | |
| ctx.restore(); | |
| // Show full label near the node | |
| ctx.save(); | |
| ctx.font = `bold ${11 * camZoom}px 'JetBrains Mono'`; | |
| ctx.textAlign = 'center'; | |
| ctx.fillStyle = 'rgba(251,191,36,0.9)'; | |
| let label = `#${n.id}`; | |
| if (n.type === 'input' && n.id < INPUT_LABELS.length) label = INPUT_LABELS[n.id]; | |
| else if (n.type === 'output') label = OUTPUT_LABELS[n.id - network.nInput - network.nHidden] || `OUT${n.id}`; | |
| ctx.fillText(label, pos.x, pos.y - radius - 8 * camZoom); | |
| // State below | |
| ctx.font = `${9 * camZoom}px 'JetBrains Mono'`; | |
| ctx.fillStyle = color; | |
| const stateStr = n.state === 1 ? '+1' : n.state === -1 ? '-1' : '0'; | |
| ctx.fillText(`s=${n.s.toFixed(2)} [${stateStr}]`, pos.x, pos.y + radius + 14 * camZoom); | |
| ctx.restore(); | |
| } else { | |
| // ββ CONTEXT STATE: minimal ID label ββ | |
| // Type indicator ring | |
| if (n.type === 'input') { | |
| ctx.beginPath(); | |
| ctx.arc(pos.x, pos.y, radius + 2.5 * camZoom, 0, Math.PI * 2); | |
| ctx.strokeStyle = 'rgba(251,191,36,0.4)'; | |
| ctx.lineWidth = 1; | |
| ctx.stroke(); | |
| } else if (n.type === 'output') { | |
| ctx.beginPath(); | |
| ctx.arc(pos.x, pos.y, radius + 2.5 * camZoom, 0, Math.PI * 2); | |
| ctx.strokeStyle = 'rgba(52,211,153,0.4)'; | |
| ctx.lineWidth = 1.5; | |
| ctx.stroke(); | |
| } | |
| // Small ID label only when zoomed in enough | |
| if (camZoom > 0.7) { | |
| ctx.font = `${Math.max(7, 8 * camZoom)}px 'JetBrains Mono'`; | |
| ctx.textAlign = 'center'; | |
| ctx.textBaseline = 'middle'; | |
| ctx.fillStyle = 'rgba(224,242,254,0.35)'; | |
| ctx.fillText(n.id, pos.x, pos.y); | |
| } | |
| } | |
| } | |
| // ββ Ring labels ββ | |
| ctx.save(); | |
| ctx.font = `${9 * camZoom}px 'JetBrains Mono'`; | |
| ctx.textAlign = 'center'; | |
| ctx.fillStyle = 'rgba(251,191,36,0.35)'; | |
| const lbl1 = worldToScreen(0, -55); | |
| ctx.fillText('INPUT', lbl1.x, lbl1.y); | |
| ctx.fillStyle = 'rgba(148,163,184,0.2)'; | |
| const lbl2 = worldToScreen(0, -165); | |
| ctx.fillText('HIDDEN', lbl2.x, lbl2.y); | |
| ctx.fillStyle = 'rgba(52,211,153,0.3)'; | |
| const lbl3 = worldToScreen(0, -380); | |
| ctx.fillText('OUTPUT', lbl3.x, lbl3.y); | |
| ctx.restore(); | |
| // ββ Focal Crosshair / Reticle ββ | |
| const cx = w / 2, cy = h / 2; | |
| ctx.save(); | |
| ctx.strokeStyle = `rgba(251,191,36,${0.25 + 0.1 * Math.sin(time * 2)})`; | |
| ctx.lineWidth = 1; | |
| const crossLen = 12; | |
| const gap = 6; | |
| // Four short lines forming a crosshair | |
| ctx.beginPath(); | |
| ctx.moveTo(cx - crossLen - gap, cy); ctx.lineTo(cx - gap, cy); | |
| ctx.moveTo(cx + gap, cy); ctx.lineTo(cx + crossLen + gap, cy); | |
| ctx.moveTo(cx, cy - crossLen - gap); ctx.lineTo(cx, cy - gap); | |
| ctx.moveTo(cx, cy + gap); ctx.lineTo(cx, cy + crossLen + gap); | |
| ctx.stroke(); | |
| // Small rotating diamond | |
| ctx.translate(cx, cy); | |
| ctx.rotate(time * 0.5); | |
| ctx.strokeStyle = `rgba(251,191,36,${0.15 + 0.08 * Math.sin(time * 3)})`; | |
| ctx.beginPath(); | |
| const d = 20; | |
| ctx.moveTo(0, -d); ctx.lineTo(d, 0); ctx.lineTo(0, d); ctx.lineTo(-d, 0); ctx.closePath(); | |
| ctx.stroke(); | |
| ctx.restore(); | |
| } | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // OSCILLATOR + ACTIVITY TRACE (unchanged) | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function drawOscillators() { | |
| const w = oscCanvas.width, h = oscCanvas.height; | |
| oscCtx.clearRect(0, 0, w, h); | |
| const bands = network.oscillators.bands; | |
| const colors = ['#3a5f7a', '#60a5fa', '#a78bfa', '#22d3ee', '#fbbf24']; | |
| const bandH = h / bands.length; | |
| for (let i = 0; i < bands.length; i++) { | |
| const b = bands[i]; | |
| const y0 = i * bandH + bandH / 2; | |
| oscCtx.beginPath(); | |
| oscCtx.strokeStyle = colors[i]; | |
| oscCtx.lineWidth = 1.5; | |
| for (let x = 0; x < w; x++) { | |
| const t = x / w * Math.PI * 4; | |
| const val = Math.sin(b.phase + t) * b.amplitude * (bandH * 0.35); | |
| if (x === 0) oscCtx.moveTo(x, y0 + val); | |
| else oscCtx.lineTo(x, y0 + val); | |
| } | |
| oscCtx.stroke(); | |
| oscCtx.font = '8px JetBrains Mono'; | |
| oscCtx.fillStyle = colors[i]; | |
| oscCtx.textAlign = 'left'; | |
| oscCtx.fillText(b.name, 3, i * bandH + 10); | |
| } | |
| } | |
| function drawActivityTrace() { | |
| const w = actCanvas.width, h = actCanvas.height; | |
| actCtx.clearRect(0, 0, w, h); | |
| if (activityHistory.length < 2) return; | |
| const step = w / MAX_HIST; | |
| actCtx.beginPath(); | |
| actCtx.strokeStyle = 'rgba(34,211,238,0.7)'; | |
| actCtx.lineWidth = 1; | |
| for (let i = 0; i < activityHistory.length; i++) { | |
| const x = i * step; | |
| const y = h - activityHistory[i].excFrac * h; | |
| if (i === 0) actCtx.moveTo(x, y); else actCtx.lineTo(x, y); | |
| } | |
| actCtx.stroke(); | |
| actCtx.beginPath(); | |
| actCtx.strokeStyle = 'rgba(244,114,182,0.7)'; | |
| for (let i = 0; i < activityHistory.length; i++) { | |
| const x = i * step; | |
| const y = h - activityHistory[i].inhFrac * h; | |
| if (i === 0) actCtx.moveTo(x, y); else actCtx.lineTo(x, y); | |
| } | |
| actCtx.stroke(); | |
| } | |
| function updateReceptorBars(R) { | |
| const mapping = { | |
| D1: 'D1', D2: 'D2', SHT1A: '5HT1A', SHT2A: '5HT2A', SHT4: '5HT4', | |
| M1: 'M1', M2: 'M2', B1: '1', A2: '2', | |
| }; | |
| for (const [key, name] of Object.entries(mapping)) { | |
| const val = R[key] || 0; | |
| const barId = 'rbar_' + name.replace(/[^a-zA-Z0-9]/g, ''); | |
| const el = document.getElementById(barId); | |
| if (el) el.style.width = (val * 100) + '%'; | |
| } | |
| } | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // MAIN SIMULATION LOOP | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| const SIM_DT = 16; | |
| function simulationTick() { | |
| if (!simRunning) { requestAnimationFrame(simulationTick); return; } | |
| const inputs = computeSensoryInputs(); | |
| network.setInputs(inputs); | |
| const stats = network.simulateStep(SIM_DT); | |
| applyTeachingSignal(); | |
| activityHistory.push({ | |
| excFrac: stats.excCount / network.N, | |
| inhFrac: stats.inhCount / network.N, | |
| }); | |
| if (activityHistory.length > MAX_HIST) activityHistory.shift(); | |
| // Update stats UI | |
| $('statExc').textContent = stats.excCount; | |
| $('statInh').textContent = stats.inhCount; | |
| $('statNeu').textContent = stats.neuCount; | |
| $('statSyn').textContent = network.synapses.length; | |
| $('stepCount').textContent = network.step; | |
| $('energyVal').textContent = network.energy.toFixed(2); | |
| $('statDw').textContent = network.meanDw.toFixed(4); | |
| $('statStruct').textContent = network.structEvent || (teachingActive ? (idleTicks > IDLE_TEACH_DELAY ? 'STOP-LEARN' : 'LEARNING') : 'stable'); | |
| $('statStruct').style.color = teachingActive ? (idleTicks > IDLE_TEACH_DELAY ? 'var(--red)' : 'var(--da-color)') : 'var(--green)'; | |
| $('statLearn').textContent = learnCount; | |
| $('statAccum').textContent = outputAccum.map(v => v.toFixed(1)).join(' '); | |
| const pct = clamp(learnAccuracy, 0, 100); | |
| $('learnPct').textContent = pct + '%'; | |
| $('learnBar').style.width = pct + '%'; | |
| $('learnBar').className = 'progress-fill' + (pct > 60 ? ' learned' : ''); | |
| if (learnCount === 0) { | |
| $('learnBarText').textContent = 'Drive in MANUAL to teach'; | |
| } else if (pct < 30) { | |
| $('learnBarText').textContent = 'Learning... drive, turn & stop!'; | |
| } else if (pct < 60) { | |
| $('learnBarText').textContent = 'Getting better β try HYBRID'; | |
| } else { | |
| $('learnBarText').textContent = 'Well trained! Neuraxon ready'; | |
| } | |
| // Source tags | |
| const tagH = $('tagHuman'); | |
| const tagN = $('tagNxon'); | |
| const tagL = $('tagLearn'); | |
| const srcDetail = $('sourceDetail'); | |
| if (controlMode === 'manual') { | |
| tagH.style.opacity = activeKeys.size > 0 ? '1' : '0.5'; | |
| tagN.style.opacity = '0.2'; | |
| tagL.style.opacity = teachingActive ? '1' : '0.2'; | |
| srcDetail.textContent = teachingActive ? 'Teaching network from your input' : 'Manual control β press keys to teach'; | |
| $('blendVal').textContent = 'H:100% N:0%'; | |
| } else if (controlMode === 'hybrid') { | |
| tagH.style.opacity = activeKeys.size > 0 ? '1' : '0.4'; | |
| tagN.style.opacity = nxonContributing ? '1' : '0.3'; | |
| tagL.style.opacity = teachingActive ? '1' : '0.2'; | |
| srcDetail.textContent = nxonContributing | |
| ? (teachingActive ? 'Both active β learning + Neuraxon assisting' : 'Neuraxon gently assisting (25%)') | |
| : (teachingActive ? 'Teaching from your input' : 'Hybrid idle β you lead, Neuraxon assists'); | |
| $('blendVal').textContent = 'H:75% N:25%'; | |
| } | |
| // Motor display | |
| const ai = getNeuraxonAction(); | |
| const human = getHumanAction(); | |
| let dispSpeed, dispTurn; | |
| if (controlMode === 'manual') { | |
| dispSpeed = Math.abs(human.speed); | |
| dispTurn = human.turn; | |
| } else if (controlMode === 'hybrid') { | |
| dispSpeed = Math.round(Math.abs(human.speed) * 0.75 + ai.speed * 0.25); | |
| dispTurn = human.turn * 0.75 + ai.turn * 0.25; | |
| } else { | |
| dispSpeed = ai.speed; | |
| dispTurn = ai.turn; | |
| } | |
| if (!isConnected) { | |
| heading = (heading + dispTurn + 360) % 360; | |
| } | |
| $('speedVal').textContent = Math.round(dispSpeed); | |
| $('headingVal').textContent = Math.round(heading) + 'Β°'; | |
| $('headingNeedle').style.transform = `rotate(${heading}deg)`; | |
| // Draw visuals | |
| drawNetwork(stats); | |
| drawOscillators(); | |
| drawActivityTrace(); | |
| updateReceptorBars(stats.R); | |
| requestAnimationFrame(simulationTick); | |
| } | |
| // Start | |
| sysLog('Neuraxon 2.0 brain initialized.'); | |
| sysLog('6 input β 24 hidden β 4 output neurons'); | |
| sysLog(`${network.synapses.length} synapses (small-world + motor pathways)`); | |
| sysLog('DA-gated STDP learning with 9 receptor subtypes'); | |
| sysLog('Learns: forward, turns, AND stopping'); | |
| sysLog('Replay buffer: Neuraxon replays your moves'); | |
| sysLog('βββββββββββββββββββββββββββββββββββββββ'); | |
| sysLog('HOW TO USE:'); | |
| sysLog('1. MANUAL mode β drive with WASD/arrows'); | |
| sysLog(' β Network learns from every keypress'); | |
| sysLog(' β Watch accuracy % rise in LEARNING panel'); | |
| sysLog(' β Gold LED = learning in progress'); | |
| sysLog('2. HYBRID mode β 75% you + 25% Neuraxon'); | |
| sysLog(' β Neuraxon gently assists, you lead'); | |
| sysLog(' β Still learns when you press keys'); | |
| sysLog('TIP: Drive fwd, turn, AND STOP to teach'); | |
| sysLog(' the full repertoire of behaviors'); | |
| sysLog('βββββββββββββββββββββββββββββββββββββββ'); | |
| sysLog('NEURAL GRAPH: Drag to pan, scroll to zoom'); | |
| sysLog('Nearest node auto-focuses with metadata'); | |
| sysLog('βββββββββββββββββββββββββββββββββββββββ'); | |
| requestAnimationFrame(simulationTick); | |
| </script> | |
| </body> | |
| </html> |