Update index.html
Browse files- index.html +2146 -19
index.html
CHANGED
|
@@ -1,19 +1,2146 @@
|
|
| 1 |
-
<!
|
| 2 |
-
<html>
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>Sphero Γ Neuraxon 2.0 β Bio-Inspired Neural Control</title>
|
| 7 |
+
<style>
|
| 8 |
+
@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');
|
| 9 |
+
|
| 10 |
+
:root {
|
| 11 |
+
--bg-abyss: #030709;
|
| 12 |
+
--bg-deep: #060d14;
|
| 13 |
+
--bg-card: #0a1520;
|
| 14 |
+
--bg-raised: #0e1a28;
|
| 15 |
+
--bg-glass: rgba(10,21,32,0.75);
|
| 16 |
+
--border-faint: rgba(34,211,238,0.08);
|
| 17 |
+
--border-glow: rgba(34,211,238,0.2);
|
| 18 |
+
--border-active: rgba(34,211,238,0.5);
|
| 19 |
+
--txt-primary: #e0f2fe;
|
| 20 |
+
--txt-secondary: #7aa3c0;
|
| 21 |
+
--txt-dim: #3a5f7a;
|
| 22 |
+
--cyan: #22d3ee;
|
| 23 |
+
--cyan-bright: #67e8f9;
|
| 24 |
+
--green: #34d399;
|
| 25 |
+
--green-dim: rgba(52,211,153,0.15);
|
| 26 |
+
--red: #fb7185;
|
| 27 |
+
--red-dim: rgba(251,113,133,0.15);
|
| 28 |
+
--amber: #fbbf24;
|
| 29 |
+
--purple: #a78bfa;
|
| 30 |
+
--pink: #f472b6;
|
| 31 |
+
--blue: #60a5fa;
|
| 32 |
+
--excite: #22d3ee;
|
| 33 |
+
--neutral: #475569;
|
| 34 |
+
--inhibit: #f472b6;
|
| 35 |
+
--da-color: #fbbf24;
|
| 36 |
+
--sht-color: #a78bfa;
|
| 37 |
+
--ach-color: #34d399;
|
| 38 |
+
--na-color: #fb7185;
|
| 39 |
+
--mono: 'JetBrains Mono', monospace;
|
| 40 |
+
--sans: 'Outfit', system-ui, sans-serif;
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
| 44 |
+
html { height: 100%; }
|
| 45 |
+
body {
|
| 46 |
+
min-height: 100%; background: var(--bg-abyss); color: var(--txt-primary);
|
| 47 |
+
font-family: var(--sans); overflow-x: hidden;
|
| 48 |
+
background-image:
|
| 49 |
+
radial-gradient(ellipse 80% 50% at 20% 80%, rgba(34,211,238,0.03) 0%, transparent 60%),
|
| 50 |
+
radial-gradient(ellipse 60% 40% at 80% 20%, rgba(164,139,250,0.03) 0%, transparent 60%);
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
/* === HEADER === */
|
| 54 |
+
header {
|
| 55 |
+
position: sticky; top: 0; z-index: 100;
|
| 56 |
+
display: flex; justify-content: space-between; align-items: center;
|
| 57 |
+
padding: 12px 24px;
|
| 58 |
+
background: rgba(3,7,9,0.9); backdrop-filter: blur(20px) saturate(1.5);
|
| 59 |
+
border-bottom: 1px solid var(--border-faint);
|
| 60 |
+
}
|
| 61 |
+
.logo-area { display: flex; align-items: center; gap: 12px; }
|
| 62 |
+
.logo-area h1 {
|
| 63 |
+
font-size: 17px; font-weight: 700; letter-spacing: -0.02em;
|
| 64 |
+
background: linear-gradient(135deg, var(--cyan), var(--purple));
|
| 65 |
+
-webkit-background-clip: text; -webkit-text-fill-color: transparent;
|
| 66 |
+
}
|
| 67 |
+
.logo-area .subtitle { font-size: 10px; color: var(--txt-dim); font-family: var(--mono); letter-spacing: 0.08em; text-transform: uppercase; }
|
| 68 |
+
.header-right { display: flex; align-items: center; gap: 14px; }
|
| 69 |
+
.status-badge {
|
| 70 |
+
font-family: var(--mono); font-size: 10px; font-weight: 600; letter-spacing: 0.06em;
|
| 71 |
+
padding: 5px 14px; border-radius: 20px; border: 1px solid;
|
| 72 |
+
display: flex; align-items: center; gap: 6px;
|
| 73 |
+
}
|
| 74 |
+
.status-badge::before { content: ''; width: 6px; height: 6px; border-radius: 50%; }
|
| 75 |
+
.status-badge.on { color: var(--green); border-color: rgba(52,211,153,0.3); }
|
| 76 |
+
.status-badge.on::before { background: var(--green); box-shadow: 0 0 8px var(--green); }
|
| 77 |
+
.status-badge.off { color: var(--red); border-color: rgba(251,113,133,0.2); }
|
| 78 |
+
.status-badge.off::before { background: var(--red); }
|
| 79 |
+
.mode-indicator {
|
| 80 |
+
font-family: var(--mono); font-size: 10px; font-weight: 500;
|
| 81 |
+
padding: 5px 12px; border-radius: 6px; letter-spacing: 0.05em;
|
| 82 |
+
background: rgba(34,211,238,0.08); color: var(--cyan); border: 1px solid rgba(34,211,238,0.15);
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
/* === LAYOUT === */
|
| 86 |
+
.app-grid {
|
| 87 |
+
display: grid;
|
| 88 |
+
grid-template-columns: 280px 1fr 260px;
|
| 89 |
+
grid-template-rows: auto 1fr auto;
|
| 90 |
+
gap: 0;
|
| 91 |
+
height: calc(100vh - 49px);
|
| 92 |
+
max-height: calc(100vh - 49px);
|
| 93 |
+
}
|
| 94 |
+
@media (max-width: 1024px) {
|
| 95 |
+
.app-grid { grid-template-columns: 1fr; grid-template-rows: auto; height: auto; }
|
| 96 |
+
.panel-left, .panel-right { border: none !important; }
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
/* === PANELS === */
|
| 100 |
+
.panel-left {
|
| 101 |
+
grid-row: 1 / -1;
|
| 102 |
+
border-right: 1px solid var(--border-faint);
|
| 103 |
+
background: var(--bg-deep);
|
| 104 |
+
overflow-y: auto; padding: 16px;
|
| 105 |
+
display: flex; flex-direction: column; gap: 14px;
|
| 106 |
+
}
|
| 107 |
+
.panel-center {
|
| 108 |
+
grid-row: 1 / -1;
|
| 109 |
+
display: flex; flex-direction: column;
|
| 110 |
+
position: relative; overflow: hidden;
|
| 111 |
+
cursor: grab;
|
| 112 |
+
}
|
| 113 |
+
.panel-center:active { cursor: grabbing; }
|
| 114 |
+
.panel-right {
|
| 115 |
+
grid-row: 1 / -1;
|
| 116 |
+
border-left: 1px solid var(--border-faint);
|
| 117 |
+
background: var(--bg-deep);
|
| 118 |
+
overflow-y: auto; padding: 16px;
|
| 119 |
+
display: flex; flex-direction: column; gap: 14px;
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
/* === CARDS === */
|
| 123 |
+
.card {
|
| 124 |
+
background: var(--bg-card); border: 1px solid var(--border-faint);
|
| 125 |
+
border-radius: 10px; padding: 14px; position: relative;
|
| 126 |
+
}
|
| 127 |
+
.card-title {
|
| 128 |
+
font-size: 10px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.1em;
|
| 129 |
+
color: var(--txt-dim); margin-bottom: 12px; display: flex; align-items: center; gap: 6px;
|
| 130 |
+
}
|
| 131 |
+
.card-title .dot { width: 5px; height: 5px; border-radius: 50%; }
|
| 132 |
+
|
| 133 |
+
/* === CONNECTION === */
|
| 134 |
+
.connect-row { display: flex; gap: 8px; }
|
| 135 |
+
.btn {
|
| 136 |
+
flex: 1; padding: 10px 16px; border: none; border-radius: 8px;
|
| 137 |
+
font-family: var(--sans); font-size: 12px; font-weight: 600;
|
| 138 |
+
cursor: pointer; transition: all 0.2s;
|
| 139 |
+
}
|
| 140 |
+
.btn:disabled { opacity: 0.35; cursor: not-allowed; }
|
| 141 |
+
.btn-primary {
|
| 142 |
+
background: linear-gradient(135deg, var(--cyan), #3b82f6);
|
| 143 |
+
color: #fff; box-shadow: 0 2px 12px rgba(34,211,238,0.2);
|
| 144 |
+
}
|
| 145 |
+
.btn-primary:hover:not(:disabled) { transform: translateY(-1px); box-shadow: 0 4px 20px rgba(34,211,238,0.3); }
|
| 146 |
+
.btn-secondary { background: var(--bg-raised); color: var(--txt-secondary); border: 1px solid var(--border-faint); }
|
| 147 |
+
.btn-secondary:hover:not(:disabled) { border-color: var(--border-glow); }
|
| 148 |
+
|
| 149 |
+
/* === MODE SELECTOR === */
|
| 150 |
+
.mode-group { display: flex; gap: 4px; background: var(--bg-abyss); border-radius: 8px; padding: 3px; }
|
| 151 |
+
.mode-btn {
|
| 152 |
+
flex: 1; padding: 8px 6px; border: none; border-radius: 6px; cursor: pointer;
|
| 153 |
+
font-family: var(--mono); font-size: 10px; font-weight: 500; letter-spacing: 0.03em;
|
| 154 |
+
background: transparent; color: var(--txt-dim); transition: all 0.2s;
|
| 155 |
+
}
|
| 156 |
+
.mode-btn.active { background: var(--bg-raised); color: var(--cyan); box-shadow: 0 0 12px rgba(34,211,238,0.1); }
|
| 157 |
+
.mode-btn:hover:not(.active) { color: var(--txt-secondary); }
|
| 158 |
+
|
| 159 |
+
/* === NEUROMOD SLIDERS === */
|
| 160 |
+
.neuromod-row { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }
|
| 161 |
+
.neuromod-label { font-family: var(--mono); font-size: 10px; font-weight: 600; width: 28px; text-align: right; }
|
| 162 |
+
.neuromod-slider {
|
| 163 |
+
flex: 1; -webkit-appearance: none; appearance: none;
|
| 164 |
+
height: 4px; border-radius: 2px; outline: none; background: var(--bg-abyss);
|
| 165 |
+
}
|
| 166 |
+
.neuromod-slider::-webkit-slider-thumb {
|
| 167 |
+
-webkit-appearance: none; width: 14px; height: 14px;
|
| 168 |
+
border-radius: 50%; cursor: pointer; border: 2px solid;
|
| 169 |
+
}
|
| 170 |
+
.neuromod-val { font-family: var(--mono); font-size: 10px; width: 32px; color: var(--txt-dim); }
|
| 171 |
+
.nm-da .neuromod-label { color: var(--da-color); }
|
| 172 |
+
.nm-da .neuromod-slider::-webkit-slider-thumb { background: var(--da-color); border-color: var(--da-color); }
|
| 173 |
+
.nm-sht .neuromod-label { color: var(--sht-color); }
|
| 174 |
+
.nm-sht .neuromod-slider::-webkit-slider-thumb { background: var(--sht-color); border-color: var(--sht-color); }
|
| 175 |
+
.nm-ach .neuromod-label { color: var(--ach-color); }
|
| 176 |
+
.nm-ach .neuromod-slider::-webkit-slider-thumb { background: var(--ach-color); border-color: var(--ach-color); }
|
| 177 |
+
.nm-na .neuromod-label { color: var(--na-color); }
|
| 178 |
+
.nm-na .neuromod-slider::-webkit-slider-thumb { background: var(--na-color); border-color: var(--na-color); }
|
| 179 |
+
|
| 180 |
+
/* === METRICS === */
|
| 181 |
+
.metrics-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; }
|
| 182 |
+
.metric-box {
|
| 183 |
+
background: var(--bg-abyss); border-radius: 6px; padding: 8px 10px;
|
| 184 |
+
border: 1px solid var(--border-faint);
|
| 185 |
+
}
|
| 186 |
+
.metric-label { font-size: 9px; color: var(--txt-dim); font-family: var(--mono); text-transform: uppercase; letter-spacing: 0.08em; }
|
| 187 |
+
.metric-value { font-size: 16px; font-weight: 700; margin-top: 2px; }
|
| 188 |
+
.metric-value.exc { color: var(--excite); }
|
| 189 |
+
.metric-value.inh { color: var(--inhibit); }
|
| 190 |
+
.metric-value.neu { color: var(--txt-dim); }
|
| 191 |
+
|
| 192 |
+
/* === D-PAD === */
|
| 193 |
+
.dpad-container { display: flex; justify-content: center; }
|
| 194 |
+
.dpad-grid {
|
| 195 |
+
display: grid; grid-template-columns: 54px 54px 54px; grid-template-rows: 54px 54px 54px;
|
| 196 |
+
gap: 5px;
|
| 197 |
+
}
|
| 198 |
+
.dpad-btn {
|
| 199 |
+
width: 54px; height: 54px; border-radius: 10px; border: 1.5px solid var(--border-glow);
|
| 200 |
+
background: var(--bg-raised); color: var(--txt-dim); font-size: 18px; font-weight: 700;
|
| 201 |
+
display: grid; place-items: center; cursor: pointer; transition: all 0.1s;
|
| 202 |
+
user-select: none; -webkit-user-select: none; font-family: var(--sans);
|
| 203 |
+
}
|
| 204 |
+
.dpad-btn:active, .dpad-btn.active {
|
| 205 |
+
background: rgba(34,211,238,0.15); border-color: var(--cyan); color: var(--cyan);
|
| 206 |
+
box-shadow: 0 0 14px rgba(34,211,238,0.25); transform: scale(0.94);
|
| 207 |
+
}
|
| 208 |
+
.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; }
|
| 209 |
+
.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); }
|
| 210 |
+
.dpad-hidden { visibility: hidden; }
|
| 211 |
+
.dpad-hint { text-align: center; font-size: 9px; color: var(--txt-dim); font-family: var(--mono); margin-top: 8px; }
|
| 212 |
+
|
| 213 |
+
/* === CANVAS === */
|
| 214 |
+
#neuralCanvas {
|
| 215 |
+
width: 100%; height: 100%; display: block;
|
| 216 |
+
background: transparent;
|
| 217 |
+
}
|
| 218 |
+
.canvas-overlay {
|
| 219 |
+
position: absolute; bottom: 12px; left: 12px; right: 12px;
|
| 220 |
+
display: flex; justify-content: space-between; align-items: flex-end;
|
| 221 |
+
pointer-events: none;
|
| 222 |
+
}
|
| 223 |
+
.overlay-chip {
|
| 224 |
+
font-family: var(--mono); font-size: 9px; letter-spacing: 0.05em;
|
| 225 |
+
padding: 4px 10px; border-radius: 4px;
|
| 226 |
+
background: rgba(3,7,9,0.8); color: var(--txt-dim);
|
| 227 |
+
border: 1px solid var(--border-faint); backdrop-filter: blur(8px);
|
| 228 |
+
}
|
| 229 |
+
.overlay-chip span { color: var(--cyan); }
|
| 230 |
+
|
| 231 |
+
/* === FOCUSED NODE HUD === */
|
| 232 |
+
.focus-hud {
|
| 233 |
+
position: absolute; top: 14px; left: 50%; transform: translateX(-50%);
|
| 234 |
+
pointer-events: none; z-index: 10;
|
| 235 |
+
background: rgba(3,7,9,0.88); backdrop-filter: blur(16px) saturate(1.4);
|
| 236 |
+
border: 1px solid rgba(251,191,36,0.3); border-radius: 10px;
|
| 237 |
+
padding: 10px 18px; min-width: 220px;
|
| 238 |
+
font-family: var(--mono); font-size: 10px;
|
| 239 |
+
display: flex; gap: 14px; align-items: center;
|
| 240 |
+
opacity: 0; transition: opacity 0.25s ease;
|
| 241 |
+
box-shadow: 0 4px 30px rgba(0,0,0,0.5), 0 0 20px rgba(251,191,36,0.05);
|
| 242 |
+
}
|
| 243 |
+
.focus-hud.visible { opacity: 1; }
|
| 244 |
+
.focus-hud .node-id { font-size: 16px; font-weight: 800; min-width: 30px; text-align: center; }
|
| 245 |
+
.focus-hud .node-meta { display: flex; flex-direction: column; gap: 2px; }
|
| 246 |
+
.focus-hud .meta-row { display: flex; gap: 8px; }
|
| 247 |
+
.focus-hud .meta-label { color: var(--txt-dim); font-size: 9px; width: 50px; }
|
| 248 |
+
.focus-hud .meta-val { font-weight: 600; font-size: 10px; }
|
| 249 |
+
|
| 250 |
+
/* === INTERACTION HINT === */
|
| 251 |
+
.pan-hint {
|
| 252 |
+
position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
|
| 253 |
+
pointer-events: none; z-index: 5;
|
| 254 |
+
font-family: var(--mono); font-size: 11px; color: rgba(224,242,254,0.25);
|
| 255 |
+
letter-spacing: 0.05em; text-align: center;
|
| 256 |
+
transition: opacity 1.2s ease;
|
| 257 |
+
}
|
| 258 |
+
.pan-hint.hidden { opacity: 0; }
|
| 259 |
+
|
| 260 |
+
/* === LEGEND === */
|
| 261 |
+
.legend-row { display: flex; gap: 14px; justify-content: center; flex-wrap: wrap; }
|
| 262 |
+
.legend-item { display: flex; align-items: center; gap: 5px; font-size: 9px; font-family: var(--mono); color: var(--txt-dim); }
|
| 263 |
+
.legend-dot { width: 8px; height: 8px; border-radius: 50%; }
|
| 264 |
+
.legend-dot.exc { background: var(--excite); box-shadow: 0 0 6px var(--excite); }
|
| 265 |
+
.legend-dot.inh { background: var(--inhibit); box-shadow: 0 0 6px var(--inhibit); }
|
| 266 |
+
.legend-dot.neu { background: var(--neutral); }
|
| 267 |
+
|
| 268 |
+
/* === LOG === */
|
| 269 |
+
.log-panel {
|
| 270 |
+
font-family: var(--mono); font-size: 10px; line-height: 1.6;
|
| 271 |
+
overflow-y: auto; padding: 10px; max-height: 160px;
|
| 272 |
+
background: rgba(0,0,0,0.4); border: 1px solid var(--border-faint);
|
| 273 |
+
border-radius: 8px; color: var(--txt-dim); white-space: pre-wrap;
|
| 274 |
+
}
|
| 275 |
+
.log-panel::-webkit-scrollbar { width: 4px; }
|
| 276 |
+
.log-panel::-webkit-scrollbar-thumb { background: var(--border-faint); border-radius: 2px; }
|
| 277 |
+
|
| 278 |
+
/* === MOTOR OUTPUT === */
|
| 279 |
+
.motor-display { display: flex; gap: 10px; align-items: center; justify-content: center; }
|
| 280 |
+
.motor-dial {
|
| 281 |
+
width: 80px; height: 80px; border-radius: 50%;
|
| 282 |
+
border: 2px solid var(--border-glow); background: var(--bg-abyss);
|
| 283 |
+
position: relative; display: flex; align-items: center; justify-content: center;
|
| 284 |
+
}
|
| 285 |
+
.motor-dial .needle {
|
| 286 |
+
width: 2px; height: 30px; background: var(--cyan);
|
| 287 |
+
position: absolute; bottom: 50%; left: calc(50% - 1px);
|
| 288 |
+
transform-origin: bottom center; transition: transform 0.15s;
|
| 289 |
+
border-radius: 1px; box-shadow: 0 0 6px var(--cyan);
|
| 290 |
+
}
|
| 291 |
+
.motor-dial .center-dot {
|
| 292 |
+
width: 8px; height: 8px; border-radius: 50%;
|
| 293 |
+
background: var(--cyan); position: absolute;
|
| 294 |
+
box-shadow: 0 0 10px var(--cyan);
|
| 295 |
+
}
|
| 296 |
+
.motor-label { font-family: var(--mono); font-size: 9px; color: var(--txt-dim); text-align: center; margin-top: 4px; }
|
| 297 |
+
.motor-val { font-family: var(--mono); font-size: 14px; font-weight: 700; color: var(--cyan); text-align: center; }
|
| 298 |
+
|
| 299 |
+
/* === ACTIVITY BAR === */
|
| 300 |
+
.activity-bar {
|
| 301 |
+
height: 40px; background: var(--bg-abyss); border-radius: 6px;
|
| 302 |
+
border: 1px solid var(--border-faint); overflow: hidden;
|
| 303 |
+
display: flex; position: relative;
|
| 304 |
+
}
|
| 305 |
+
.activity-bar canvas { width: 100%; height: 100%; }
|
| 306 |
+
|
| 307 |
+
/* === SCROLLBAR === */
|
| 308 |
+
.panel-left::-webkit-scrollbar, .panel-right::-webkit-scrollbar { width: 4px; }
|
| 309 |
+
.panel-left::-webkit-scrollbar-thumb, .panel-right::-webkit-scrollbar-thumb { background: var(--border-faint); border-radius: 2px; }
|
| 310 |
+
|
| 311 |
+
.controls-disabled { opacity: 0.3; pointer-events: none; }
|
| 312 |
+
|
| 313 |
+
/* === LEARNING PROGRESS BAR === */
|
| 314 |
+
.progress-track {
|
| 315 |
+
width: 100%; height: 18px; background: var(--bg-abyss);
|
| 316 |
+
border-radius: 9px; border: 1px solid var(--border-faint);
|
| 317 |
+
overflow: hidden; position: relative;
|
| 318 |
+
}
|
| 319 |
+
.progress-fill {
|
| 320 |
+
height: 100%; border-radius: 9px; transition: width 0.4s ease;
|
| 321 |
+
background: linear-gradient(90deg, rgba(251,191,36,0.4), rgba(52,211,153,0.8));
|
| 322 |
+
position: relative;
|
| 323 |
+
}
|
| 324 |
+
.progress-fill.learned {
|
| 325 |
+
background: linear-gradient(90deg, rgba(52,211,153,0.6), rgba(34,211,238,0.9));
|
| 326 |
+
}
|
| 327 |
+
.progress-text {
|
| 328 |
+
position: absolute; top: 0; left: 0; right: 0; bottom: 0;
|
| 329 |
+
display: flex; align-items: center; justify-content: center;
|
| 330 |
+
font-family: var(--mono); font-size: 9px; font-weight: 600;
|
| 331 |
+
color: var(--txt-primary); text-shadow: 0 1px 3px rgba(0,0,0,0.6);
|
| 332 |
+
pointer-events: none; z-index: 1;
|
| 333 |
+
}
|
| 334 |
+
.nxon-source-tag {
|
| 335 |
+
font-family: var(--mono); font-size: 9px; font-weight: 600;
|
| 336 |
+
padding: 3px 8px; border-radius: 4px; letter-spacing: 0.04em;
|
| 337 |
+
}
|
| 338 |
+
.nxon-source-tag.human { background: rgba(52,211,153,0.15); color: var(--green); }
|
| 339 |
+
.nxon-source-tag.nxon { background: rgba(164,139,250,0.15); color: var(--purple); }
|
| 340 |
+
.nxon-source-tag.learning { background: rgba(251,191,36,0.15); color: var(--amber); }
|
| 341 |
+
</style>
|
| 342 |
+
</head>
|
| 343 |
+
<body>
|
| 344 |
+
|
| 345 |
+
<header>
|
| 346 |
+
<div class="logo-area">
|
| 347 |
+
<div>
|
| 348 |
+
<h1> <a href="https://github.com/DavidVivancos/Neuraxon"> Neuraxon 2.0</a> <a href="https://sphero.com/collections/mini"> Sphero Mini<a> Control</h1>
|
| 349 |
+
<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>
|
| 350 |
+
</div>
|
| 351 |
+
</div>
|
| 352 |
+
<div class="header-right">
|
| 353 |
+
<div class="mode-indicator" id="modeLabel">MANUAL</div>
|
| 354 |
+
<div class="status-badge off" id="connStatus">OFFLINE</div>
|
| 355 |
+
</div>
|
| 356 |
+
</header>
|
| 357 |
+
|
| 358 |
+
<div class="app-grid">
|
| 359 |
+
|
| 360 |
+
<!-- =================== LEFT PANEL =================== -->
|
| 361 |
+
<div class="panel-left">
|
| 362 |
+
|
| 363 |
+
<div class="card">
|
| 364 |
+
<div class="card-title"><div class="dot" style="background:var(--cyan)"></div> SPHERO CONNECTION</div>
|
| 365 |
+
<div class="connect-row">
|
| 366 |
+
<button class="btn btn-primary" id="btnConnect">Connect</button>
|
| 367 |
+
<button class="btn btn-secondary" id="btnDisconnect" disabled>Disconnect</button>
|
| 368 |
+
</div>
|
| 369 |
+
</div>
|
| 370 |
+
|
| 371 |
+
<div class="card">
|
| 372 |
+
<div class="card-title"><div class="dot" style="background:var(--purple)"></div> CONTROL MODE</div>
|
| 373 |
+
<div class="mode-group">
|
| 374 |
+
<button class="mode-btn active" data-mode="manual" onclick="setMode('manual')">Manual</button>
|
| 375 |
+
<button class="mode-btn" data-mode="hybrid" onclick="setMode('hybrid')">Hybrid</button>
|
| 376 |
+
</div>
|
| 377 |
+
</div>
|
| 378 |
+
|
| 379 |
+
<div class="card" id="driveCard">
|
| 380 |
+
<div class="card-title"><div class="dot" style="background:var(--green)"></div> HUMAN DRIVE</div>
|
| 381 |
+
<div class="dpad-container">
|
| 382 |
+
<div class="dpad-grid">
|
| 383 |
+
<div class="dpad-hidden"></div>
|
| 384 |
+
<div class="dpad-btn" id="btnUp">W</div>
|
| 385 |
+
<div class="dpad-hidden"></div>
|
| 386 |
+
<div class="dpad-btn" id="btnLeft">A</div>
|
| 387 |
+
<div class="dpad-btn stop-btn" id="btnBrake">STOP</div>
|
| 388 |
+
<div class="dpad-btn" id="btnRight">D</div>
|
| 389 |
+
<div class="dpad-hidden"></div>
|
| 390 |
+
<div class="dpad-btn" id="btnDown">S</div>
|
| 391 |
+
<div class="dpad-hidden"></div>
|
| 392 |
+
</div>
|
| 393 |
+
</div>
|
| 394 |
+
<div class="dpad-hint">WASD / Arrows / Space</div>
|
| 395 |
+
</div>
|
| 396 |
+
|
| 397 |
+
<div class="card">
|
| 398 |
+
<div class="card-title"><div class="dot" style="background:var(--amber)"></div> MOTOR OUTPUT</div>
|
| 399 |
+
<div class="motor-display">
|
| 400 |
+
<div>
|
| 401 |
+
<div class="motor-dial" id="headingDial">
|
| 402 |
+
<div class="needle" id="headingNeedle"></div>
|
| 403 |
+
<div class="center-dot"></div>
|
| 404 |
+
</div>
|
| 405 |
+
<div class="motor-label">HEADING</div>
|
| 406 |
+
<div class="motor-val" id="headingVal">0Β°</div>
|
| 407 |
+
</div>
|
| 408 |
+
<div style="text-align:center;">
|
| 409 |
+
<div class="metric-box" style="width:80px;padding:12px;">
|
| 410 |
+
<div class="metric-label">SPEED</div>
|
| 411 |
+
<div class="metric-value exc" id="speedVal" style="font-size:22px;">0</div>
|
| 412 |
+
</div>
|
| 413 |
+
<div style="margin-top:6px;">
|
| 414 |
+
<div class="metric-label" style="font-size:8px;">BLEND</div>
|
| 415 |
+
<div class="motor-val" id="blendVal" style="font-size:11px;color:var(--purple);">H:100% N:0%</div>
|
| 416 |
+
</div>
|
| 417 |
+
</div>
|
| 418 |
+
</div>
|
| 419 |
+
</div>
|
| 420 |
+
|
| 421 |
+
<div class="card" style="flex:1;min-height:100px;display:flex;flex-direction:column;">
|
| 422 |
+
<div class="card-title"><div class="dot" style="background:var(--txt-dim)"></div> SYSTEM LOG</div>
|
| 423 |
+
<div class="log-panel" id="logPanel" style="flex:1;max-height:none;"></div>
|
| 424 |
+
</div>
|
| 425 |
+
|
| 426 |
+
</div>
|
| 427 |
+
|
| 428 |
+
<!-- =================== CENTER - NEURAL VIS =================== -->
|
| 429 |
+
<div class="panel-center" id="panelCenter">
|
| 430 |
+
<canvas id="neuralCanvas"></canvas>
|
| 431 |
+
|
| 432 |
+
<!-- Focus HUD (shows details of nearest node) -->
|
| 433 |
+
<div class="focus-hud" id="focusHud">
|
| 434 |
+
<div class="node-id" id="focusNodeId">β</div>
|
| 435 |
+
<div class="node-meta">
|
| 436 |
+
<div class="meta-row"><span class="meta-label">TYPE</span><span class="meta-val" id="focusType">β</span></div>
|
| 437 |
+
<div class="meta-row"><span class="meta-label">STATE</span><span class="meta-val" id="focusState">β</span></div>
|
| 438 |
+
<div class="meta-row"><span class="meta-label">s(t)</span><span class="meta-val" id="focusS">β</span></div>
|
| 439 |
+
<div class="meta-row"><span class="meta-label">ADAPT</span><span class="meta-val" id="focusAdapt">β</span></div>
|
| 440 |
+
<div class="meta-row"><span class="meta-label">HEALTH</span><span class="meta-val" id="focusHealth">β</span></div>
|
| 441 |
+
<div class="meta-row"><span class="meta-label">SYNAPSES</span><span class="meta-val" id="focusSyn">β</span></div>
|
| 442 |
+
</div>
|
| 443 |
+
</div>
|
| 444 |
+
|
| 445 |
+
<!-- Interaction hint -->
|
| 446 |
+
<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>
|
| 447 |
+
|
| 448 |
+
<div class="canvas-overlay">
|
| 449 |
+
<div class="overlay-chip">STEP <span id="stepCount">0</span></div>
|
| 450 |
+
<div class="legend-row">
|
| 451 |
+
<div class="legend-item"><div class="legend-dot exc"></div>Excite (+1)</div>
|
| 452 |
+
<div class="legend-item"><div class="legend-dot neu"></div>Neutral (0)</div>
|
| 453 |
+
<div class="legend-item"><div class="legend-dot inh"></div>Inhibit (-1)</div>
|
| 454 |
+
<div class="legend-item" style="opacity:0.6;">β Input</div>
|
| 455 |
+
<div class="legend-item" style="opacity:0.6;">β Hidden</div>
|
| 456 |
+
<div class="legend-item" style="opacity:0.6;">β‘ Output</div>
|
| 457 |
+
</div>
|
| 458 |
+
<div class="overlay-chip">ENERGY <span id="energyVal">0.00</span></div>
|
| 459 |
+
</div>
|
| 460 |
+
</div>
|
| 461 |
+
|
| 462 |
+
<!-- =================== RIGHT PANEL =================== -->
|
| 463 |
+
<div class="panel-right">
|
| 464 |
+
|
| 465 |
+
<div class="card">
|
| 466 |
+
<div class="card-title"><div class="dot" style="background:var(--da-color)"></div> NEUROMODULATORS</div>
|
| 467 |
+
<div class="neuromod-row nm-da">
|
| 468 |
+
<span class="neuromod-label">DA</span>
|
| 469 |
+
<input type="range" class="neuromod-slider" id="sliderDA" min="0" max="100" value="50">
|
| 470 |
+
<span class="neuromod-val" id="valDA">0.50</span>
|
| 471 |
+
</div>
|
| 472 |
+
<div class="neuromod-row nm-sht">
|
| 473 |
+
<span class="neuromod-label">5HT</span>
|
| 474 |
+
<input type="range" class="neuromod-slider" id="slider5HT" min="0" max="100" value="50">
|
| 475 |
+
<span class="neuromod-val" id="val5HT">0.50</span>
|
| 476 |
+
</div>
|
| 477 |
+
<div class="neuromod-row nm-ach">
|
| 478 |
+
<span class="neuromod-label">ACh</span>
|
| 479 |
+
<input type="range" class="neuromod-slider" id="sliderACh" min="0" max="100" value="50">
|
| 480 |
+
<span class="neuromod-val" id="valACh">0.50</span>
|
| 481 |
+
</div>
|
| 482 |
+
<div class="neuromod-row nm-na">
|
| 483 |
+
<span class="neuromod-label">NA</span>
|
| 484 |
+
<input type="range" class="neuromod-slider" id="sliderNA" min="0" max="100" value="50">
|
| 485 |
+
<span class="neuromod-val" id="valNA">0.50</span>
|
| 486 |
+
</div>
|
| 487 |
+
</div>
|
| 488 |
+
|
| 489 |
+
<div class="card">
|
| 490 |
+
<div class="card-title"><div class="dot" style="background:var(--cyan)"></div> NETWORK STATS</div>
|
| 491 |
+
<div class="metrics-grid">
|
| 492 |
+
<div class="metric-box">
|
| 493 |
+
<div class="metric-label">Excitatory</div>
|
| 494 |
+
<div class="metric-value exc" id="statExc">0</div>
|
| 495 |
+
</div>
|
| 496 |
+
<div class="metric-box">
|
| 497 |
+
<div class="metric-label">Inhibitory</div>
|
| 498 |
+
<div class="metric-value inh" id="statInh">0</div>
|
| 499 |
+
</div>
|
| 500 |
+
<div class="metric-box">
|
| 501 |
+
<div class="metric-label">Neutral</div>
|
| 502 |
+
<div class="metric-value neu" id="statNeu">0</div>
|
| 503 |
+
</div>
|
| 504 |
+
<div class="metric-box">
|
| 505 |
+
<div class="metric-label">Synapses</div>
|
| 506 |
+
<div class="metric-value" style="color:var(--blue)" id="statSyn">0</div>
|
| 507 |
+
</div>
|
| 508 |
+
</div>
|
| 509 |
+
</div>
|
| 510 |
+
|
| 511 |
+
<div class="card">
|
| 512 |
+
<div class="card-title"><div class="dot" style="background:var(--sht-color)"></div> OSCILLATOR BANK</div>
|
| 513 |
+
<canvas id="oscCanvas" width="228" height="90" style="width:100%;height:90px;border-radius:6px;background:var(--bg-abyss);"></canvas>
|
| 514 |
+
</div>
|
| 515 |
+
|
| 516 |
+
<div class="card">
|
| 517 |
+
<div class="card-title"><div class="dot" style="background:var(--green)"></div> ACTIVITY TRACE</div>
|
| 518 |
+
<div class="activity-bar">
|
| 519 |
+
<canvas id="activityCanvas" width="228" height="40" style="width:100%;height:40px;"></canvas>
|
| 520 |
+
</div>
|
| 521 |
+
</div>
|
| 522 |
+
|
| 523 |
+
<div class="card">
|
| 524 |
+
<div class="card-title"><div class="dot" style="background:var(--pink)"></div> RECEPTOR ACTIVATIONS</div>
|
| 525 |
+
<div id="receptorBars" style="display:flex;flex-direction:column;gap:4px;"></div>
|
| 526 |
+
</div>
|
| 527 |
+
|
| 528 |
+
<div class="card">
|
| 529 |
+
<div class="card-title"><div class="dot" style="background:var(--amber)"></div> LEARNING PROGRESS</div>
|
| 530 |
+
<div style="margin-bottom:8px;">
|
| 531 |
+
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:4px;">
|
| 532 |
+
<span style="font-family:var(--mono);font-size:9px;color:var(--txt-dim);">ACCURACY</span>
|
| 533 |
+
<span style="font-family:var(--mono);font-size:11px;font-weight:700;color:var(--green);" id="learnPct">0%</span>
|
| 534 |
+
</div>
|
| 535 |
+
<div class="progress-track">
|
| 536 |
+
<div class="progress-fill" id="learnBar" style="width:0%;"></div>
|
| 537 |
+
<div class="progress-text" id="learnBarText">Drive in MANUAL to teach</div>
|
| 538 |
+
</div>
|
| 539 |
+
</div>
|
| 540 |
+
<div class="metrics-grid">
|
| 541 |
+
<div class="metric-box">
|
| 542 |
+
<div class="metric-label">Teach Steps</div>
|
| 543 |
+
<div class="metric-value" style="color:var(--da-color);font-size:13px;" id="statLearn">0</div>
|
| 544 |
+
</div>
|
| 545 |
+
<div class="metric-box">
|
| 546 |
+
<div class="metric-label">Mean |Ξw|</div>
|
| 547 |
+
<div class="metric-value" style="color:var(--amber);font-size:13px;" id="statDw">0.000</div>
|
| 548 |
+
</div>
|
| 549 |
+
<div class="metric-box">
|
| 550 |
+
<div class="metric-label">Status</div>
|
| 551 |
+
<div class="metric-value" style="color:var(--green);font-size:11px;" id="statStruct">stable</div>
|
| 552 |
+
</div>
|
| 553 |
+
<div class="metric-box">
|
| 554 |
+
<div class="metric-label">Out Accum</div>
|
| 555 |
+
<div class="metric-value" style="color:var(--cyan);font-size:10px;font-family:var(--mono);" id="statAccum">0 0 0 0</div>
|
| 556 |
+
</div>
|
| 557 |
+
</div>
|
| 558 |
+
</div>
|
| 559 |
+
|
| 560 |
+
<div class="card">
|
| 561 |
+
<div class="card-title"><div class="dot" style="background:var(--purple)"></div> MOTOR SOURCE</div>
|
| 562 |
+
<div style="display:flex;gap:6px;justify-content:center;flex-wrap:wrap;" id="sourceTagArea">
|
| 563 |
+
<span class="nxon-source-tag human" id="tagHuman">HUMAN</span>
|
| 564 |
+
<span class="nxon-source-tag nxon" id="tagNxon" style="opacity:0.3;">NEURAXON</span>
|
| 565 |
+
<span class="nxon-source-tag learning" id="tagLearn" style="opacity:0.3;">LEARNING</span>
|
| 566 |
+
</div>
|
| 567 |
+
<div style="margin-top:8px;text-align:center;">
|
| 568 |
+
<span style="font-family:var(--mono);font-size:10px;color:var(--txt-dim);" id="sourceDetail">Manual control active</span>
|
| 569 |
+
</div>
|
| 570 |
+
</div>
|
| 571 |
+
|
| 572 |
+
</div>
|
| 573 |
+
</div>
|
| 574 |
+
|
| 575 |
+
<script>
|
| 576 |
+
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 577 |
+
// SPHERO MINI BLE PROTOCOL
|
| 578 |
+
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 579 |
+
|
| 580 |
+
const UUID_SPHERO_SERVICE = '00010001-574f-4f20-5370-6865726f2121';
|
| 581 |
+
const UUID_SPHERO_SERVICE_INIT = '00020001-574f-4f20-5370-6865726f2121';
|
| 582 |
+
const UUID_CHAR_HANDLE = '00010002-574f-4f20-5370-6865726f2121';
|
| 583 |
+
const UUID_CHAR_USETHEFORCE = '00020005-574f-4f20-5370-6865726f2121';
|
| 584 |
+
const UseTheForceBytes = new Uint8Array([0x75,0x73,0x65,0x74,0x68,0x65,0x66,0x6f,0x72,0x63,0x65,0x2e,0x2e,0x2e,0x62,0x61,0x6e,0x64]);
|
| 585 |
+
const API = { ESC: 0xAB, SOP: 0x8D, EOP: 0xD8, ESC_MASK: 0x88 };
|
| 586 |
+
const DeviceId = { powerInfo: 0x13, driving: 0x16, userIO: 0x1A };
|
| 587 |
+
const PowerCmd = { wake: 0x0D, sleep: 0x01 };
|
| 588 |
+
const DrivingCmd = { resetYaw: 0x06, driveWithHeading: 0x07 };
|
| 589 |
+
const UserIOCmd = { allLEDs: 0x0E };
|
| 590 |
+
const Flags = { requestsResponse: 2, requestsOnlyErrorResponse: 4, resetsInactivityTimeout: 8 };
|
| 591 |
+
|
| 592 |
+
class SpheroMiniBLE {
|
| 593 |
+
constructor() {
|
| 594 |
+
this.device = null; this.server = null; this.ch = new Map(); this.seq = 0;
|
| 595 |
+
this._chain = Promise.resolve(); this._qDepth = 0; this._closed = false;
|
| 596 |
+
this.onDisconnect = null;
|
| 597 |
+
}
|
| 598 |
+
_pushEscaped(out, b) {
|
| 599 |
+
if (b === API.SOP || b === API.EOP || b === API.ESC) { out.push(API.ESC); out.push(b & (~API.ESC_MASK)); }
|
| 600 |
+
else out.push(b);
|
| 601 |
+
}
|
| 602 |
+
_buildPacket(did, cid, dataBytes, cmdFlags) {
|
| 603 |
+
this.seq = (this.seq + 1) & 0xFF; let sum = 0; const out = [];
|
| 604 |
+
out.push(API.SOP); out.push(cmdFlags); sum += cmdFlags;
|
| 605 |
+
this._pushEscaped(out, did); sum += did;
|
| 606 |
+
this._pushEscaped(out, cid); sum += cid;
|
| 607 |
+
this._pushEscaped(out, this.seq); sum += this.seq;
|
| 608 |
+
for (const b of dataBytes) { this._pushEscaped(out, b); sum += b; }
|
| 609 |
+
const chk = (~sum) & 0xFF; this._pushEscaped(out, chk); out.push(API.EOP);
|
| 610 |
+
return new Uint8Array(out);
|
| 611 |
+
}
|
| 612 |
+
_enqueueWrite(fn) {
|
| 613 |
+
if (this._closed) return Promise.reject(new Error('BLE closed'));
|
| 614 |
+
this._qDepth++;
|
| 615 |
+
const run = async () => { try { return await fn(); } finally { this._qDepth = Math.max(0, this._qDepth - 1); } };
|
| 616 |
+
this._chain = this._chain.then(run, run); return this._chain;
|
| 617 |
+
}
|
| 618 |
+
async connect() {
|
| 619 |
+
sysLog('Requesting Bluetooth device...');
|
| 620 |
+
this.device = await navigator.bluetooth.requestDevice({
|
| 621 |
+
filters: [{ services: [UUID_SPHERO_SERVICE] }],
|
| 622 |
+
optionalServices: [UUID_SPHERO_SERVICE_INIT]
|
| 623 |
+
});
|
| 624 |
+
this.device.addEventListener('gattserverdisconnected', () => {
|
| 625 |
+
sysLog('BLE disconnected');
|
| 626 |
+
if (this.onDisconnect) this.onDisconnect();
|
| 627 |
+
});
|
| 628 |
+
sysLog(`Connecting to: ${this.device.name || 'Sphero Mini'}`);
|
| 629 |
+
this.server = await this.device.gatt.connect();
|
| 630 |
+
const svcCmd = await this.server.getPrimaryService(UUID_SPHERO_SERVICE);
|
| 631 |
+
const svcInit = await this.server.getPrimaryService(UUID_SPHERO_SERVICE_INIT);
|
| 632 |
+
const chHandle = await svcCmd.getCharacteristic(UUID_CHAR_HANDLE);
|
| 633 |
+
const chForce = await svcInit.getCharacteristic(UUID_CHAR_USETHEFORCE);
|
| 634 |
+
this.ch.set('handle', chHandle);
|
| 635 |
+
sysLog('Waking Sphero...');
|
| 636 |
+
await this._enqueueWrite(() => chForce.writeValue(UseTheForceBytes));
|
| 637 |
+
await this.send(DeviceId.powerInfo, PowerCmd.wake, [], { response: 'full' });
|
| 638 |
+
await sleep(200);
|
| 639 |
+
await this.send(DeviceId.powerInfo, PowerCmd.wake, [], { response: 'full' });
|
| 640 |
+
}
|
| 641 |
+
async disconnect() {
|
| 642 |
+
this._closed = true;
|
| 643 |
+
if (this.device && this.device.gatt.connected) this.device.gatt.disconnect();
|
| 644 |
+
}
|
| 645 |
+
async send(did, cid, data, opts = { response: 'errorOnly' }) {
|
| 646 |
+
const chHandle = this.ch.get('handle'); if (!chHandle) throw new Error('Handle missing');
|
| 647 |
+
let cmdFlags = Flags.resetsInactivityTimeout;
|
| 648 |
+
if (opts.response === 'full') cmdFlags |= Flags.requestsResponse;
|
| 649 |
+
else if (opts.response === 'errorOnly') cmdFlags |= Flags.requestsOnlyErrorResponse;
|
| 650 |
+
const pkt = this._buildPacket(did, cid, data, cmdFlags);
|
| 651 |
+
return this._enqueueWrite(async () => {
|
| 652 |
+
if (chHandle.writeValueWithoutResponse && opts.response !== 'full') await chHandle.writeValueWithoutResponse(pkt);
|
| 653 |
+
else await chHandle.writeValue(pkt);
|
| 654 |
+
return this.seq;
|
| 655 |
+
});
|
| 656 |
+
}
|
| 657 |
+
async setMainLED(r, g, b) { return this.send(DeviceId.userIO, UserIOCmd.allLEDs, [0x00, 0x70, r & 255, g & 255, b & 255], { response: 'none' }); }
|
| 658 |
+
async roll(speed, headingDeg) {
|
| 659 |
+
const head = ((headingDeg % 360) + 360) % 360;
|
| 660 |
+
return this.send(DeviceId.driving, DrivingCmd.driveWithHeading, [speed & 255, (head >> 8) & 0xFF, head & 0xFF, 0x01], { response: 'none' });
|
| 661 |
+
}
|
| 662 |
+
async resetYaw() { return this.send(DeviceId.driving, DrivingCmd.resetYaw, [], { response: 'full' }); }
|
| 663 |
+
async stop() { return this.roll(0, 0); }
|
| 664 |
+
}
|
| 665 |
+
|
| 666 |
+
|
| 667 |
+
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 668 |
+
// NEURAXON 2.0 β JavaScript Implementation
|
| 669 |
+
// Based on the architecture by David Vivancos & Jose Sanchez
|
| 670 |
+
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 671 |
+
|
| 672 |
+
class ReceptorSubtype {
|
| 673 |
+
constructor(name, threshold, gain, isTonic) {
|
| 674 |
+
this.name = name; this.threshold = threshold;
|
| 675 |
+
this.gain = gain; this.isTonic = isTonic; this.activation = 0;
|
| 676 |
+
}
|
| 677 |
+
computeActivation(concentration) {
|
| 678 |
+
const k = this.isTonic ? 20 : 10;
|
| 679 |
+
this.activation = this.gain / (1 + Math.exp(-k * (concentration - this.threshold)));
|
| 680 |
+
return this.activation;
|
| 681 |
+
}
|
| 682 |
+
}
|
| 683 |
+
|
| 684 |
+
class OscillatorBank {
|
| 685 |
+
constructor() {
|
| 686 |
+
this.bands = [
|
| 687 |
+
{ name: 'infraslow', freq: 0.05, phase: Math.random() * Math.PI * 2, amplitude: 0.15 },
|
| 688 |
+
{ name: 'slow', freq: 0.5, phase: Math.random() * Math.PI * 2, amplitude: 0.2 },
|
| 689 |
+
{ name: 'theta', freq: 6, phase: Math.random() * Math.PI * 2, amplitude: 0.3 },
|
| 690 |
+
{ name: 'alpha', freq: 10, phase: Math.random() * Math.PI * 2, amplitude: 0.15 },
|
| 691 |
+
{ name: 'gamma', freq: 40, phase: Math.random() * Math.PI * 2, amplitude: 0.25 },
|
| 692 |
+
];
|
| 693 |
+
this.coupling = 0.3;
|
| 694 |
+
}
|
| 695 |
+
update(dt) {
|
| 696 |
+
for (const b of this.bands) {
|
| 697 |
+
b.phase += 2 * Math.PI * b.freq * dt / 1000;
|
| 698 |
+
b.phase %= (2 * Math.PI);
|
| 699 |
+
}
|
| 700 |
+
}
|
| 701 |
+
getDrive(neuronId, N) {
|
| 702 |
+
const phi = 2 * Math.PI * neuronId / N;
|
| 703 |
+
const theta = this.bands[2];
|
| 704 |
+
const gamma = this.bands[4];
|
| 705 |
+
const slow = this.bands[1];
|
| 706 |
+
const infra = this.bands[0];
|
| 707 |
+
const gateTheta = Math.max(0, Math.cos(theta.phase + phi));
|
| 708 |
+
const gammaSig = gamma.amplitude * gateTheta * Math.sin(gamma.phase + 2 * phi);
|
| 709 |
+
const slowSig = slow.amplitude * Math.sin(slow.phase + 0.3 * phi);
|
| 710 |
+
const infraSig = infra.amplitude * Math.sin(infra.phase);
|
| 711 |
+
return this.coupling * (gammaSig + 0.5 * slowSig + 0.3 * infraSig);
|
| 712 |
+
}
|
| 713 |
+
}
|
| 714 |
+
|
| 715 |
+
class NeuromodulatorSystem {
|
| 716 |
+
constructor() {
|
| 717 |
+
this.modulators = {
|
| 718 |
+
DA: { tonic: 0.5, phasic: 0, tauTonic: 5000, tauPhasic: 200, releaseRate: 0.3 },
|
| 719 |
+
SHT: { tonic: 0.5, phasic: 0, tauTonic: 8000, tauPhasic: 500, releaseRate: 0.2 },
|
| 720 |
+
ACh: { tonic: 0.5, phasic: 0, tauTonic: 4000, tauPhasic: 150, releaseRate: 0.25 },
|
| 721 |
+
NA: { tonic: 0.5, phasic: 0, tauTonic: 6000, tauPhasic: 300, releaseRate: 0.2 },
|
| 722 |
+
};
|
| 723 |
+
this.receptors = {
|
| 724 |
+
D1: new ReceptorSubtype('D1', 0.4, 1.0, false),
|
| 725 |
+
D2: new ReceptorSubtype('D2', 0.6, 0.8, true),
|
| 726 |
+
SHT1A: new ReceptorSubtype('5HT1A', 0.3, 1.0, true),
|
| 727 |
+
SHT2A: new ReceptorSubtype('5HT2A', 0.5, 0.9, false),
|
| 728 |
+
SHT4: new ReceptorSubtype('5HT4', 0.4, 0.7, false),
|
| 729 |
+
M1: new ReceptorSubtype('M1', 0.35, 1.0, false),
|
| 730 |
+
M2: new ReceptorSubtype('M2', 0.5, 0.6, true),
|
| 731 |
+
B1: new ReceptorSubtype('Ξ²1', 0.4, 0.9, false),
|
| 732 |
+
A2: new ReceptorSubtype('Ξ±2', 0.3, 0.7, true),
|
| 733 |
+
};
|
| 734 |
+
this.externalOverrides = { DA: null, SHT: null, ACh: null, NA: null };
|
| 735 |
+
}
|
| 736 |
+
update(activity, dt) {
|
| 737 |
+
const { excFrac, meanAct, changeRate } = activity;
|
| 738 |
+
for (const [key, m] of Object.entries(this.modulators)) {
|
| 739 |
+
m.tonic += (0.5 - m.tonic) * dt / m.tauTonic;
|
| 740 |
+
m.phasic *= Math.exp(-dt / m.tauPhasic);
|
| 741 |
+
}
|
| 742 |
+
this.modulators.DA.phasic += this.modulators.DA.releaseRate * changeRate * dt / 1000;
|
| 743 |
+
this.modulators.SHT.tonic += this.modulators.SHT.releaseRate * meanAct * dt / 5000;
|
| 744 |
+
this.modulators.ACh.phasic += this.modulators.ACh.releaseRate * excFrac * dt / 1000;
|
| 745 |
+
this.modulators.NA.phasic += this.modulators.NA.releaseRate * changeRate * dt / 1000;
|
| 746 |
+
this.modulators.ACh.phasic *= (1 - 0.1 * this.modulators.DA.phasic);
|
| 747 |
+
this.modulators.SHT.tonic += 0.02 * (this.modulators.NA.tonic + this.modulators.NA.phasic) * dt / 1000;
|
| 748 |
+
for (const m of Object.values(this.modulators)) {
|
| 749 |
+
m.tonic = clamp(m.tonic, 0, 1);
|
| 750 |
+
m.phasic = clamp(m.phasic, 0, 1);
|
| 751 |
+
}
|
| 752 |
+
for (const [key, val] of Object.entries(this.externalOverrides)) {
|
| 753 |
+
if (val !== null) this.modulators[key].tonic = val;
|
| 754 |
+
}
|
| 755 |
+
}
|
| 756 |
+
computeReceptorActivations() {
|
| 757 |
+
const R = {};
|
| 758 |
+
const getConc = (modKey, isTonic) => {
|
| 759 |
+
const m = this.modulators[modKey];
|
| 760 |
+
return isTonic ? m.tonic : m.tonic + m.phasic;
|
| 761 |
+
};
|
| 762 |
+
R.D1 = this.receptors.D1.computeActivation(getConc('DA', false));
|
| 763 |
+
R.D2 = this.receptors.D2.computeActivation(getConc('DA', true));
|
| 764 |
+
R.SHT1A = this.receptors.SHT1A.computeActivation(getConc('SHT', true));
|
| 765 |
+
R.SHT2A = this.receptors.SHT2A.computeActivation(getConc('SHT', false));
|
| 766 |
+
R.SHT4 = this.receptors.SHT4.computeActivation(getConc('SHT', false));
|
| 767 |
+
R.M1 = this.receptors.M1.computeActivation(getConc('ACh', false));
|
| 768 |
+
R.M2 = this.receptors.M2.computeActivation(getConc('ACh', true));
|
| 769 |
+
R.B1 = this.receptors.B1.computeActivation(getConc('NA', false));
|
| 770 |
+
R.A2 = this.receptors.A2.computeActivation(getConc('NA', true));
|
| 771 |
+
return R;
|
| 772 |
+
}
|
| 773 |
+
}
|
| 774 |
+
|
| 775 |
+
class NeuraxonSynapse {
|
| 776 |
+
constructor(preId, postId, branchId) {
|
| 777 |
+
this.preId = preId; this.postId = postId; this.branchId = branchId;
|
| 778 |
+
this.wFast = (Math.random() - 0.5) * 0.6;
|
| 779 |
+
this.wSlow = (Math.random() - 0.5) * 0.3;
|
| 780 |
+
this.wMeta = (Math.random() - 0.5) * 0.1;
|
| 781 |
+
this.tauFast = 50; this.tauSlow = 500; this.tauMeta = 5000;
|
| 782 |
+
this.preTrace = 0; this.postTrace = 0;
|
| 783 |
+
this.recentDw = 0; this.integrity = 1.0;
|
| 784 |
+
this.silent = Math.random() < 0.05;
|
| 785 |
+
}
|
| 786 |
+
computeInput(preState) {
|
| 787 |
+
if (this.silent) return 0;
|
| 788 |
+
return (this.wFast + this.wSlow) * preState;
|
| 789 |
+
}
|
| 790 |
+
getModulatoryEffect() { return this.wMeta; }
|
| 791 |
+
update(preState, postState, R, neighborDws, dt) {
|
| 792 |
+
const tauSTDP = 150;
|
| 793 |
+
this.preTrace += (-this.preTrace / tauSTDP + (preState === 1 ? 1 : 0)) * dt / 1000;
|
| 794 |
+
this.postTrace += (-this.postTrace / tauSTDP + (postState === 1 ? 1 : 0)) * dt / 1000;
|
| 795 |
+
const Aplus = this.preTrace * (postState === 1 ? 1 : 0);
|
| 796 |
+
const Aminus = this.postTrace * (preState === 1 ? 1 : 0);
|
| 797 |
+
// Base learning rate β slight boost for turn output neurons to compensate for rarity
|
| 798 |
+
const isTurnTarget = (this.postId === 32 || this.postId === 33);
|
| 799 |
+
const eta = isTurnTarget ? 0.06 : 0.05;
|
| 800 |
+
const d1 = R.D1 || 0.5;
|
| 801 |
+
const d2 = R.D2 || 0.5;
|
| 802 |
+
let dw = eta * Aplus * d1 - eta * 0.6 * Aminus * d2;
|
| 803 |
+
if (preState === 1 && postState === 1) dw += eta * 0.3 * d1;
|
| 804 |
+
if (preState === 1 && postState === -1) dw -= eta * 0.2 * d2;
|
| 805 |
+
if (postState === 0) dw *= 0.1;
|
| 806 |
+
if (neighborDws.length > 0) {
|
| 807 |
+
let assoc = 0;
|
| 808 |
+
for (const nd of neighborDws) assoc += nd / (1 + Math.random());
|
| 809 |
+
dw += 0.01 * assoc;
|
| 810 |
+
}
|
| 811 |
+
this.recentDw = dw;
|
| 812 |
+
this.wFast += (dt / this.tauFast) * (-this.wFast * 0.0005 + 0.5 * dw);
|
| 813 |
+
const slowRate = isTurnTarget ? 0.18 : 0.15;
|
| 814 |
+
this.wSlow += (dt / this.tauSlow) * (-this.wSlow * 0.0005 + slowRate * dw);
|
| 815 |
+
this.wFast = clamp(this.wFast, -1, 1);
|
| 816 |
+
this.wSlow = clamp(this.wSlow, -1, 1);
|
| 817 |
+
const shtFactor = 0.5 * (R.SHT2A || 0.5) + 0.1 * (1 - (R.SHT1A || 0.5));
|
| 818 |
+
this.wMeta += (dt / this.tauMeta) * (-this.wMeta * 0.0005 + 0.05 * dw * shtFactor);
|
| 819 |
+
this.wMeta = clamp(this.wMeta, -0.5, 0.5);
|
| 820 |
+
const activityBonus = (Math.abs(preState) + Math.abs(postState)) * 0.00005;
|
| 821 |
+
this.integrity += (-0.00001 + activityBonus) * dt;
|
| 822 |
+
this.integrity = clamp(this.integrity, 0, 1);
|
| 823 |
+
if (this.silent && Math.abs(preState) === 1 && Math.abs(postState) === 1 && Math.random() < 0.02) {
|
| 824 |
+
this.silent = false;
|
| 825 |
+
}
|
| 826 |
+
}
|
| 827 |
+
}
|
| 828 |
+
|
| 829 |
+
class Neuraxon {
|
| 830 |
+
constructor(id, type) {
|
| 831 |
+
this.id = id; this.type = type;
|
| 832 |
+
this.s = 0; this.state = 0;
|
| 833 |
+
this.theta1 = 0.5; this.theta2 = -0.5;
|
| 834 |
+
this.adaptation = 0; this.autoReceptor = 0;
|
| 835 |
+
this.rBar = 0; this.targetRate = 0.3;
|
| 836 |
+
this.health = 1.0; this.baseRate = 0.05;
|
| 837 |
+
this.tau = 80 + Math.random() * 60;
|
| 838 |
+
this.branches = 3;
|
| 839 |
+
this.x = 0; this.y = 0;
|
| 840 |
+
this.prevState = 0;
|
| 841 |
+
}
|
| 842 |
+
update(branchInputs, modInputs, Iext, oscDrive, R, dt) {
|
| 843 |
+
const alpha = 0.001;
|
| 844 |
+
this.rBar += alpha * (Math.abs(this.state) - this.rBar) * dt / 1000;
|
| 845 |
+
const thetaDend = 0.8;
|
| 846 |
+
let D = 0;
|
| 847 |
+
for (let b = 0; b < this.branches; b++) {
|
| 848 |
+
const inputs = branchInputs[b] || [];
|
| 849 |
+
let sigma = 0;
|
| 850 |
+
for (const inp of inputs) sigma += inp;
|
| 851 |
+
if (Math.abs(sigma) > thetaDend) {
|
| 852 |
+
D += Math.sign(sigma) * Math.pow(Math.abs(sigma), 1.3);
|
| 853 |
+
} else {
|
| 854 |
+
D += sigma;
|
| 855 |
+
}
|
| 856 |
+
}
|
| 857 |
+
const gNA = 1 + 0.5 * (R.B1 || 0.5) + 0.2 * (R.A2 || 0.5);
|
| 858 |
+
const spont = (this.baseRate + 0.3 * (R.A2 || 0.3)) * (Math.random() - 0.4);
|
| 859 |
+
this.s += (dt / this.tau) * (-this.s + gNA * D + Iext + oscDrive - this.adaptation + spont);
|
| 860 |
+
let rawMod = 0;
|
| 861 |
+
for (const m of (modInputs || [])) rawMod += m;
|
| 862 |
+
rawMod += 0.3 * (R.M1 || 0.5) - 0.2 * (R.M2 || 0.3);
|
| 863 |
+
const dThetaMeta = 0.3 * Math.tanh(rawMod);
|
| 864 |
+
const etaH = 0.01;
|
| 865 |
+
const dThetaHomeo = etaH * (this.rBar - this.targetRate);
|
| 866 |
+
const theta1Eff = this.theta1 - dThetaMeta + dThetaHomeo - 0.1 * this.autoReceptor;
|
| 867 |
+
const theta2Eff = this.theta2 - dThetaMeta + dThetaHomeo + 0.1 * this.autoReceptor;
|
| 868 |
+
this.prevState = this.state;
|
| 869 |
+
if (this.s > theta1Eff) this.state = 1;
|
| 870 |
+
else if (this.s < theta2Eff) this.state = -1;
|
| 871 |
+
else this.state = 0;
|
| 872 |
+
const tauA = 300, tauR = 1000;
|
| 873 |
+
this.adaptation += (dt / tauA) * (-this.adaptation + 0.1 * Math.abs(this.state));
|
| 874 |
+
this.autoReceptor += (dt / tauR) * (-this.autoReceptor + 0.2 * this.state);
|
| 875 |
+
this.health -= 0.00001 * (1 - Math.abs(this.state)) * dt / 1000;
|
| 876 |
+
this.health = clamp(this.health, 0.1, 1);
|
| 877 |
+
}
|
| 878 |
+
}
|
| 879 |
+
|
| 880 |
+
class NeuraxonNetwork {
|
| 881 |
+
constructor(nInput, nHidden, nOutput) {
|
| 882 |
+
this.neurons = [];
|
| 883 |
+
this.synapses = [];
|
| 884 |
+
this.neuromod = new NeuromodulatorSystem();
|
| 885 |
+
this.oscillators = new OscillatorBank();
|
| 886 |
+
this.energy = 0;
|
| 887 |
+
this.step = 0;
|
| 888 |
+
this.meanDw = 0;
|
| 889 |
+
this.structEvent = '';
|
| 890 |
+
for (let i = 0; i < nInput; i++) this.neurons.push(new Neuraxon(i, 'input'));
|
| 891 |
+
for (let i = 0; i < nHidden; i++) this.neurons.push(new Neuraxon(nInput + i, 'hidden'));
|
| 892 |
+
for (let i = 0; i < nOutput; i++) this.neurons.push(new Neuraxon(nInput + nHidden + i, 'output'));
|
| 893 |
+
this.N = this.neurons.length;
|
| 894 |
+
this.nInput = nInput; this.nHidden = nHidden; this.nOutput = nOutput;
|
| 895 |
+
this._buildSmallWorld(6, 0.2);
|
| 896 |
+
this._addMotorPathways();
|
| 897 |
+
this._layoutNeurons();
|
| 898 |
+
}
|
| 899 |
+
_addMotorPathways() {
|
| 900 |
+
const outStart = this.nInput + this.nHidden;
|
| 901 |
+
// Direct inputβoutput: turn pathways get a slight head start
|
| 902 |
+
const directMap = [[0, 0], [1, 2], [2, 3], [3, 1]];
|
| 903 |
+
const turnOutputs = new Set([2, 3]);
|
| 904 |
+
for (const [inp, out] of directMap) {
|
| 905 |
+
const syn = new NeuraxonSynapse(inp, outStart + out, 0);
|
| 906 |
+
const isTurn = turnOutputs.has(out);
|
| 907 |
+
syn.wFast = isTurn ? 0.7 + Math.random() * 0.15 : 0.6 + Math.random() * 0.2;
|
| 908 |
+
syn.wSlow = isTurn ? 0.35 + Math.random() * 0.1 : 0.3 + Math.random() * 0.1;
|
| 909 |
+
syn.integrity = 1.0;
|
| 910 |
+
syn.silent = false;
|
| 911 |
+
this.synapses.push(syn);
|
| 912 |
+
}
|
| 913 |
+
for (let h = this.nInput; h < outStart; h++) {
|
| 914 |
+
for (let o = outStart; o < this.N; o++) {
|
| 915 |
+
const isTurnOut = (o === outStart + 2 || o === outStart + 3);
|
| 916 |
+
const connProb = isTurnOut ? 0.45 : 0.4;
|
| 917 |
+
if (Math.random() < connProb) {
|
| 918 |
+
const syn = new NeuraxonSynapse(h, o, Math.floor(Math.random() * 3));
|
| 919 |
+
syn.wFast = (Math.random() - 0.5) * 0.4;
|
| 920 |
+
syn.wSlow = (Math.random() - 0.5) * 0.2;
|
| 921 |
+
this.synapses.push(syn);
|
| 922 |
+
}
|
| 923 |
+
}
|
| 924 |
+
}
|
| 925 |
+
for (let i = 0; i < this.nInput; i++) {
|
| 926 |
+
for (let h = this.nInput; h < outStart; h++) {
|
| 927 |
+
if (Math.random() < 0.35) {
|
| 928 |
+
const syn = new NeuraxonSynapse(i, h, Math.floor(Math.random() * 3));
|
| 929 |
+
syn.wFast = (Math.random() - 0.5) * 0.5;
|
| 930 |
+
syn.wSlow = (Math.random() - 0.5) * 0.2;
|
| 931 |
+
this.synapses.push(syn);
|
| 932 |
+
}
|
| 933 |
+
}
|
| 934 |
+
}
|
| 935 |
+
for (let i = 4; i <= 5; i++) {
|
| 936 |
+
for (let h = this.nInput; h < outStart; h++) {
|
| 937 |
+
if (Math.random() < 0.5) {
|
| 938 |
+
const syn = new NeuraxonSynapse(i, h, Math.floor(Math.random() * 3));
|
| 939 |
+
syn.wFast = (Math.random() - 0.3) * 0.3;
|
| 940 |
+
this.synapses.push(syn);
|
| 941 |
+
}
|
| 942 |
+
}
|
| 943 |
+
}
|
| 944 |
+
}
|
| 945 |
+
_buildSmallWorld(k, beta) {
|
| 946 |
+
const N = this.N;
|
| 947 |
+
const halfK = Math.floor(k / 2);
|
| 948 |
+
const edges = new Set();
|
| 949 |
+
for (let i = 0; i < N; i++) {
|
| 950 |
+
for (let j = 1; j <= halfK; j++) {
|
| 951 |
+
const target = (i + j) % N;
|
| 952 |
+
const key = Math.min(i, target) + '-' + Math.max(i, target);
|
| 953 |
+
if (!edges.has(key)) {
|
| 954 |
+
edges.add(key);
|
| 955 |
+
const branchId = Math.floor(Math.random() * 3);
|
| 956 |
+
this.synapses.push(new NeuraxonSynapse(i, target, branchId));
|
| 957 |
+
}
|
| 958 |
+
}
|
| 959 |
+
}
|
| 960 |
+
const synCopy = [...this.synapses];
|
| 961 |
+
for (const syn of synCopy) {
|
| 962 |
+
if (Math.random() < beta) {
|
| 963 |
+
let newTarget;
|
| 964 |
+
do { newTarget = Math.floor(Math.random() * N); } while (newTarget === syn.preId);
|
| 965 |
+
syn.postId = newTarget;
|
| 966 |
+
syn.branchId = Math.floor(Math.random() * 3);
|
| 967 |
+
}
|
| 968 |
+
}
|
| 969 |
+
}
|
| 970 |
+
_layoutNeurons() {
|
| 971 |
+
// βββ ENHANCED POLAR / RADIAL LAYOUT βββ
|
| 972 |
+
// Spread neurons in polar coordinates with staggered radii for visual depth
|
| 973 |
+
const cx = 0, cy = 0; // world-space center
|
| 974 |
+
|
| 975 |
+
// Input neurons: inner ring with slight jitter
|
| 976 |
+
for (let i = 0; i < this.nInput; i++) {
|
| 977 |
+
const angle = (2 * Math.PI * i / this.nInput) - Math.PI / 2;
|
| 978 |
+
const r = 80 + (i % 2) * 12;
|
| 979 |
+
this.neurons[i].x = cx + r * Math.cos(angle);
|
| 980 |
+
this.neurons[i].y = cy + r * Math.sin(angle);
|
| 981 |
+
}
|
| 982 |
+
// Hidden neurons: two staggered concentric bands
|
| 983 |
+
const hidStart = this.nInput;
|
| 984 |
+
for (let i = 0; i < this.nHidden; i++) {
|
| 985 |
+
const angle = (2 * Math.PI * i / this.nHidden) - Math.PI / 2;
|
| 986 |
+
const band = (i % 3 === 0) ? 190 : (i % 3 === 1) ? 230 : 260;
|
| 987 |
+
const jitter = (Math.sin(i * 2.7) * 15);
|
| 988 |
+
this.neurons[hidStart + i].x = cx + (band + jitter) * Math.cos(angle);
|
| 989 |
+
this.neurons[hidStart + i].y = cy + (band + jitter) * Math.sin(angle);
|
| 990 |
+
}
|
| 991 |
+
// Output neurons: outer ring
|
| 992 |
+
const outStart = this.nInput + this.nHidden;
|
| 993 |
+
for (let i = 0; i < this.nOutput; i++) {
|
| 994 |
+
const angle = (2 * Math.PI * i / this.nOutput) - Math.PI / 2;
|
| 995 |
+
const r = 350;
|
| 996 |
+
this.neurons[outStart + i].x = cx + r * Math.cos(angle);
|
| 997 |
+
this.neurons[outStart + i].y = cy + r * Math.sin(angle);
|
| 998 |
+
}
|
| 999 |
+
}
|
| 1000 |
+
setInputs(values) {
|
| 1001 |
+
for (let i = 0; i < Math.min(values.length, this.nInput); i++) {
|
| 1002 |
+
const n = this.neurons[i];
|
| 1003 |
+
n.s = values[i] * 2.0;
|
| 1004 |
+
n.state = values[i] > 0.2 ? 1 : values[i] < -0.2 ? -1 : 0;
|
| 1005 |
+
n.adaptation = 0;
|
| 1006 |
+
n.autoReceptor = 0;
|
| 1007 |
+
}
|
| 1008 |
+
}
|
| 1009 |
+
getOutputStates() {
|
| 1010 |
+
const start = this.nInput + this.nHidden;
|
| 1011 |
+
return this.neurons.slice(start).map(n => n.state);
|
| 1012 |
+
}
|
| 1013 |
+
getOutputValues() {
|
| 1014 |
+
const start = this.nInput + this.nHidden;
|
| 1015 |
+
return this.neurons.slice(start).map(n => n.s);
|
| 1016 |
+
}
|
| 1017 |
+
simulateStep(dt) {
|
| 1018 |
+
this.step++;
|
| 1019 |
+
let excCount = 0, inhCount = 0, neuCount = 0, totalAbs = 0, changes = 0;
|
| 1020 |
+
for (const n of this.neurons) {
|
| 1021 |
+
if (n.state === 1) excCount++;
|
| 1022 |
+
else if (n.state === -1) inhCount++;
|
| 1023 |
+
else neuCount++;
|
| 1024 |
+
totalAbs += Math.abs(n.state);
|
| 1025 |
+
if (n.state !== n.prevState) changes++;
|
| 1026 |
+
}
|
| 1027 |
+
const activity = {
|
| 1028 |
+
excFrac: excCount / this.N,
|
| 1029 |
+
meanAct: totalAbs / this.N,
|
| 1030 |
+
changeRate: changes / this.N,
|
| 1031 |
+
};
|
| 1032 |
+
this.neuromod.update(activity, dt);
|
| 1033 |
+
this.oscillators.update(dt);
|
| 1034 |
+
const R = this.neuromod.computeReceptorActivations();
|
| 1035 |
+
const branchInputs = new Array(this.N);
|
| 1036 |
+
const modInputs = new Array(this.N);
|
| 1037 |
+
for (let i = 0; i < this.N; i++) {
|
| 1038 |
+
branchInputs[i] = [[], [], []];
|
| 1039 |
+
modInputs[i] = [];
|
| 1040 |
+
}
|
| 1041 |
+
for (const syn of this.synapses) {
|
| 1042 |
+
const preState = this.neurons[syn.preId].state;
|
| 1043 |
+
const input = syn.computeInput(preState);
|
| 1044 |
+
if (branchInputs[syn.postId] && branchInputs[syn.postId][syn.branchId]) {
|
| 1045 |
+
branchInputs[syn.postId][syn.branchId].push(input);
|
| 1046 |
+
}
|
| 1047 |
+
if (modInputs[syn.postId]) {
|
| 1048 |
+
modInputs[syn.postId].push(syn.getModulatoryEffect());
|
| 1049 |
+
}
|
| 1050 |
+
}
|
| 1051 |
+
for (const n of this.neurons) {
|
| 1052 |
+
if (n.type === 'input') continue;
|
| 1053 |
+
const osc = this.oscillators.getDrive(n.id, this.N);
|
| 1054 |
+
n.update(branchInputs[n.id], modInputs[n.id], 0, osc, R, dt);
|
| 1055 |
+
}
|
| 1056 |
+
let totalDw = 0;
|
| 1057 |
+
for (const syn of this.synapses) {
|
| 1058 |
+
const pre = this.neurons[syn.preId];
|
| 1059 |
+
const post = this.neurons[syn.postId];
|
| 1060 |
+
const neighbors = this.synapses
|
| 1061 |
+
.filter(s => s.postId === syn.postId && s !== syn && s.branchId === syn.branchId)
|
| 1062 |
+
.slice(0, 3)
|
| 1063 |
+
.map(s => s.recentDw);
|
| 1064 |
+
syn.update(pre.state, post.state, R, neighbors, dt);
|
| 1065 |
+
totalDw += Math.abs(syn.recentDw);
|
| 1066 |
+
}
|
| 1067 |
+
this.meanDw = this.synapses.length > 0 ? totalDw / this.synapses.length : 0;
|
| 1068 |
+
this.structEvent = '';
|
| 1069 |
+
const toRemove = [];
|
| 1070 |
+
for (let i = this.synapses.length - 1; i >= 0; i--) {
|
| 1071 |
+
if (this.synapses[i].integrity < 0.1) { toRemove.push(i); }
|
| 1072 |
+
}
|
| 1073 |
+
for (const idx of toRemove) { this.synapses.splice(idx, 1); this.structEvent = 'pruned'; }
|
| 1074 |
+
if (Math.random() < 0.002 && this.synapses.length < this.N * 8) {
|
| 1075 |
+
const a = Math.floor(Math.random() * this.N);
|
| 1076 |
+
const b = Math.floor(Math.random() * this.N);
|
| 1077 |
+
if (a !== b) {
|
| 1078 |
+
this.synapses.push(new NeuraxonSynapse(a, b, Math.floor(Math.random() * 3)));
|
| 1079 |
+
this.structEvent = 'formed';
|
| 1080 |
+
}
|
| 1081 |
+
}
|
| 1082 |
+
this.energy += 0.01 * excCount * dt / 1000;
|
| 1083 |
+
return { excCount, inhCount, neuCount, R, activity };
|
| 1084 |
+
}
|
| 1085 |
+
}
|
| 1086 |
+
|
| 1087 |
+
|
| 1088 |
+
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1089 |
+
// APPLICATION LAYER
|
| 1090 |
+
// βββββββββββοΏ½οΏ½βββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1091 |
+
|
| 1092 |
+
const $ = id => document.getElementById(id);
|
| 1093 |
+
const sleep = ms => new Promise(r => setTimeout(r, ms));
|
| 1094 |
+
const clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
|
| 1095 |
+
|
| 1096 |
+
function sysLog(msg) {
|
| 1097 |
+
const el = $('logPanel');
|
| 1098 |
+
const ts = new Date().toLocaleTimeString('en-US', { hour12: false });
|
| 1099 |
+
el.textContent += `[${ts}] ${msg}\n`;
|
| 1100 |
+
el.scrollTop = el.scrollHeight;
|
| 1101 |
+
}
|
| 1102 |
+
|
| 1103 |
+
let sphero = null;
|
| 1104 |
+
let isConnected = false;
|
| 1105 |
+
let controlMode = 'manual';
|
| 1106 |
+
let heading = 0;
|
| 1107 |
+
const BASE_SPEED = 90;
|
| 1108 |
+
const TURN_DEG = 8;
|
| 1109 |
+
const activeKeys = new Set();
|
| 1110 |
+
let simRunning = true;
|
| 1111 |
+
|
| 1112 |
+
const outputAccum = [0, 0, 0, 0];
|
| 1113 |
+
const OUTPUT_DECAY = 0.92;
|
| 1114 |
+
const OUTPUT_GAIN = 0.5;
|
| 1115 |
+
|
| 1116 |
+
let teachingActive = false;
|
| 1117 |
+
let teachingTarget = [0, 0, 0, 0];
|
| 1118 |
+
let learnCount = 0;
|
| 1119 |
+
let lastLearnLog = 0;
|
| 1120 |
+
|
| 1121 |
+
// ββ STOP learning: track idle to teach "all-off" as a real behavior ββ
|
| 1122 |
+
let idleTicks = 0;
|
| 1123 |
+
const IDLE_TEACH_DELAY = 25; // ~0.4s no keys β teach "stop"
|
| 1124 |
+
|
| 1125 |
+
// ββ Movement replay: remember human sequences for Neuraxon mode ββ
|
| 1126 |
+
const replayBuffer = []; // { inputs: [...], target: [...] }
|
| 1127 |
+
const REPLAY_MAX = 200;
|
| 1128 |
+
let replayIdx = 0;
|
| 1129 |
+
let isReplaying = false;
|
| 1130 |
+
|
| 1131 |
+
let learnHits = 0;
|
| 1132 |
+
let learnTrials = 0;
|
| 1133 |
+
let learnAccuracy = 0;
|
| 1134 |
+
const LEARN_WINDOW = 300;
|
| 1135 |
+
const learnHistory = [];
|
| 1136 |
+
let nxonContributing = false;
|
| 1137 |
+
|
| 1138 |
+
let explorePhase = 0;
|
| 1139 |
+
let exploreBehavior = 0;
|
| 1140 |
+
let exploreTimer = 0;
|
| 1141 |
+
const EXPLORE_DURATION = 160; // ticks per behavior (~2.6s)
|
| 1142 |
+
|
| 1143 |
+
const network = new NeuraxonNetwork(6, 24, 4);
|
| 1144 |
+
|
| 1145 |
+
const activityHistory = [];
|
| 1146 |
+
const MAX_HIST = 200;
|
| 1147 |
+
|
| 1148 |
+
// --- Build receptor bars UI ---
|
| 1149 |
+
function buildReceptorUI() {
|
| 1150 |
+
const container = $('receptorBars');
|
| 1151 |
+
const receptorNames = ['D1','D2','5HT1A','5HT2A','5HT4','M1','M2','Ξ²1','Ξ±2'];
|
| 1152 |
+
const colors = {
|
| 1153 |
+
D1: 'var(--da-color)', D2: 'var(--da-color)',
|
| 1154 |
+
'5HT1A': 'var(--sht-color)', '5HT2A': 'var(--sht-color)', '5HT4': 'var(--sht-color)',
|
| 1155 |
+
M1: 'var(--ach-color)', M2: 'var(--ach-color)',
|
| 1156 |
+
'Ξ²1': 'var(--na-color)', 'Ξ±2': 'var(--na-color)',
|
| 1157 |
+
};
|
| 1158 |
+
container.innerHTML = '';
|
| 1159 |
+
for (const name of receptorNames) {
|
| 1160 |
+
const row = document.createElement('div');
|
| 1161 |
+
row.style.cssText = 'display:flex;align-items:center;gap:6px;';
|
| 1162 |
+
row.innerHTML = `
|
| 1163 |
+
<span style="font-family:var(--mono);font-size:9px;width:38px;text-align:right;color:${colors[name]};font-weight:600;">${name}</span>
|
| 1164 |
+
<div style="flex:1;height:4px;background:var(--bg-abyss);border-radius:2px;overflow:hidden;">
|
| 1165 |
+
<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>
|
| 1166 |
+
</div>
|
| 1167 |
+
`;
|
| 1168 |
+
container.appendChild(row);
|
| 1169 |
+
}
|
| 1170 |
+
}
|
| 1171 |
+
buildReceptorUI();
|
| 1172 |
+
|
| 1173 |
+
['DA','5HT','ACh','NA'].forEach(key => {
|
| 1174 |
+
const sliderId = key === '5HT' ? 'slider5HT' : `slider${key}`;
|
| 1175 |
+
const valId = key === '5HT' ? 'val5HT' : `val${key}`;
|
| 1176 |
+
const modKey = key === '5HT' ? 'SHT' : key;
|
| 1177 |
+
$(sliderId).addEventListener('input', e => {
|
| 1178 |
+
const v = e.target.value / 100;
|
| 1179 |
+
$(valId).textContent = v.toFixed(2);
|
| 1180 |
+
network.neuromod.externalOverrides[modKey] = v;
|
| 1181 |
+
});
|
| 1182 |
+
});
|
| 1183 |
+
|
| 1184 |
+
function setMode(mode) {
|
| 1185 |
+
controlMode = mode;
|
| 1186 |
+
document.querySelectorAll('.mode-btn').forEach(b => b.classList.toggle('active', b.dataset.mode === mode));
|
| 1187 |
+
$('modeLabel').textContent = mode.toUpperCase();
|
| 1188 |
+
const driveCard = $('driveCard');
|
| 1189 |
+
if (mode === 'neuraxon') {
|
| 1190 |
+
driveCard.classList.add('controls-disabled');
|
| 1191 |
+
} else {
|
| 1192 |
+
driveCard.classList.remove('controls-disabled');
|
| 1193 |
+
}
|
| 1194 |
+
sysLog(`Mode: ${mode.toUpperCase()}`);
|
| 1195 |
+
}
|
| 1196 |
+
|
| 1197 |
+
function getHumanAction() {
|
| 1198 |
+
let speed = 0, turn = 0;
|
| 1199 |
+
if (activeKeys.has('up')) speed = BASE_SPEED;
|
| 1200 |
+
if (activeKeys.has('down')) speed = -BASE_SPEED;
|
| 1201 |
+
if (activeKeys.has('left')) turn = -TURN_DEG;
|
| 1202 |
+
if (activeKeys.has('right')) turn = TURN_DEG;
|
| 1203 |
+
if (activeKeys.has('up') && activeKeys.has('left')) { speed = BASE_SPEED * 0.7; turn = -TURN_DEG; }
|
| 1204 |
+
if (activeKeys.has('up') && activeKeys.has('right')) { speed = BASE_SPEED * 0.7; turn = TURN_DEG; }
|
| 1205 |
+
return { speed, turn };
|
| 1206 |
+
}
|
| 1207 |
+
|
| 1208 |
+
function applyTeachingSignal() {
|
| 1209 |
+
const outStart = network.nInput + network.nHidden;
|
| 1210 |
+
teachingTarget = [0, 0, 0, 0];
|
| 1211 |
+
teachingActive = false;
|
| 1212 |
+
|
| 1213 |
+
if (activeKeys.has('up')) { teachingTarget[0] = 1; teachingActive = true; }
|
| 1214 |
+
if (activeKeys.has('down')) { teachingTarget[1] = 1; teachingActive = true; }
|
| 1215 |
+
if (activeKeys.has('left')) { teachingTarget[2] = 1; teachingActive = true; }
|
| 1216 |
+
if (activeKeys.has('right')) { teachingTarget[3] = 1; teachingActive = true; }
|
| 1217 |
+
|
| 1218 |
+
// ββ STOP TEACHING: when human releases keys, teach "no output" ββ
|
| 1219 |
+
if (!teachingActive && (controlMode === 'manual' || controlMode === 'hybrid')) {
|
| 1220 |
+
idleTicks++;
|
| 1221 |
+
// After a brief pause, actively teach the network to be quiet
|
| 1222 |
+
if (idleTicks > IDLE_TEACH_DELAY && idleTicks < IDLE_TEACH_DELAY + 60) {
|
| 1223 |
+
teachingActive = true;
|
| 1224 |
+
// All targets stay 0 β this IS the stop signal
|
| 1225 |
+
for (let i = 0; i < 4; i++) {
|
| 1226 |
+
const n = network.neurons[outStart + i];
|
| 1227 |
+
n.s = -0.3;
|
| 1228 |
+
n.state = 0;
|
| 1229 |
+
}
|
| 1230 |
+
// Mild DA for the stop-learning (smaller than move-learning)
|
| 1231 |
+
network.neuromod.modulators.DA.phasic = clamp(
|
| 1232 |
+
network.neuromod.modulators.DA.phasic + 0.08, 0, 1
|
| 1233 |
+
);
|
| 1234 |
+
learnCount++;
|
| 1235 |
+
return; // don't also run the movement clamp below
|
| 1236 |
+
}
|
| 1237 |
+
} else {
|
| 1238 |
+
idleTicks = 0;
|
| 1239 |
+
}
|
| 1240 |
+
|
| 1241 |
+
if (teachingActive && (controlMode === 'manual' || controlMode === 'hybrid')) {
|
| 1242 |
+
const isTurnTeach = (teachingTarget[2] === 1 || teachingTarget[3] === 1);
|
| 1243 |
+
|
| 1244 |
+
// ββ Record to replay buffer (raw inputβtarget pair) ββ
|
| 1245 |
+
const currentInputs = computeSensoryInputs();
|
| 1246 |
+
replayBuffer.push({ inputs: [...currentInputs], target: [...teachingTarget] });
|
| 1247 |
+
if (replayBuffer.length > REPLAY_MAX) replayBuffer.shift();
|
| 1248 |
+
|
| 1249 |
+
// ββ MEASURE ACCURACY BEFORE CLAMPING ββ
|
| 1250 |
+
let hits = 0, evaluated = 0;
|
| 1251 |
+
for (let i = 0; i < 4; i++) {
|
| 1252 |
+
const n = network.neurons[outStart + i];
|
| 1253 |
+
const naturalState = n.state;
|
| 1254 |
+
const desired = teachingTarget[i];
|
| 1255 |
+
if (desired === 1) {
|
| 1256 |
+
evaluated++;
|
| 1257 |
+
if (naturalState === 1) hits++;
|
| 1258 |
+
} else {
|
| 1259 |
+
evaluated++;
|
| 1260 |
+
if (naturalState <= 0) hits++;
|
| 1261 |
+
}
|
| 1262 |
+
}
|
| 1263 |
+
const hitRatio = evaluated > 0 ? hits / evaluated : 0;
|
| 1264 |
+
learnHistory.push(hitRatio);
|
| 1265 |
+
if (learnHistory.length > LEARN_WINDOW) learnHistory.shift();
|
| 1266 |
+
if (learnHistory.length > 10) {
|
| 1267 |
+
const sum = learnHistory.reduce((a, b) => a + b, 0);
|
| 1268 |
+
learnAccuracy = Math.round((sum / learnHistory.length) * 100);
|
| 1269 |
+
}
|
| 1270 |
+
|
| 1271 |
+
// ββ CLAMP OUTPUT NEURONS ββ
|
| 1272 |
+
for (let i = 0; i < 4; i++) {
|
| 1273 |
+
const n = network.neurons[outStart + i];
|
| 1274 |
+
if (teachingTarget[i] > 0) {
|
| 1275 |
+
n.s = n.theta1 + 0.8;
|
| 1276 |
+
n.state = 1;
|
| 1277 |
+
} else {
|
| 1278 |
+
n.s = -0.2;
|
| 1279 |
+
n.state = 0;
|
| 1280 |
+
}
|
| 1281 |
+
}
|
| 1282 |
+
|
| 1283 |
+
// ββ NEUROMODULATOR BURSTS β moderate turn bonus ββ
|
| 1284 |
+
const daBoost = isTurnTeach ? 0.25 : 0.2;
|
| 1285 |
+
network.neuromod.modulators.DA.phasic = clamp(
|
| 1286 |
+
network.neuromod.modulators.DA.phasic + daBoost, 0, 1
|
| 1287 |
+
);
|
| 1288 |
+
const achBase = 0.12 * (1 - learnAccuracy / 150);
|
| 1289 |
+
const achBoost = isTurnTeach ? Math.max(0.05, achBase) : Math.max(0.03, achBase);
|
| 1290 |
+
network.neuromod.modulators.ACh.phasic = clamp(
|
| 1291 |
+
network.neuromod.modulators.ACh.phasic + achBoost, 0, 1
|
| 1292 |
+
);
|
| 1293 |
+
network.neuromod.modulators.NA.phasic = clamp(
|
| 1294 |
+
network.neuromod.modulators.NA.phasic + 0.05, 0, 1
|
| 1295 |
+
);
|
| 1296 |
+
|
| 1297 |
+
learnCount++;
|
| 1298 |
+
if (learnCount - lastLearnLog >= 200) {
|
| 1299 |
+
sysLog(`Learn: ${learnCount} steps | acc: ${learnAccuracy}% | ${isTurnTeach ? 'β» turn' : 'β fwd'}`);
|
| 1300 |
+
lastLearnLog = learnCount;
|
| 1301 |
+
}
|
| 1302 |
+
}
|
| 1303 |
+
}
|
| 1304 |
+
|
| 1305 |
+
function getNeuraxonAction() {
|
| 1306 |
+
const outStart = network.nInput + network.nHidden;
|
| 1307 |
+
for (let i = 0; i < 4; i++) {
|
| 1308 |
+
const state = network.neurons[outStart + i].state;
|
| 1309 |
+
outputAccum[i] = outputAccum[i] * OUTPUT_DECAY + state * OUTPUT_GAIN;
|
| 1310 |
+
outputAccum[i] = clamp(outputAccum[i], -2.0, 2.0);
|
| 1311 |
+
}
|
| 1312 |
+
const fwd = Math.max(0, outputAccum[0]);
|
| 1313 |
+
const back = Math.max(0, outputAccum[1]);
|
| 1314 |
+
const left = Math.max(0, outputAccum[2]);
|
| 1315 |
+
const right = Math.max(0, outputAccum[3]);
|
| 1316 |
+
const rawSpeed = fwd - back * 0.5;
|
| 1317 |
+
const speed = clamp(Math.round(rawSpeed * BASE_SPEED * 0.9), 0, 180);
|
| 1318 |
+
const turnRaw = (right - left);
|
| 1319 |
+
const turn = turnRaw * TURN_DEG * 2.8;
|
| 1320 |
+
nxonContributing = (speed > 12 || Math.abs(turn) > 2);
|
| 1321 |
+
return { speed, turn };
|
| 1322 |
+
}
|
| 1323 |
+
|
| 1324 |
+
function computeSensoryInputs() {
|
| 1325 |
+
const inputs = new Array(6).fill(0);
|
| 1326 |
+
if (controlMode === 'manual' || controlMode === 'hybrid') {
|
| 1327 |
+
if (activeKeys.has('up')) inputs[0] = 1.0;
|
| 1328 |
+
if (activeKeys.has('left')) inputs[1] = 1.0;
|
| 1329 |
+
if (activeKeys.has('right')) inputs[2] = 1.0;
|
| 1330 |
+
if (activeKeys.has('down')) inputs[3] = 1.0;
|
| 1331 |
+
inputs[4] = Math.sin(Date.now() / 4000) * 0.2;
|
| 1332 |
+
inputs[5] = Math.cos(Date.now() / 5000) * 0.15;
|
| 1333 |
+
}
|
| 1334 |
+
if (controlMode === 'hybrid') {
|
| 1335 |
+
inputs[4] += Math.sin(Date.now() / 2000) * 0.25;
|
| 1336 |
+
inputs[5] += Math.cos(Date.now() / 2500) * 0.2;
|
| 1337 |
+
if (nxonContributing) {
|
| 1338 |
+
inputs[4] += outputAccum[0] * 0.15;
|
| 1339 |
+
inputs[5] += (outputAccum[3] - outputAccum[2]) * 0.1;
|
| 1340 |
+
}
|
| 1341 |
+
}
|
| 1342 |
+
if (controlMode === 'neuraxon') {
|
| 1343 |
+
// ββ REPLAY-FIRST STRATEGY: replay what the human actually did ββ
|
| 1344 |
+
// If we have recorded moves, prefer replaying them over random exploration
|
| 1345 |
+
if (replayBuffer.length > 20 && Math.random() < 0.6) {
|
| 1346 |
+
// Replay a stored input pattern β cycles through the buffer
|
| 1347 |
+
isReplaying = true;
|
| 1348 |
+
const frame = replayBuffer[replayIdx % replayBuffer.length];
|
| 1349 |
+
replayIdx++;
|
| 1350 |
+
// Feed the stored sensory inputs (first 4 channels = directional)
|
| 1351 |
+
for (let i = 0; i < 4; i++) {
|
| 1352 |
+
inputs[i] = frame.inputs[i] * 0.8; // slightly attenuated replay
|
| 1353 |
+
}
|
| 1354 |
+
} else {
|
| 1355 |
+
// ββ EXPLORATION: balanced patterns including STOP ββ
|
| 1356 |
+
isReplaying = false;
|
| 1357 |
+
exploreTimer++;
|
| 1358 |
+
if (exploreTimer >= EXPLORE_DURATION) {
|
| 1359 |
+
exploreTimer = 0;
|
| 1360 |
+
exploreBehavior = (exploreBehavior + 1) % 8;
|
| 1361 |
+
}
|
| 1362 |
+
const t = exploreTimer / EXPLORE_DURATION;
|
| 1363 |
+
const ramp = t < 0.15 ? t / 0.15 : t > 0.85 ? (1 - t) / 0.15 : 1.0;
|
| 1364 |
+
const intensity = (0.85 + 0.15 * Math.sin(Date.now() / 800)) * ramp;
|
| 1365 |
+
switch (exploreBehavior) {
|
| 1366 |
+
case 0: inputs[0] = intensity; break; // forward
|
| 1367 |
+
case 1: inputs[1] = intensity; break; // left
|
| 1368 |
+
case 2: inputs[2] = intensity; break; // right
|
| 1369 |
+
case 3: inputs[0] = intensity; inputs[2] = intensity * 0.6; break; // fwd + right
|
| 1370 |
+
case 4: inputs[0] = intensity; inputs[1] = intensity * 0.6; break; // fwd + left
|
| 1371 |
+
case 5: inputs[3] = intensity * 0.5; break; // back
|
| 1372 |
+
case 6: break; // STOP β all zero
|
| 1373 |
+
case 7: break; // STOP β all zero (2nd pause)
|
| 1374 |
+
}
|
| 1375 |
+
}
|
| 1376 |
+
|
| 1377 |
+
// Self-reinforcement: feed output accumulators back evenly
|
| 1378 |
+
const selfGain = 0.25;
|
| 1379 |
+
inputs[0] += clamp(outputAccum[0] * selfGain, 0, 0.3);
|
| 1380 |
+
inputs[1] += clamp(outputAccum[2] * selfGain, 0, 0.3);
|
| 1381 |
+
inputs[2] += clamp(outputAccum[3] * selfGain, 0, 0.3);
|
| 1382 |
+
inputs[3] += clamp(outputAccum[1] * selfGain, 0, 0.3);
|
| 1383 |
+
|
| 1384 |
+
// Rhythmic drive
|
| 1385 |
+
inputs[4] = Math.sin(Date.now() / 1000) * 0.5 + Math.sin(Date.now() / 3300) * 0.2;
|
| 1386 |
+
inputs[5] = Math.cos(Date.now() / 1400) * 0.4 + Math.cos(Date.now() / 4500) * 0.2;
|
| 1387 |
+
}
|
| 1388 |
+
for (let i = 0; i < 6; i++) inputs[i] = clamp(inputs[i], -1.5, 1.5);
|
| 1389 |
+
return inputs;
|
| 1390 |
+
}
|
| 1391 |
+
|
| 1392 |
+
// --- Drive loop ---
|
| 1393 |
+
let driveInterval = null;
|
| 1394 |
+
function startDriveLoop() {
|
| 1395 |
+
if (driveInterval) clearInterval(driveInterval);
|
| 1396 |
+
driveInterval = setInterval(() => {
|
| 1397 |
+
if (!isConnected || !sphero) return;
|
| 1398 |
+
const human = getHumanAction();
|
| 1399 |
+
const ai = getNeuraxonAction();
|
| 1400 |
+
let finalSpeed, finalTurn;
|
| 1401 |
+
if (controlMode === 'manual') {
|
| 1402 |
+
finalSpeed = Math.abs(human.speed);
|
| 1403 |
+
finalTurn = human.turn;
|
| 1404 |
+
if (human.speed < 0) finalTurn = 180;
|
| 1405 |
+
} else if (controlMode === 'hybrid') {
|
| 1406 |
+
const hSpd = Math.abs(human.speed);
|
| 1407 |
+
finalSpeed = Math.round(hSpd * 0.75 + ai.speed * 0.25);
|
| 1408 |
+
finalTurn = human.turn * 0.75 + ai.turn * 0.25;
|
| 1409 |
+
} else {
|
| 1410 |
+
finalSpeed = ai.speed;
|
| 1411 |
+
finalTurn = ai.turn;
|
| 1412 |
+
}
|
| 1413 |
+
heading = (heading + finalTurn + 360) % 360;
|
| 1414 |
+
sphero.roll(clamp(Math.round(finalSpeed), 0, 255), Math.round(heading)).catch(() => {});
|
| 1415 |
+
|
| 1416 |
+
if (controlMode === 'manual') {
|
| 1417 |
+
if (teachingActive && idleTicks > IDLE_TEACH_DELAY) {
|
| 1418 |
+
// Red-orange = teaching STOP
|
| 1419 |
+
sphero.setMainLED(200, 80, 50).catch(() => {});
|
| 1420 |
+
} else if (teachingActive) {
|
| 1421 |
+
sphero.setMainLED(250, 180, 30).catch(() => {});
|
| 1422 |
+
} else {
|
| 1423 |
+
sphero.setMainLED(40, 200, 140).catch(() => {});
|
| 1424 |
+
}
|
| 1425 |
+
} else if (controlMode === 'hybrid') {
|
| 1426 |
+
if (teachingActive && nxonContributing) sphero.setMainLED(220, 180, 255).catch(() => {});
|
| 1427 |
+
else if (teachingActive) sphero.setMainLED(250, 180, 30).catch(() => {});
|
| 1428 |
+
else if (nxonContributing) sphero.setMainLED(160, 100, 255).catch(() => {});
|
| 1429 |
+
else sphero.setMainLED(40, 140, 160).catch(() => {});
|
| 1430 |
+
} else {
|
| 1431 |
+
if (nxonContributing) sphero.setMainLED(140, 80, 255).catch(() => {});
|
| 1432 |
+
else sphero.setMainLED(30, 180, 220).catch(() => {});
|
| 1433 |
+
}
|
| 1434 |
+
|
| 1435 |
+
$('speedVal').textContent = Math.round(finalSpeed);
|
| 1436 |
+
$('headingVal').textContent = Math.round(heading) + 'Β°';
|
| 1437 |
+
$('headingNeedle').style.transform = `rotate(${heading}deg)`;
|
| 1438 |
+
}, 60);
|
| 1439 |
+
}
|
| 1440 |
+
|
| 1441 |
+
function stopDriveLoop() {
|
| 1442 |
+
if (driveInterval) { clearInterval(driveInterval); driveInterval = null; }
|
| 1443 |
+
if (sphero && isConnected) sphero.stop().catch(() => {});
|
| 1444 |
+
}
|
| 1445 |
+
|
| 1446 |
+
$('btnConnect').onclick = async () => {
|
| 1447 |
+
try {
|
| 1448 |
+
$('btnConnect').disabled = true;
|
| 1449 |
+
sphero = new SpheroMiniBLE();
|
| 1450 |
+
sphero.onDisconnect = () => {
|
| 1451 |
+
sysLog('Disconnected from Sphero.');
|
| 1452 |
+
stopDriveLoop();
|
| 1453 |
+
updateConnectionUI(false);
|
| 1454 |
+
sphero = null;
|
| 1455 |
+
};
|
| 1456 |
+
await sphero.connect();
|
| 1457 |
+
sysLog('Setting up...');
|
| 1458 |
+
await sphero.setMainLED(34, 211, 238);
|
| 1459 |
+
await sphero.resetYaw();
|
| 1460 |
+
heading = 0;
|
| 1461 |
+
updateConnectionUI(true);
|
| 1462 |
+
startDriveLoop();
|
| 1463 |
+
sysLog('Connected! Neuraxon brain online.');
|
| 1464 |
+
} catch (e) {
|
| 1465 |
+
sysLog(`Connection failed: ${e.message}`);
|
| 1466 |
+
updateConnectionUI(false);
|
| 1467 |
+
}
|
| 1468 |
+
};
|
| 1469 |
+
|
| 1470 |
+
$('btnDisconnect').onclick = async () => {
|
| 1471 |
+
sysLog('Disconnecting...');
|
| 1472 |
+
stopDriveLoop();
|
| 1473 |
+
if (sphero) {
|
| 1474 |
+
try { await sphero.setMainLED(0, 0, 0); } catch (e) {}
|
| 1475 |
+
await sphero.disconnect();
|
| 1476 |
+
}
|
| 1477 |
+
};
|
| 1478 |
+
|
| 1479 |
+
function updateConnectionUI(connected) {
|
| 1480 |
+
isConnected = connected;
|
| 1481 |
+
const s = $('connStatus');
|
| 1482 |
+
s.textContent = connected ? 'CONNECTED' : 'OFFLINE';
|
| 1483 |
+
s.className = 'status-badge ' + (connected ? 'on' : 'off');
|
| 1484 |
+
$('btnConnect').disabled = connected;
|
| 1485 |
+
$('btnDisconnect').disabled = !connected;
|
| 1486 |
+
}
|
| 1487 |
+
|
| 1488 |
+
// --- Controls ---
|
| 1489 |
+
function bindControls() {
|
| 1490 |
+
const dirs = { Up: 'up', Down: 'down', Left: 'left', Right: 'right' };
|
| 1491 |
+
for (const [btnSuffix, dir] of Object.entries(dirs)) {
|
| 1492 |
+
const btn = $('btn' + btnSuffix);
|
| 1493 |
+
const down = () => { activeKeys.add(dir); btn.classList.add('active'); };
|
| 1494 |
+
const up = () => { activeKeys.delete(dir); btn.classList.remove('active'); };
|
| 1495 |
+
btn.addEventListener('mousedown', down);
|
| 1496 |
+
btn.addEventListener('mouseup', up);
|
| 1497 |
+
btn.addEventListener('mouseleave', up);
|
| 1498 |
+
btn.addEventListener('touchstart', e => { e.preventDefault(); down(); });
|
| 1499 |
+
btn.addEventListener('touchend', e => { e.preventDefault(); up(); });
|
| 1500 |
+
}
|
| 1501 |
+
$('btnBrake').addEventListener('mousedown', () => activeKeys.clear());
|
| 1502 |
+
$('btnBrake').addEventListener('touchstart', e => { e.preventDefault(); activeKeys.clear(); });
|
| 1503 |
+
|
| 1504 |
+
document.addEventListener('keydown', e => {
|
| 1505 |
+
if (['ArrowUp','ArrowDown','ArrowLeft','ArrowRight',' ','w','a','s','d','W','A','S','D'].includes(e.key)) e.preventDefault();
|
| 1506 |
+
let dir = null;
|
| 1507 |
+
if (e.key === 'ArrowUp' || e.key.toLowerCase() === 'w') dir = 'up';
|
| 1508 |
+
else if (e.key === 'ArrowDown' || e.key.toLowerCase() === 's') dir = 'down';
|
| 1509 |
+
else if (e.key === 'ArrowLeft' || e.key.toLowerCase() === 'a') dir = 'left';
|
| 1510 |
+
else if (e.key === 'ArrowRight' || e.key.toLowerCase() === 'd') dir = 'right';
|
| 1511 |
+
else if (e.key === ' ') { activeKeys.clear(); return; }
|
| 1512 |
+
if (dir && !activeKeys.has(dir)) {
|
| 1513 |
+
activeKeys.add(dir);
|
| 1514 |
+
const btn = $('btn' + dir.charAt(0).toUpperCase() + dir.slice(1));
|
| 1515 |
+
if (btn) btn.classList.add('active');
|
| 1516 |
+
}
|
| 1517 |
+
});
|
| 1518 |
+
document.addEventListener('keyup', e => {
|
| 1519 |
+
let dir = null;
|
| 1520 |
+
if (e.key === 'ArrowUp' || e.key.toLowerCase() === 'w') dir = 'up';
|
| 1521 |
+
else if (e.key === 'ArrowDown' || e.key.toLowerCase() === 's') dir = 'down';
|
| 1522 |
+
else if (e.key === 'ArrowLeft' || e.key.toLowerCase() === 'a') dir = 'left';
|
| 1523 |
+
else if (e.key === 'ArrowRight' || e.key.toLowerCase() === 'd') dir = 'right';
|
| 1524 |
+
if (dir) {
|
| 1525 |
+
activeKeys.delete(dir);
|
| 1526 |
+
const btn = $('btn' + dir.charAt(0).toUpperCase() + dir.slice(1));
|
| 1527 |
+
if (btn) btn.classList.remove('active');
|
| 1528 |
+
}
|
| 1529 |
+
});
|
| 1530 |
+
}
|
| 1531 |
+
bindControls();
|
| 1532 |
+
|
| 1533 |
+
|
| 1534 |
+
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1535 |
+
// ENHANCED INTERACTIVE VISUALIZATION
|
| 1536 |
+
// β Continuous Panning, Proximity Focus, Polar Layout
|
| 1537 |
+
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1538 |
+
|
| 1539 |
+
const canvas = $('neuralCanvas');
|
| 1540 |
+
const ctx = canvas.getContext('2d');
|
| 1541 |
+
const oscCanvas = $('oscCanvas');
|
| 1542 |
+
const oscCtx = oscCanvas.getContext('2d');
|
| 1543 |
+
const actCanvas = $('activityCanvas');
|
| 1544 |
+
const actCtx = actCanvas.getContext('2d');
|
| 1545 |
+
|
| 1546 |
+
// ββ Camera / Pan State ββ
|
| 1547 |
+
let camX = 0, camY = 0; // world offset (panning)
|
| 1548 |
+
let camZoom = 1.0;
|
| 1549 |
+
let isDragging = false;
|
| 1550 |
+
let dragStartX = 0, dragStartY = 0;
|
| 1551 |
+
let dragStartCamX = 0, dragStartCamY = 0;
|
| 1552 |
+
let hasDragged = false; // track if user has interacted
|
| 1553 |
+
|
| 1554 |
+
// ββ Focused Node State ββ
|
| 1555 |
+
let focusedNodeId = -1;
|
| 1556 |
+
let focusAnimT = 0; // animation interpolant
|
| 1557 |
+
|
| 1558 |
+
// ββ Synapse Pulse Particles ββ
|
| 1559 |
+
const pulseParticles = [];
|
| 1560 |
+
const MAX_PARTICLES = 80;
|
| 1561 |
+
|
| 1562 |
+
function resizeCanvas() {
|
| 1563 |
+
const rect = canvas.parentElement.getBoundingClientRect();
|
| 1564 |
+
canvas.width = rect.width * window.devicePixelRatio;
|
| 1565 |
+
canvas.height = rect.height * window.devicePixelRatio;
|
| 1566 |
+
ctx.setTransform(window.devicePixelRatio, 0, 0, window.devicePixelRatio, 0, 0);
|
| 1567 |
+
}
|
| 1568 |
+
window.addEventListener('resize', resizeCanvas);
|
| 1569 |
+
resizeCanvas();
|
| 1570 |
+
|
| 1571 |
+
// ββ Mouse / Touch Handlers for Panning ββ
|
| 1572 |
+
const panelCenter = $('panelCenter');
|
| 1573 |
+
|
| 1574 |
+
panelCenter.addEventListener('mousedown', e => {
|
| 1575 |
+
if (e.target !== canvas) return;
|
| 1576 |
+
isDragging = true;
|
| 1577 |
+
dragStartX = e.clientX;
|
| 1578 |
+
dragStartY = e.clientY;
|
| 1579 |
+
dragStartCamX = camX;
|
| 1580 |
+
dragStartCamY = camY;
|
| 1581 |
+
});
|
| 1582 |
+
|
| 1583 |
+
window.addEventListener('mousemove', e => {
|
| 1584 |
+
if (!isDragging) return;
|
| 1585 |
+
const dx = e.clientX - dragStartX;
|
| 1586 |
+
const dy = e.clientY - dragStartY;
|
| 1587 |
+
camX = dragStartCamX + dx / camZoom;
|
| 1588 |
+
camY = dragStartCamY + dy / camZoom;
|
| 1589 |
+
if (!hasDragged && (Math.abs(dx) > 5 || Math.abs(dy) > 5)) {
|
| 1590 |
+
hasDragged = true;
|
| 1591 |
+
$('panHint').classList.add('hidden');
|
| 1592 |
+
}
|
| 1593 |
+
});
|
| 1594 |
+
|
| 1595 |
+
window.addEventListener('mouseup', () => { isDragging = false; });
|
| 1596 |
+
|
| 1597 |
+
// Zoom with scroll wheel
|
| 1598 |
+
panelCenter.addEventListener('wheel', e => {
|
| 1599 |
+
e.preventDefault();
|
| 1600 |
+
const delta = e.deltaY > 0 ? 0.92 : 1.08;
|
| 1601 |
+
camZoom = clamp(camZoom * delta, 0.3, 4.0);
|
| 1602 |
+
if (!hasDragged) { hasDragged = true; $('panHint').classList.add('hidden'); }
|
| 1603 |
+
}, { passive: false });
|
| 1604 |
+
|
| 1605 |
+
// Touch support for mobile
|
| 1606 |
+
let touchStartX = 0, touchStartY = 0, touchStartCamX = 0, touchStartCamY = 0;
|
| 1607 |
+
panelCenter.addEventListener('touchstart', e => {
|
| 1608 |
+
if (e.touches.length === 1) {
|
| 1609 |
+
isDragging = true;
|
| 1610 |
+
touchStartX = e.touches[0].clientX;
|
| 1611 |
+
touchStartY = e.touches[0].clientY;
|
| 1612 |
+
touchStartCamX = camX;
|
| 1613 |
+
touchStartCamY = camY;
|
| 1614 |
+
}
|
| 1615 |
+
}, { passive: true });
|
| 1616 |
+
panelCenter.addEventListener('touchmove', e => {
|
| 1617 |
+
if (!isDragging || e.touches.length !== 1) return;
|
| 1618 |
+
const dx = e.touches[0].clientX - touchStartX;
|
| 1619 |
+
const dy = e.touches[0].clientY - touchStartY;
|
| 1620 |
+
camX = touchStartCamX + dx / camZoom;
|
| 1621 |
+
camY = touchStartCamY + dy / camZoom;
|
| 1622 |
+
if (!hasDragged) { hasDragged = true; $('panHint').classList.add('hidden'); }
|
| 1623 |
+
}, { passive: true });
|
| 1624 |
+
panelCenter.addEventListener('touchend', () => { isDragging = false; });
|
| 1625 |
+
|
| 1626 |
+
|
| 1627 |
+
// ββ World-to-Screen coordinate transforms ββ
|
| 1628 |
+
function worldToScreen(wx, wy) {
|
| 1629 |
+
const w = canvas.width / window.devicePixelRatio;
|
| 1630 |
+
const h = canvas.height / window.devicePixelRatio;
|
| 1631 |
+
return {
|
| 1632 |
+
x: w / 2 + (wx + camX) * camZoom,
|
| 1633 |
+
y: h / 2 + (wy + camY) * camZoom,
|
| 1634 |
+
};
|
| 1635 |
+
}
|
| 1636 |
+
|
| 1637 |
+
function screenToWorld(sx, sy) {
|
| 1638 |
+
const w = canvas.width / window.devicePixelRatio;
|
| 1639 |
+
const h = canvas.height / window.devicePixelRatio;
|
| 1640 |
+
return {
|
| 1641 |
+
x: (sx - w / 2) / camZoom - camX,
|
| 1642 |
+
y: (sy - h / 2) / camZoom - camY,
|
| 1643 |
+
};
|
| 1644 |
+
}
|
| 1645 |
+
|
| 1646 |
+
// ββ Find the nearest node to the screen center (focal point) ββ
|
| 1647 |
+
function findFocalNode() {
|
| 1648 |
+
const w = canvas.width / window.devicePixelRatio;
|
| 1649 |
+
const h = canvas.height / window.devicePixelRatio;
|
| 1650 |
+
// Focal point is screen center
|
| 1651 |
+
const focal = screenToWorld(w / 2, h / 2);
|
| 1652 |
+
let minDist = Infinity;
|
| 1653 |
+
let nearestId = -1;
|
| 1654 |
+
for (const n of network.neurons) {
|
| 1655 |
+
const dx = n.x - focal.x;
|
| 1656 |
+
const dy = n.y - focal.y;
|
| 1657 |
+
const d = dx * dx + dy * dy;
|
| 1658 |
+
if (d < minDist) { minDist = d; nearestId = n.id; }
|
| 1659 |
+
}
|
| 1660 |
+
return nearestId;
|
| 1661 |
+
}
|
| 1662 |
+
|
| 1663 |
+
// ββ Update Focus HUD ββ
|
| 1664 |
+
function updateFocusHUD(nodeId) {
|
| 1665 |
+
const hud = $('focusHud');
|
| 1666 |
+
if (nodeId < 0) { hud.classList.remove('visible'); return; }
|
| 1667 |
+
hud.classList.add('visible');
|
| 1668 |
+
const n = network.neurons[nodeId];
|
| 1669 |
+
$('focusNodeId').textContent = '#' + n.id;
|
| 1670 |
+
$('focusNodeId').style.color = n.state === 1 ? '#22d3ee' : n.state === -1 ? '#f472b6' : '#475569';
|
| 1671 |
+
$('focusType').textContent = n.type.toUpperCase();
|
| 1672 |
+
$('focusType').style.color = n.type === 'input' ? '#fbbf24' : n.type === 'output' ? '#34d399' : '#7aa3c0';
|
| 1673 |
+
const stateLabel = n.state === 1 ? '+1 EXCITE' : n.state === -1 ? '-1 INHIBIT' : ' 0 NEUTRAL';
|
| 1674 |
+
$('focusState').textContent = stateLabel;
|
| 1675 |
+
$('focusState').style.color = n.state === 1 ? '#22d3ee' : n.state === -1 ? '#f472b6' : '#475569';
|
| 1676 |
+
$('focusS').textContent = n.s.toFixed(3);
|
| 1677 |
+
$('focusAdapt').textContent = n.adaptation.toFixed(4);
|
| 1678 |
+
$('focusHealth').textContent = (n.health * 100).toFixed(1) + '%';
|
| 1679 |
+
// Count synapses
|
| 1680 |
+
let synIn = 0, synOut = 0;
|
| 1681 |
+
for (const syn of network.synapses) {
|
| 1682 |
+
if (syn.postId === nodeId) synIn++;
|
| 1683 |
+
if (syn.preId === nodeId) synOut++;
|
| 1684 |
+
}
|
| 1685 |
+
$('focusSyn').textContent = `in:${synIn} out:${synOut}`;
|
| 1686 |
+
}
|
| 1687 |
+
|
| 1688 |
+
|
| 1689 |
+
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1690 |
+
// DRAW NETWORK β Enhanced Interactive Polar Visualization
|
| 1691 |
+
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1692 |
+
|
| 1693 |
+
const INPUT_LABELS = ['FWD', 'LEFT', 'RIGHT', 'BACK', 'EXPLORE', 'RHYTHM'];
|
| 1694 |
+
const OUTPUT_LABELS = ['SPD+', 'SPD-', 'TRNβ', 'TRNβ'];
|
| 1695 |
+
|
| 1696 |
+
function drawNetwork(stats) {
|
| 1697 |
+
const w = canvas.width / window.devicePixelRatio;
|
| 1698 |
+
const h = canvas.height / window.devicePixelRatio;
|
| 1699 |
+
ctx.clearRect(0, 0, w, h);
|
| 1700 |
+
|
| 1701 |
+
const time = Date.now() / 1000;
|
| 1702 |
+
|
| 1703 |
+
// ββ Background: Concentric polar grid rings ββ
|
| 1704 |
+
ctx.save();
|
| 1705 |
+
const center = worldToScreen(0, 0);
|
| 1706 |
+
const ringRadii = [80, 190, 230, 260, 350];
|
| 1707 |
+
for (const r of ringRadii) {
|
| 1708 |
+
const sr = r * camZoom;
|
| 1709 |
+
ctx.beginPath();
|
| 1710 |
+
ctx.arc(center.x, center.y, sr, 0, Math.PI * 2);
|
| 1711 |
+
ctx.strokeStyle = 'rgba(34,211,238,0.04)';
|
| 1712 |
+
ctx.lineWidth = 1;
|
| 1713 |
+
ctx.stroke();
|
| 1714 |
+
}
|
| 1715 |
+
// Radial spokes
|
| 1716 |
+
for (let i = 0; i < 12; i++) {
|
| 1717 |
+
const angle = (Math.PI * 2 * i) / 12;
|
| 1718 |
+
const outer = 400 * camZoom;
|
| 1719 |
+
ctx.beginPath();
|
| 1720 |
+
ctx.moveTo(center.x, center.y);
|
| 1721 |
+
ctx.lineTo(center.x + outer * Math.cos(angle), center.y + outer * Math.sin(angle));
|
| 1722 |
+
ctx.strokeStyle = 'rgba(34,211,238,0.02)';
|
| 1723 |
+
ctx.lineWidth = 0.5;
|
| 1724 |
+
ctx.stroke();
|
| 1725 |
+
}
|
| 1726 |
+
ctx.restore();
|
| 1727 |
+
|
| 1728 |
+
// ββ Find and update focal node ββ
|
| 1729 |
+
const newFocusId = findFocalNode();
|
| 1730 |
+
if (newFocusId !== focusedNodeId) {
|
| 1731 |
+
focusedNodeId = newFocusId;
|
| 1732 |
+
focusAnimT = 0;
|
| 1733 |
+
}
|
| 1734 |
+
focusAnimT = Math.min(1, focusAnimT + 0.08);
|
| 1735 |
+
updateFocusHUD(focusedNodeId);
|
| 1736 |
+
|
| 1737 |
+
// ββ Spawn pulse particles on active synapses ββ
|
| 1738 |
+
if (pulseParticles.length < MAX_PARTICLES && Math.random() < 0.3) {
|
| 1739 |
+
for (const syn of network.synapses) {
|
| 1740 |
+
if (syn.silent) continue;
|
| 1741 |
+
const pre = network.neurons[syn.preId];
|
| 1742 |
+
if (pre.state !== 0 && Math.random() < 0.008) {
|
| 1743 |
+
pulseParticles.push({
|
| 1744 |
+
preId: syn.preId, postId: syn.postId,
|
| 1745 |
+
t: 0, speed: 0.015 + Math.random() * 0.02,
|
| 1746 |
+
color: pre.state === 1 ? [34,211,238] : [244,114,182],
|
| 1747 |
+
});
|
| 1748 |
+
if (pulseParticles.length >= MAX_PARTICLES) break;
|
| 1749 |
+
}
|
| 1750 |
+
}
|
| 1751 |
+
}
|
| 1752 |
+
|
| 1753 |
+
// ββ Draw Synapses ββ
|
| 1754 |
+
ctx.lineWidth = 0.5;
|
| 1755 |
+
for (const syn of network.synapses) {
|
| 1756 |
+
if (syn.silent) continue;
|
| 1757 |
+
const pre = network.neurons[syn.preId];
|
| 1758 |
+
const post = network.neurons[syn.postId];
|
| 1759 |
+
const p1 = worldToScreen(pre.x, pre.y);
|
| 1760 |
+
const p2 = worldToScreen(post.x, post.y);
|
| 1761 |
+
const strength = Math.abs(syn.wFast + syn.wSlow);
|
| 1762 |
+
|
| 1763 |
+
// Highlight synapses connected to focused node
|
| 1764 |
+
const isFocusConn = (syn.preId === focusedNodeId || syn.postId === focusedNodeId);
|
| 1765 |
+
const alpha = isFocusConn ? 0.15 + strength * 0.4 : 0.02 + strength * 0.08;
|
| 1766 |
+
const lw = isFocusConn ? 1.2 : 0.5;
|
| 1767 |
+
|
| 1768 |
+
if (syn.wFast + syn.wSlow > 0) {
|
| 1769 |
+
ctx.strokeStyle = `rgba(34,211,238,${alpha})`;
|
| 1770 |
+
} else {
|
| 1771 |
+
ctx.strokeStyle = `rgba(244,114,182,${alpha})`;
|
| 1772 |
+
}
|
| 1773 |
+
ctx.lineWidth = lw;
|
| 1774 |
+
|
| 1775 |
+
const mx = (p1.x + p2.x) / 2 + (p2.y - p1.y) * 0.12;
|
| 1776 |
+
const my = (p1.y + p2.y) / 2 - (p2.x - p1.x) * 0.12;
|
| 1777 |
+
ctx.beginPath();
|
| 1778 |
+
ctx.moveTo(p1.x, p1.y);
|
| 1779 |
+
ctx.quadraticCurveTo(mx, my, p2.x, p2.y);
|
| 1780 |
+
ctx.stroke();
|
| 1781 |
+
}
|
| 1782 |
+
|
| 1783 |
+
// ββ Draw Pulse Particles ββ
|
| 1784 |
+
for (let i = pulseParticles.length - 1; i >= 0; i--) {
|
| 1785 |
+
const p = pulseParticles[i];
|
| 1786 |
+
p.t += p.speed;
|
| 1787 |
+
if (p.t >= 1) { pulseParticles.splice(i, 1); continue; }
|
| 1788 |
+
const pre = network.neurons[p.preId];
|
| 1789 |
+
const post = network.neurons[p.postId];
|
| 1790 |
+
const p1 = worldToScreen(pre.x, pre.y);
|
| 1791 |
+
const p2 = worldToScreen(post.x, post.y);
|
| 1792 |
+
const mx = (p1.x + p2.x) / 2 + (p2.y - p1.y) * 0.12;
|
| 1793 |
+
const my = (p1.y + p2.y) / 2 - (p2.x - p1.x) * 0.12;
|
| 1794 |
+
// Quadratic bezier interpolation
|
| 1795 |
+
const t = p.t;
|
| 1796 |
+
const u = 1 - t;
|
| 1797 |
+
const px = u * u * p1.x + 2 * u * t * mx + t * t * p2.x;
|
| 1798 |
+
const py = u * u * p1.y + 2 * u * t * my + t * t * p2.y;
|
| 1799 |
+
const [cr, cg, cb] = p.color;
|
| 1800 |
+
const pAlpha = Math.sin(t * Math.PI) * 0.8;
|
| 1801 |
+
ctx.beginPath();
|
| 1802 |
+
ctx.arc(px, py, 2 * camZoom, 0, Math.PI * 2);
|
| 1803 |
+
ctx.fillStyle = `rgba(${cr},${cg},${cb},${pAlpha})`;
|
| 1804 |
+
ctx.fill();
|
| 1805 |
+
}
|
| 1806 |
+
|
| 1807 |
+
// ββ Draw Neurons ββ
|
| 1808 |
+
for (const n of network.neurons) {
|
| 1809 |
+
const pos = worldToScreen(n.x, n.y);
|
| 1810 |
+
const isFocused = (n.id === focusedNodeId);
|
| 1811 |
+
const easeT = isFocused ? focusAnimT : 0;
|
| 1812 |
+
|
| 1813 |
+
// Base styling
|
| 1814 |
+
let color, glowColor;
|
| 1815 |
+
if (n.state === 1) {
|
| 1816 |
+
color = '#22d3ee'; glowColor = 'rgba(34,211,238,0.5)';
|
| 1817 |
+
} else if (n.state === -1) {
|
| 1818 |
+
color = '#f472b6'; glowColor = 'rgba(244,114,182,0.5)';
|
| 1819 |
+
} else {
|
| 1820 |
+
color = '#475569'; glowColor = 'rgba(71,85,105,0.15)';
|
| 1821 |
+
}
|
| 1822 |
+
|
| 1823 |
+
// Radius: focused nodes are much larger
|
| 1824 |
+
let baseR = n.type === 'input' ? 6 : n.type === 'output' ? 8 : 4.5;
|
| 1825 |
+
if (n.state !== 0) baseR += 1.5;
|
| 1826 |
+
const focusR = baseR * 2.5;
|
| 1827 |
+
const radius = (baseR + (focusR - baseR) * easeT) * camZoom;
|
| 1828 |
+
|
| 1829 |
+
// Glow effect
|
| 1830 |
+
if (n.state !== 0 || isFocused) {
|
| 1831 |
+
const glowR = radius * (isFocused ? 5 : 3.5);
|
| 1832 |
+
ctx.beginPath();
|
| 1833 |
+
const grad = ctx.createRadialGradient(pos.x, pos.y, 0, pos.x, pos.y, glowR);
|
| 1834 |
+
grad.addColorStop(0, isFocused ? 'rgba(251,191,36,0.2)' : glowColor);
|
| 1835 |
+
grad.addColorStop(1, 'transparent');
|
| 1836 |
+
ctx.fillStyle = grad;
|
| 1837 |
+
ctx.arc(pos.x, pos.y, glowR, 0, Math.PI * 2);
|
| 1838 |
+
ctx.fill();
|
| 1839 |
+
}
|
| 1840 |
+
|
| 1841 |
+
// Pulsing ring for active nodes
|
| 1842 |
+
if (n.state !== 0) {
|
| 1843 |
+
const pulseR = radius + Math.sin(time * 4 + n.id) * 2 * camZoom;
|
| 1844 |
+
ctx.beginPath();
|
| 1845 |
+
ctx.arc(pos.x, pos.y, pulseR + 3 * camZoom, 0, Math.PI * 2);
|
| 1846 |
+
ctx.strokeStyle = n.state === 1 ? 'rgba(34,211,238,0.15)' : 'rgba(244,114,182,0.15)';
|
| 1847 |
+
ctx.lineWidth = 1;
|
| 1848 |
+
ctx.stroke();
|
| 1849 |
+
}
|
| 1850 |
+
|
| 1851 |
+
// Body
|
| 1852 |
+
ctx.beginPath();
|
| 1853 |
+
ctx.arc(pos.x, pos.y, radius, 0, Math.PI * 2);
|
| 1854 |
+
ctx.fillStyle = color;
|
| 1855 |
+
ctx.fill();
|
| 1856 |
+
|
| 1857 |
+
// ββ FOCUSED STATE: yellow dashed border + metadata ββ
|
| 1858 |
+
if (isFocused) {
|
| 1859 |
+
ctx.save();
|
| 1860 |
+
ctx.beginPath();
|
| 1861 |
+
ctx.arc(pos.x, pos.y, radius + 4 * camZoom, 0, Math.PI * 2);
|
| 1862 |
+
ctx.setLineDash([4, 4]);
|
| 1863 |
+
ctx.strokeStyle = `rgba(251,191,36,${0.6 + 0.3 * Math.sin(time * 3)})`;
|
| 1864 |
+
ctx.lineWidth = 2;
|
| 1865 |
+
ctx.stroke();
|
| 1866 |
+
ctx.setLineDash([]);
|
| 1867 |
+
ctx.restore();
|
| 1868 |
+
|
| 1869 |
+
// Show full label near the node
|
| 1870 |
+
ctx.save();
|
| 1871 |
+
ctx.font = `bold ${11 * camZoom}px 'JetBrains Mono'`;
|
| 1872 |
+
ctx.textAlign = 'center';
|
| 1873 |
+
ctx.fillStyle = 'rgba(251,191,36,0.9)';
|
| 1874 |
+
let label = `#${n.id}`;
|
| 1875 |
+
if (n.type === 'input' && n.id < INPUT_LABELS.length) label = INPUT_LABELS[n.id];
|
| 1876 |
+
else if (n.type === 'output') label = OUTPUT_LABELS[n.id - network.nInput - network.nHidden] || `OUT${n.id}`;
|
| 1877 |
+
ctx.fillText(label, pos.x, pos.y - radius - 8 * camZoom);
|
| 1878 |
+
// State below
|
| 1879 |
+
ctx.font = `${9 * camZoom}px 'JetBrains Mono'`;
|
| 1880 |
+
ctx.fillStyle = color;
|
| 1881 |
+
const stateStr = n.state === 1 ? '+1' : n.state === -1 ? '-1' : '0';
|
| 1882 |
+
ctx.fillText(`s=${n.s.toFixed(2)} [${stateStr}]`, pos.x, pos.y + radius + 14 * camZoom);
|
| 1883 |
+
ctx.restore();
|
| 1884 |
+
} else {
|
| 1885 |
+
// ββ CONTEXT STATE: minimal ID label ββ
|
| 1886 |
+
// Type indicator ring
|
| 1887 |
+
if (n.type === 'input') {
|
| 1888 |
+
ctx.beginPath();
|
| 1889 |
+
ctx.arc(pos.x, pos.y, radius + 2.5 * camZoom, 0, Math.PI * 2);
|
| 1890 |
+
ctx.strokeStyle = 'rgba(251,191,36,0.4)';
|
| 1891 |
+
ctx.lineWidth = 1;
|
| 1892 |
+
ctx.stroke();
|
| 1893 |
+
} else if (n.type === 'output') {
|
| 1894 |
+
ctx.beginPath();
|
| 1895 |
+
ctx.arc(pos.x, pos.y, radius + 2.5 * camZoom, 0, Math.PI * 2);
|
| 1896 |
+
ctx.strokeStyle = 'rgba(52,211,153,0.4)';
|
| 1897 |
+
ctx.lineWidth = 1.5;
|
| 1898 |
+
ctx.stroke();
|
| 1899 |
+
}
|
| 1900 |
+
|
| 1901 |
+
// Small ID label only when zoomed in enough
|
| 1902 |
+
if (camZoom > 0.7) {
|
| 1903 |
+
ctx.font = `${Math.max(7, 8 * camZoom)}px 'JetBrains Mono'`;
|
| 1904 |
+
ctx.textAlign = 'center';
|
| 1905 |
+
ctx.textBaseline = 'middle';
|
| 1906 |
+
ctx.fillStyle = 'rgba(224,242,254,0.35)';
|
| 1907 |
+
ctx.fillText(n.id, pos.x, pos.y);
|
| 1908 |
+
}
|
| 1909 |
+
}
|
| 1910 |
+
}
|
| 1911 |
+
|
| 1912 |
+
// ββ Ring labels ββ
|
| 1913 |
+
ctx.save();
|
| 1914 |
+
ctx.font = `${9 * camZoom}px 'JetBrains Mono'`;
|
| 1915 |
+
ctx.textAlign = 'center';
|
| 1916 |
+
ctx.fillStyle = 'rgba(251,191,36,0.35)';
|
| 1917 |
+
const lbl1 = worldToScreen(0, -55);
|
| 1918 |
+
ctx.fillText('INPUT', lbl1.x, lbl1.y);
|
| 1919 |
+
ctx.fillStyle = 'rgba(148,163,184,0.2)';
|
| 1920 |
+
const lbl2 = worldToScreen(0, -165);
|
| 1921 |
+
ctx.fillText('HIDDEN', lbl2.x, lbl2.y);
|
| 1922 |
+
ctx.fillStyle = 'rgba(52,211,153,0.3)';
|
| 1923 |
+
const lbl3 = worldToScreen(0, -380);
|
| 1924 |
+
ctx.fillText('OUTPUT', lbl3.x, lbl3.y);
|
| 1925 |
+
ctx.restore();
|
| 1926 |
+
|
| 1927 |
+
// ββ Focal Crosshair / Reticle ββ
|
| 1928 |
+
const cx = w / 2, cy = h / 2;
|
| 1929 |
+
ctx.save();
|
| 1930 |
+
ctx.strokeStyle = `rgba(251,191,36,${0.25 + 0.1 * Math.sin(time * 2)})`;
|
| 1931 |
+
ctx.lineWidth = 1;
|
| 1932 |
+
const crossLen = 12;
|
| 1933 |
+
const gap = 6;
|
| 1934 |
+
// Four short lines forming a crosshair
|
| 1935 |
+
ctx.beginPath();
|
| 1936 |
+
ctx.moveTo(cx - crossLen - gap, cy); ctx.lineTo(cx - gap, cy);
|
| 1937 |
+
ctx.moveTo(cx + gap, cy); ctx.lineTo(cx + crossLen + gap, cy);
|
| 1938 |
+
ctx.moveTo(cx, cy - crossLen - gap); ctx.lineTo(cx, cy - gap);
|
| 1939 |
+
ctx.moveTo(cx, cy + gap); ctx.lineTo(cx, cy + crossLen + gap);
|
| 1940 |
+
ctx.stroke();
|
| 1941 |
+
// Small rotating diamond
|
| 1942 |
+
ctx.translate(cx, cy);
|
| 1943 |
+
ctx.rotate(time * 0.5);
|
| 1944 |
+
ctx.strokeStyle = `rgba(251,191,36,${0.15 + 0.08 * Math.sin(time * 3)})`;
|
| 1945 |
+
ctx.beginPath();
|
| 1946 |
+
const d = 20;
|
| 1947 |
+
ctx.moveTo(0, -d); ctx.lineTo(d, 0); ctx.lineTo(0, d); ctx.lineTo(-d, 0); ctx.closePath();
|
| 1948 |
+
ctx.stroke();
|
| 1949 |
+
ctx.restore();
|
| 1950 |
+
}
|
| 1951 |
+
|
| 1952 |
+
|
| 1953 |
+
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1954 |
+
// OSCILLATOR + ACTIVITY TRACE (unchanged)
|
| 1955 |
+
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1956 |
+
|
| 1957 |
+
function drawOscillators() {
|
| 1958 |
+
const w = oscCanvas.width, h = oscCanvas.height;
|
| 1959 |
+
oscCtx.clearRect(0, 0, w, h);
|
| 1960 |
+
const bands = network.oscillators.bands;
|
| 1961 |
+
const colors = ['#3a5f7a', '#60a5fa', '#a78bfa', '#22d3ee', '#fbbf24'];
|
| 1962 |
+
const bandH = h / bands.length;
|
| 1963 |
+
for (let i = 0; i < bands.length; i++) {
|
| 1964 |
+
const b = bands[i];
|
| 1965 |
+
const y0 = i * bandH + bandH / 2;
|
| 1966 |
+
oscCtx.beginPath();
|
| 1967 |
+
oscCtx.strokeStyle = colors[i];
|
| 1968 |
+
oscCtx.lineWidth = 1.5;
|
| 1969 |
+
for (let x = 0; x < w; x++) {
|
| 1970 |
+
const t = x / w * Math.PI * 4;
|
| 1971 |
+
const val = Math.sin(b.phase + t) * b.amplitude * (bandH * 0.35);
|
| 1972 |
+
if (x === 0) oscCtx.moveTo(x, y0 + val);
|
| 1973 |
+
else oscCtx.lineTo(x, y0 + val);
|
| 1974 |
+
}
|
| 1975 |
+
oscCtx.stroke();
|
| 1976 |
+
oscCtx.font = '8px JetBrains Mono';
|
| 1977 |
+
oscCtx.fillStyle = colors[i];
|
| 1978 |
+
oscCtx.textAlign = 'left';
|
| 1979 |
+
oscCtx.fillText(b.name, 3, i * bandH + 10);
|
| 1980 |
+
}
|
| 1981 |
+
}
|
| 1982 |
+
|
| 1983 |
+
function drawActivityTrace() {
|
| 1984 |
+
const w = actCanvas.width, h = actCanvas.height;
|
| 1985 |
+
actCtx.clearRect(0, 0, w, h);
|
| 1986 |
+
if (activityHistory.length < 2) return;
|
| 1987 |
+
const step = w / MAX_HIST;
|
| 1988 |
+
actCtx.beginPath();
|
| 1989 |
+
actCtx.strokeStyle = 'rgba(34,211,238,0.7)';
|
| 1990 |
+
actCtx.lineWidth = 1;
|
| 1991 |
+
for (let i = 0; i < activityHistory.length; i++) {
|
| 1992 |
+
const x = i * step;
|
| 1993 |
+
const y = h - activityHistory[i].excFrac * h;
|
| 1994 |
+
if (i === 0) actCtx.moveTo(x, y); else actCtx.lineTo(x, y);
|
| 1995 |
+
}
|
| 1996 |
+
actCtx.stroke();
|
| 1997 |
+
actCtx.beginPath();
|
| 1998 |
+
actCtx.strokeStyle = 'rgba(244,114,182,0.7)';
|
| 1999 |
+
for (let i = 0; i < activityHistory.length; i++) {
|
| 2000 |
+
const x = i * step;
|
| 2001 |
+
const y = h - activityHistory[i].inhFrac * h;
|
| 2002 |
+
if (i === 0) actCtx.moveTo(x, y); else actCtx.lineTo(x, y);
|
| 2003 |
+
}
|
| 2004 |
+
actCtx.stroke();
|
| 2005 |
+
}
|
| 2006 |
+
|
| 2007 |
+
function updateReceptorBars(R) {
|
| 2008 |
+
const mapping = {
|
| 2009 |
+
D1: 'D1', D2: 'D2', SHT1A: '5HT1A', SHT2A: '5HT2A', SHT4: '5HT4',
|
| 2010 |
+
M1: 'M1', M2: 'M2', B1: '1', A2: '2',
|
| 2011 |
+
};
|
| 2012 |
+
for (const [key, name] of Object.entries(mapping)) {
|
| 2013 |
+
const val = R[key] || 0;
|
| 2014 |
+
const barId = 'rbar_' + name.replace(/[^a-zA-Z0-9]/g, '');
|
| 2015 |
+
const el = document.getElementById(barId);
|
| 2016 |
+
if (el) el.style.width = (val * 100) + '%';
|
| 2017 |
+
}
|
| 2018 |
+
}
|
| 2019 |
+
|
| 2020 |
+
|
| 2021 |
+
// ββββββββββββββββββββββββοΏ½οΏ½ββββββββββββββββββββββββββββββββββββββ
|
| 2022 |
+
// MAIN SIMULATION LOOP
|
| 2023 |
+
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 2024 |
+
|
| 2025 |
+
const SIM_DT = 16;
|
| 2026 |
+
|
| 2027 |
+
function simulationTick() {
|
| 2028 |
+
if (!simRunning) { requestAnimationFrame(simulationTick); return; }
|
| 2029 |
+
|
| 2030 |
+
const inputs = computeSensoryInputs();
|
| 2031 |
+
network.setInputs(inputs);
|
| 2032 |
+
const stats = network.simulateStep(SIM_DT);
|
| 2033 |
+
applyTeachingSignal();
|
| 2034 |
+
|
| 2035 |
+
activityHistory.push({
|
| 2036 |
+
excFrac: stats.excCount / network.N,
|
| 2037 |
+
inhFrac: stats.inhCount / network.N,
|
| 2038 |
+
});
|
| 2039 |
+
if (activityHistory.length > MAX_HIST) activityHistory.shift();
|
| 2040 |
+
|
| 2041 |
+
// Update stats UI
|
| 2042 |
+
$('statExc').textContent = stats.excCount;
|
| 2043 |
+
$('statInh').textContent = stats.inhCount;
|
| 2044 |
+
$('statNeu').textContent = stats.neuCount;
|
| 2045 |
+
$('statSyn').textContent = network.synapses.length;
|
| 2046 |
+
$('stepCount').textContent = network.step;
|
| 2047 |
+
$('energyVal').textContent = network.energy.toFixed(2);
|
| 2048 |
+
$('statDw').textContent = network.meanDw.toFixed(4);
|
| 2049 |
+
$('statStruct').textContent = network.structEvent || (teachingActive ? (idleTicks > IDLE_TEACH_DELAY ? 'STOP-LEARN' : 'LEARNING') : 'stable');
|
| 2050 |
+
$('statStruct').style.color = teachingActive ? (idleTicks > IDLE_TEACH_DELAY ? 'var(--red)' : 'var(--da-color)') : 'var(--green)';
|
| 2051 |
+
$('statLearn').textContent = learnCount;
|
| 2052 |
+
$('statAccum').textContent = outputAccum.map(v => v.toFixed(1)).join(' ');
|
| 2053 |
+
|
| 2054 |
+
const pct = clamp(learnAccuracy, 0, 100);
|
| 2055 |
+
$('learnPct').textContent = pct + '%';
|
| 2056 |
+
$('learnBar').style.width = pct + '%';
|
| 2057 |
+
$('learnBar').className = 'progress-fill' + (pct > 60 ? ' learned' : '');
|
| 2058 |
+
if (learnCount === 0) {
|
| 2059 |
+
$('learnBarText').textContent = 'Drive in MANUAL to teach';
|
| 2060 |
+
} else if (pct < 30) {
|
| 2061 |
+
$('learnBarText').textContent = 'Learning... drive, turn & stop!';
|
| 2062 |
+
} else if (pct < 60) {
|
| 2063 |
+
$('learnBarText').textContent = 'Getting better β try HYBRID';
|
| 2064 |
+
} else {
|
| 2065 |
+
$('learnBarText').textContent = 'Well trained! Neuraxon ready';
|
| 2066 |
+
}
|
| 2067 |
+
|
| 2068 |
+
// Source tags
|
| 2069 |
+
const tagH = $('tagHuman');
|
| 2070 |
+
const tagN = $('tagNxon');
|
| 2071 |
+
const tagL = $('tagLearn');
|
| 2072 |
+
const srcDetail = $('sourceDetail');
|
| 2073 |
+
|
| 2074 |
+
if (controlMode === 'manual') {
|
| 2075 |
+
tagH.style.opacity = activeKeys.size > 0 ? '1' : '0.5';
|
| 2076 |
+
tagN.style.opacity = '0.2';
|
| 2077 |
+
tagL.style.opacity = teachingActive ? '1' : '0.2';
|
| 2078 |
+
srcDetail.textContent = teachingActive ? 'Teaching network from your input' : 'Manual control β press keys to teach';
|
| 2079 |
+
$('blendVal').textContent = 'H:100% N:0%';
|
| 2080 |
+
} else if (controlMode === 'hybrid') {
|
| 2081 |
+
tagH.style.opacity = activeKeys.size > 0 ? '1' : '0.4';
|
| 2082 |
+
tagN.style.opacity = nxonContributing ? '1' : '0.3';
|
| 2083 |
+
tagL.style.opacity = teachingActive ? '1' : '0.2';
|
| 2084 |
+
srcDetail.textContent = nxonContributing
|
| 2085 |
+
? (teachingActive ? 'Both active β learning + Neuraxon assisting' : 'Neuraxon gently assisting (25%)')
|
| 2086 |
+
: (teachingActive ? 'Teaching from your input' : 'Hybrid idle β you lead, Neuraxon assists');
|
| 2087 |
+
$('blendVal').textContent = 'H:75% N:25%';
|
| 2088 |
+
}
|
| 2089 |
+
|
| 2090 |
+
// Motor display
|
| 2091 |
+
const ai = getNeuraxonAction();
|
| 2092 |
+
const human = getHumanAction();
|
| 2093 |
+
let dispSpeed, dispTurn;
|
| 2094 |
+
if (controlMode === 'manual') {
|
| 2095 |
+
dispSpeed = Math.abs(human.speed);
|
| 2096 |
+
dispTurn = human.turn;
|
| 2097 |
+
} else if (controlMode === 'hybrid') {
|
| 2098 |
+
dispSpeed = Math.round(Math.abs(human.speed) * 0.75 + ai.speed * 0.25);
|
| 2099 |
+
dispTurn = human.turn * 0.75 + ai.turn * 0.25;
|
| 2100 |
+
} else {
|
| 2101 |
+
dispSpeed = ai.speed;
|
| 2102 |
+
dispTurn = ai.turn;
|
| 2103 |
+
}
|
| 2104 |
+
if (!isConnected) {
|
| 2105 |
+
heading = (heading + dispTurn + 360) % 360;
|
| 2106 |
+
}
|
| 2107 |
+
$('speedVal').textContent = Math.round(dispSpeed);
|
| 2108 |
+
$('headingVal').textContent = Math.round(heading) + 'Β°';
|
| 2109 |
+
$('headingNeedle').style.transform = `rotate(${heading}deg)`;
|
| 2110 |
+
|
| 2111 |
+
// Draw visuals
|
| 2112 |
+
drawNetwork(stats);
|
| 2113 |
+
drawOscillators();
|
| 2114 |
+
drawActivityTrace();
|
| 2115 |
+
updateReceptorBars(stats.R);
|
| 2116 |
+
|
| 2117 |
+
requestAnimationFrame(simulationTick);
|
| 2118 |
+
}
|
| 2119 |
+
|
| 2120 |
+
// Start
|
| 2121 |
+
sysLog('Neuraxon 2.0 brain initialized.');
|
| 2122 |
+
sysLog('6 input β 24 hidden β 4 output neurons');
|
| 2123 |
+
sysLog(`${network.synapses.length} synapses (small-world + motor pathways)`);
|
| 2124 |
+
sysLog('DA-gated STDP learning with 9 receptor subtypes');
|
| 2125 |
+
sysLog('Learns: forward, turns, AND stopping');
|
| 2126 |
+
sysLog('Replay buffer: Neuraxon replays your moves');
|
| 2127 |
+
sysLog('βββββββββββββββββββββββββββββββββββββββ');
|
| 2128 |
+
sysLog('HOW TO USE:');
|
| 2129 |
+
sysLog('1. MANUAL mode β drive with WASD/arrows');
|
| 2130 |
+
sysLog(' β Network learns from every keypress');
|
| 2131 |
+
sysLog(' β Watch accuracy % rise in LEARNING panel');
|
| 2132 |
+
sysLog(' β Gold LED = learning in progress');
|
| 2133 |
+
sysLog('2. HYBRID mode β 75% you + 25% Neuraxon');
|
| 2134 |
+
sysLog(' β Neuraxon gently assists, you lead');
|
| 2135 |
+
sysLog(' β Still learns when you press keys');
|
| 2136 |
+
sysLog('TIP: Drive fwd, turn, AND STOP to teach');
|
| 2137 |
+
sysLog(' the full repertoire of behaviors');
|
| 2138 |
+
sysLog('ββββββββββββοΏ½οΏ½ββββββββββββββββββββββββββ');
|
| 2139 |
+
sysLog('NEURAL GRAPH: Drag to pan, scroll to zoom');
|
| 2140 |
+
sysLog('Nearest node auto-focuses with metadata');
|
| 2141 |
+
sysLog('βββββββββββββββββββββββββββββββββββββββ');
|
| 2142 |
+
requestAnimationFrame(simulationTick);
|
| 2143 |
+
|
| 2144 |
+
</script>
|
| 2145 |
+
</body>
|
| 2146 |
+
</html>
|