| | <!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> |