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