| <!DOCTYPE html> |
| <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 { |
| 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); |
| } |
| |
| |
| .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 !important; } |
| } |
| |
| |
| .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; |
| } |
| |
| |
| .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%; } |
| |
| |
| .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-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-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-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); } |
| |
| |
| .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; } |
| |
| |
| #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); } |
| |
| |
| .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; } |
| |
| |
| .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-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-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-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 { |
| 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%; } |
| |
| |
| .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; } |
| |
| |
| .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"> |
|
|
| |
| <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> |
|
|
| |
| <div class="panel-center" id="panelCenter"> |
| <canvas id="neuralCanvas"></canvas> |
|
|
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <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> |
| |
| |
| |
| |
| 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); } |
| } |
| |
| |
| |
| |
| |
| |
| |
| 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); |
| |
| 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; |
| |
| 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() { |
| |
| |
| const cx = 0, cy = 0; |
| |
| |
| 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); |
| } |
| |
| 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); |
| } |
| |
| 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 }; |
| } |
| } |
| |
| |
| |
| |
| |
| |
| 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; |
| |
| |
| let idleTicks = 0; |
| const IDLE_TEACH_DELAY = 25; |
| |
| |
| const replayBuffer = []; |
| 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; |
| |
| const network = new NeuraxonNetwork(6, 24, 4); |
| |
| const activityHistory = []; |
| const MAX_HIST = 200; |
| |
| |
| 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; } |
| |
| |
| if (!teachingActive && (controlMode === 'manual' || controlMode === 'hybrid')) { |
| idleTicks++; |
| |
| if (idleTicks > IDLE_TEACH_DELAY && idleTicks < IDLE_TEACH_DELAY + 60) { |
| teachingActive = true; |
| |
| for (let i = 0; i < 4; i++) { |
| const n = network.neurons[outStart + i]; |
| n.s = -0.3; |
| n.state = 0; |
| } |
| |
| network.neuromod.modulators.DA.phasic = clamp( |
| network.neuromod.modulators.DA.phasic + 0.08, 0, 1 |
| ); |
| learnCount++; |
| return; |
| } |
| } else { |
| idleTicks = 0; |
| } |
| |
| if (teachingActive && (controlMode === 'manual' || controlMode === 'hybrid')) { |
| const isTurnTeach = (teachingTarget[2] === 1 || teachingTarget[3] === 1); |
| |
| |
| const currentInputs = computeSensoryInputs(); |
| replayBuffer.push({ inputs: [...currentInputs], target: [...teachingTarget] }); |
| if (replayBuffer.length > REPLAY_MAX) replayBuffer.shift(); |
| |
| |
| 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); |
| } |
| |
| |
| 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; |
| } |
| } |
| |
| |
| 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') { |
| |
| |
| if (replayBuffer.length > 20 && Math.random() < 0.6) { |
| |
| isReplaying = true; |
| const frame = replayBuffer[replayIdx % replayBuffer.length]; |
| replayIdx++; |
| |
| for (let i = 0; i < 4; i++) { |
| inputs[i] = frame.inputs[i] * 0.8; |
| } |
| } else { |
| |
| 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; |
| case 1: inputs[1] = intensity; break; |
| case 2: inputs[2] = intensity; break; |
| case 3: inputs[0] = intensity; inputs[2] = intensity * 0.6; break; |
| case 4: inputs[0] = intensity; inputs[1] = intensity * 0.6; break; |
| case 5: inputs[3] = intensity * 0.5; break; |
| case 6: break; |
| case 7: break; |
| } |
| } |
| |
| |
| 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); |
| |
| |
| 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; |
| } |
| |
| |
| 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) { |
| |
| 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; |
| } |
| |
| |
| 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(); |
| |
| |
| |
| |
| |
| |
| |
| const canvas = $('neuralCanvas'); |
| const ctx = canvas.getContext('2d'); |
| const oscCanvas = $('oscCanvas'); |
| const oscCtx = oscCanvas.getContext('2d'); |
| const actCanvas = $('activityCanvas'); |
| const actCtx = actCanvas.getContext('2d'); |
| |
| |
| let camX = 0, camY = 0; |
| let camZoom = 1.0; |
| let isDragging = false; |
| let dragStartX = 0, dragStartY = 0; |
| let dragStartCamX = 0, dragStartCamY = 0; |
| let hasDragged = false; |
| |
| |
| let focusedNodeId = -1; |
| let focusAnimT = 0; |
| |
| |
| 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(); |
| |
| |
| 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; }); |
| |
| |
| 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 }); |
| |
| |
| 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; }); |
| |
| |
| |
| 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, |
| }; |
| } |
| |
| |
| function findFocalNode() { |
| const w = canvas.width / window.devicePixelRatio; |
| const h = canvas.height / window.devicePixelRatio; |
| |
| 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; |
| } |
| |
| |
| 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) + '%'; |
| |
| 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}`; |
| } |
| |
| |
| |
| |
| |
| |
| 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; |
| |
| |
| 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(); |
| } |
| |
| 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(); |
| |
| |
| const newFocusId = findFocalNode(); |
| if (newFocusId !== focusedNodeId) { |
| focusedNodeId = newFocusId; |
| focusAnimT = 0; |
| } |
| focusAnimT = Math.min(1, focusAnimT + 0.08); |
| updateFocusHUD(focusedNodeId); |
| |
| |
| 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; |
| } |
| } |
| } |
| |
| |
| 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); |
| |
| |
| 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(); |
| } |
| |
| |
| 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; |
| |
| 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(); |
| } |
| |
| |
| for (const n of network.neurons) { |
| const pos = worldToScreen(n.x, n.y); |
| const isFocused = (n.id === focusedNodeId); |
| const easeT = isFocused ? focusAnimT : 0; |
| |
| |
| 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)'; |
| } |
| |
| |
| 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; |
| |
| |
| 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(); |
| } |
| |
| |
| 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(); |
| } |
| |
| |
| ctx.beginPath(); |
| ctx.arc(pos.x, pos.y, radius, 0, Math.PI * 2); |
| ctx.fillStyle = color; |
| ctx.fill(); |
| |
| |
| 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(); |
| |
| |
| 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); |
| |
| 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 { |
| |
| |
| 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(); |
| } |
| |
| |
| 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); |
| } |
| } |
| } |
| |
| |
| 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(); |
| |
| |
| 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; |
| |
| 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(); |
| |
| 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(); |
| } |
| |
| |
| |
| |
| |
| |
| 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) + '%'; |
| } |
| } |
| |
| |
| |
| |
| |
| |
| 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(); |
| |
| |
| $('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'; |
| } |
| |
| |
| 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%'; |
| } |
| |
| |
| 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)`; |
| |
| |
| drawNetwork(stats); |
| drawOscillators(); |
| drawActivityTrace(); |
| updateReceptorBars(stats.R); |
| |
| requestAnimationFrame(simulationTick); |
| } |
| |
| |
| 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> |