diff --git a/brainsnn-r3f-app/src/app/GaugeGapLanding.jsx b/brainsnn-r3f-app/src/app/GaugeGapLanding.jsx
index e9508bb..5ae2eb5 100644
--- a/brainsnn-r3f-app/src/app/GaugeGapLanding.jsx
+++ b/brainsnn-r3f-app/src/app/GaugeGapLanding.jsx
@@ -4,16 +4,20 @@ import {
BrainCircuit,
CheckCircle2,
ChevronDown,
+ CircleDot,
Dna,
FlaskConical,
Gauge,
Layers3,
Microscope,
+ Orbit,
Play,
Share2,
+ Shield,
Sparkles,
WandSparkles,
Waves,
+ Wind,
Zap,
} from 'lucide-react';
import { Button } from '../components/ui/Button.jsx';
@@ -56,6 +60,38 @@ const LABS = [
icon: Dna,
action: 'Grow a species',
},
+ {
+ id: 'gravity',
+ eyebrow: 'Live experiment 005',
+ title: 'Gravity Forge',
+ description: 'Launch planets into a live n-body field and build an orbital system that does not destroy itself.',
+ icon: Orbit,
+ action: 'Build a system',
+ },
+ {
+ id: 'flock',
+ eyebrow: 'Live experiment 006',
+ title: 'Flock Mind',
+ description: 'Steer a leaderless crowd as predator or beacon while local rules create group intelligence.',
+ icon: Wind,
+ action: 'Steer emergence',
+ },
+ {
+ id: 'outbreak',
+ eyebrow: 'Live experiment 007',
+ title: 'Outbreak Zero',
+ description: 'Pick patient zero, spend twelve vaccines and interrupt the contact graph before it turns red.',
+ icon: Shield,
+ action: 'Contain it',
+ },
+ {
+ id: 'pendulum',
+ eyebrow: 'Live experiment 008',
+ title: 'Chaos Twins',
+ description: 'Race two nearly identical double pendulums and measure when their futures stop agreeing.',
+ icon: CircleDot,
+ action: 'Race the futures',
+ },
{
id: 'soliton',
eyebrow: 'Research engine',
@@ -80,7 +116,7 @@ const LOOP = [
{ number: '03', title: 'Publish', text: 'Export the visual, the challenge link and the explanation from one run.' },
];
-const ARCADE_IDS = new Set(['attractor', 'fireflies', 'waves', 'reaction']);
+const ARCADE_IDS = new Set(['attractor', 'fireflies', 'waves', 'reaction', 'gravity', 'flock', 'outbreak', 'pendulum']);
export function GaugeGapLanding({ onStart, onNavigate, onOpenReconstruct }) {
useEffect(() => {
@@ -140,16 +176,16 @@ export function GaugeGapLanding({ onStart, onNavigate, onOpenReconstruct }) {
Science you can touch, remix and publish
Don’t just learn the universe. Play with it.
- Four live experiments turn difficult science into visual games and shareable challenges.
- Shape chaos, synchronize a crowd, erase a wave or draw a new species.
+ Eight live experiments turn difficult science into visual games and shareable challenges.
+ Shape chaos, build orbits, steer a flock, contain an outbreak and race competing futures.
Enter the science arcade
onNavigate?.('research')}>See the research layer
-
4 live Distinct experiments
-
1-click Challenge links
+
8 live Distinct experiments
+
8 verbs Different ways to play
Exact Shareable run states
Open Equations and limits
@@ -182,8 +218,8 @@ export function GaugeGapLanding({ onStart, onNavigate, onOpenReconstruct }) {
Find the most beautiful edge of chaos.
-
Then try three completely different systems
-
4 labs
+
Then jump into seven completely different systems
+
8 labs
Accept challenge
@@ -280,7 +316,7 @@ export function GaugeGapLanding({ onStart, onNavigate, onOpenReconstruct }) {
- Four worlds are already running
+ Eight worlds are already running
Pick a system. Break its rules. Share what appears.
The next person should arrive through your challenge—not through another explanation page.
Enter the science arcade
diff --git a/brainsnn-r3f-app/src/features/gaugegap/AgentPlaygrounds.jsx b/brainsnn-r3f-app/src/features/gaugegap/AgentPlaygrounds.jsx
new file mode 100644
index 0000000..3065565
--- /dev/null
+++ b/brainsnn-r3f-app/src/features/gaugegap/AgentPlaygrounds.jsx
@@ -0,0 +1,511 @@
+import React, { useEffect, useMemo, useRef, useState } from 'react';
+import { Crosshair, Shield, Target, Wind } from 'lucide-react';
+import {
+ clamp,
+ copyChallenge,
+ DeepLabActions,
+ DeepLabHeading,
+ round,
+ seededRandom,
+ shareChallenge,
+} from './DeepLabChrome.jsx';
+
+function makeFlock(count, width, height, seed = 42) {
+ const random = seededRandom(seed);
+ return Array.from({ length: count }, (_, index) => {
+ const angle = random() * Math.PI * 2;
+ const speed = 0.7 + random();
+ return {
+ x: random() * width,
+ y: random() * height,
+ vx: Math.cos(angle) * speed,
+ vy: Math.sin(angle) * speed,
+ hue: 176 + (index % 70),
+ };
+ });
+}
+
+function parseFlockState() {
+ if (typeof window === 'undefined') return null;
+ const query = new URLSearchParams(window.location.search);
+ if (query.get('lab') !== 'flock') return null;
+ const [countValue, alignmentValue, cohesionValue, separationValue, modeValue] = (query.get('state') || '').split(',');
+ const values = [countValue, alignmentValue, cohesionValue, separationValue].map(Number);
+ if (!values.every(Number.isFinite)) return null;
+ return {
+ count: clamp(values[0], 40, 220),
+ alignment: clamp(values[1], 0, 1.4),
+ cohesion: clamp(values[2], 0, 1.4),
+ separation: clamp(values[3], 0, 1.5),
+ mode: modeValue === 'beacon' ? 'beacon' : 'predator',
+ };
+}
+
+export function FlockMindLab() {
+ const shared = useMemo(() => parseFlockState(), []);
+ const [count, setCount] = useState(shared?.count || 120);
+ const [alignment, setAlignment] = useState(shared?.alignment || 0.72);
+ const [cohesion, setCohesion] = useState(shared?.cohesion || 0.54);
+ const [separation, setSeparation] = useState(shared?.separation || 0.86);
+ const [mode, setMode] = useState(shared?.mode || 'predator');
+ const [paused, setPaused] = useState(false);
+ const [coherence, setCoherence] = useState(0);
+ const [notice, setNotice] = useState(shared ? 'Shared flock loaded. Move over the field to test it.' : 'Move across the field. The flock treats you as a predator.');
+ const canvasRef = useRef(null);
+ const birdsRef = useRef([]);
+ const controlsRef = useRef({ count, alignment, cohesion, separation, mode });
+ const pointerRef = useRef({ x: -9999, y: -9999, active: false });
+ const pausedRef = useRef(paused);
+ const resetRef = useRef(0);
+
+ useEffect(() => { controlsRef.current = { count, alignment, cohesion, separation, mode }; }, [count, alignment, cohesion, separation, mode]);
+ useEffect(() => { pausedRef.current = paused; }, [paused]);
+
+ useEffect(() => {
+ const canvas = canvasRef.current;
+ if (!canvas) return undefined;
+ const context = canvas.getContext('2d');
+ if (!context) return undefined;
+ let width = 0;
+ let height = 0;
+ let animationId = 0;
+ let frameCount = 0;
+ let lastReset = -1;
+
+ function resize() {
+ const rect = canvas.getBoundingClientRect();
+ const ratio = Math.min(window.devicePixelRatio || 1, 2);
+ width = Math.max(320, rect.width);
+ height = Math.max(360, rect.height);
+ canvas.width = Math.floor(width * ratio);
+ canvas.height = Math.floor(height * ratio);
+ context.setTransform(ratio, 0, 0, ratio, 0, 0);
+ birdsRef.current = makeFlock(controlsRef.current.count, width, height, 91);
+ }
+
+ function pointer(event) {
+ const rect = canvas.getBoundingClientRect();
+ pointerRef.current = { x: event.clientX - rect.left, y: event.clientY - rect.top, active: true };
+ }
+
+ function pointerLeave() {
+ pointerRef.current.active = false;
+ }
+
+ const observer = new ResizeObserver(resize);
+ observer.observe(canvas);
+ canvas.addEventListener('pointermove', pointer);
+ canvas.addEventListener('pointerenter', pointer);
+ canvas.addEventListener('pointerleave', pointerLeave);
+ resize();
+
+ function render() {
+ if (lastReset !== resetRef.current || birdsRef.current.length !== controlsRef.current.count) {
+ lastReset = resetRef.current;
+ birdsRef.current = makeFlock(controlsRef.current.count, width, height, Date.now() % 100000);
+ }
+
+ context.fillStyle = 'rgba(2, 5, 12, 0.3)';
+ context.fillRect(0, 0, width, height);
+ const birds = birdsRef.current;
+ const controls = controlsRef.current;
+
+ if (!pausedRef.current) {
+ for (let index = 0; index < birds.length; index += 1) {
+ const bird = birds[index];
+ let neighbors = 0;
+ let avgVx = 0;
+ let avgVy = 0;
+ let centerX = 0;
+ let centerY = 0;
+ let avoidX = 0;
+ let avoidY = 0;
+
+ for (let otherIndex = 0; otherIndex < birds.length; otherIndex += 1) {
+ if (otherIndex === index) continue;
+ const other = birds[otherIndex];
+ let dx = other.x - bird.x;
+ let dy = other.y - bird.y;
+ if (Math.abs(dx) > width / 2) dx -= Math.sign(dx) * width;
+ if (Math.abs(dy) > height / 2) dy -= Math.sign(dy) * height;
+ const distanceSquared = dx * dx + dy * dy;
+ if (distanceSquared < 4200) {
+ neighbors += 1;
+ avgVx += other.vx;
+ avgVy += other.vy;
+ centerX += dx;
+ centerY += dy;
+ if (distanceSquared < 420) {
+ const scale = 1 / Math.max(1, distanceSquared);
+ avoidX -= dx * scale;
+ avoidY -= dy * scale;
+ }
+ }
+ }
+
+ if (neighbors) {
+ avgVx /= neighbors;
+ avgVy /= neighbors;
+ centerX /= neighbors;
+ centerY /= neighbors;
+ bird.vx += (avgVx - bird.vx) * 0.012 * controls.alignment;
+ bird.vy += (avgVy - bird.vy) * 0.012 * controls.alignment;
+ bird.vx += centerX * 0.00018 * controls.cohesion;
+ bird.vy += centerY * 0.00018 * controls.cohesion;
+ bird.vx += avoidX * 2.8 * controls.separation;
+ bird.vy += avoidY * 2.8 * controls.separation;
+ }
+
+ if (pointerRef.current.active) {
+ const dx = pointerRef.current.x - bird.x;
+ const dy = pointerRef.current.y - bird.y;
+ const distanceSquared = Math.max(120, dx * dx + dy * dy);
+ const direction = controls.mode === 'beacon' ? 1 : -1;
+ bird.vx += direction * dx * 18 / distanceSquared;
+ bird.vy += direction * dy * 18 / distanceSquared;
+ }
+
+ const speed = Math.hypot(bird.vx, bird.vy);
+ const maxSpeed = 2.45;
+ if (speed > maxSpeed) {
+ bird.vx = bird.vx / speed * maxSpeed;
+ bird.vy = bird.vy / speed * maxSpeed;
+ }
+ bird.x = (bird.x + bird.vx + width) % width;
+ bird.y = (bird.y + bird.vy + height) % height;
+ }
+ }
+
+ let directionX = 0;
+ let directionY = 0;
+ birds.forEach((bird) => {
+ const angle = Math.atan2(bird.vy, bird.vx);
+ directionX += Math.cos(angle);
+ directionY += Math.sin(angle);
+ context.save();
+ context.translate(bird.x, bird.y);
+ context.rotate(angle);
+ context.fillStyle = `hsla(${bird.hue}, 90%, 68%, 0.82)`;
+ context.beginPath();
+ context.moveTo(7, 0);
+ context.lineTo(-4, 3.2);
+ context.lineTo(-2.2, 0);
+ context.lineTo(-4, -3.2);
+ context.closePath();
+ context.fill();
+ context.restore();
+ });
+
+ if (frameCount % 20 === 0) setCoherence(Math.round(Math.hypot(directionX, directionY) / Math.max(1, birds.length) * 100));
+ frameCount += 1;
+ animationId = window.requestAnimationFrame(render);
+ }
+
+ animationId = window.requestAnimationFrame(render);
+ return () => {
+ observer.disconnect();
+ window.cancelAnimationFrame(animationId);
+ canvas.removeEventListener('pointermove', pointer);
+ canvas.removeEventListener('pointerenter', pointer);
+ canvas.removeEventListener('pointerleave', pointerLeave);
+ };
+ }, []);
+
+ function challengeUrl() {
+ const url = new URL(window.location.href);
+ url.searchParams.set('lab', 'flock');
+ url.searchParams.set('state', [count, round(alignment, 2), round(cohesion, 2), round(separation, 2), mode].join(','));
+ url.hash = 'playground';
+ return url.toString();
+ }
+
+ return (
+
+
82 ? 'Shared direction achieved' : 'Local rules still competing'}
+ />
+
+
+
+
Move over the canvas to influence the flock
+
+
+
+ setPaused((value) => !value)}
+ onReset={() => { resetRef.current += 1; setNotice('New flock generated from the same rules.'); }}
+ onShare={() => shareChallenge({ title: `My GaugeGap flock reached ${coherence}% coherence`, text: 'Can you make a leaderless flock coordinate better?', url: challengeUrl(), setNotice })}
+ onCopy={() => copyChallenge(challengeUrl(), setNotice)}
+ notice={notice}
+ />
+
+ );
+}
+
+function buildNetwork(count, width, height, seed) {
+ const random = seededRandom(seed);
+ const nodes = Array.from({ length: count }, (_, index) => ({
+ x: 38 + random() * Math.max(20, width - 76),
+ y: 38 + random() * Math.max(20, height - 76),
+ state: 'susceptible',
+ timer: 0,
+ id: index,
+ }));
+ const edges = [];
+ const keys = new Set();
+ for (let index = 0; index < count; index += 1) {
+ const distances = nodes
+ .map((node, other) => ({ other, distance: Math.hypot(node.x - nodes[index].x, node.y - nodes[index].y) }))
+ .filter((item) => item.other !== index)
+ .sort((a, b) => a.distance - b.distance)
+ .slice(0, 3);
+ distances.forEach(({ other }) => {
+ const key = index < other ? `${index}-${other}` : `${other}-${index}`;
+ if (!keys.has(key)) {
+ keys.add(key);
+ edges.push({ key, a: index, b: other });
+ }
+ });
+ }
+ return { nodes, edges };
+}
+
+function parseOutbreakState() {
+ if (typeof window === 'undefined') return null;
+ const query = new URLSearchParams(window.location.search);
+ if (query.get('lab') !== 'outbreak') return null;
+ const [transmissionValue, recoveryValue, contactsValue] = (query.get('state') || '').split(',').map(Number);
+ if (![transmissionValue, recoveryValue, contactsValue].every(Number.isFinite)) return null;
+ return {
+ transmission: clamp(transmissionValue, 0.08, 0.9),
+ recovery: clamp(recoveryValue, 2, 12),
+ contacts: clamp(contactsValue, 1, 4),
+ };
+}
+
+export function OutbreakZeroLab() {
+ const shared = useMemo(() => parseOutbreakState(), []);
+ const [transmission, setTransmission] = useState(shared?.transmission || 0.38);
+ const [recovery, setRecovery] = useState(shared?.recovery || 6);
+ const [contacts, setContacts] = useState(shared?.contacts || 1);
+ const [tool, setTool] = useState('infect');
+ const [paused, setPaused] = useState(false);
+ const [stats, setStats] = useState({ infected: 0, recovered: 0, immunized: 0, peak: 0 });
+ const [budget, setBudget] = useState(12);
+ const [notice, setNotice] = useState(shared ? 'Shared outbreak settings loaded. Choose patient zero.' : 'Choose patient zero, then use your limited vaccine budget.');
+ const canvasRef = useRef(null);
+ const networkRef = useRef({ nodes: [], edges: [] });
+ const controlsRef = useRef({ transmission, recovery, contacts });
+ const pausedRef = useRef(paused);
+ const budgetRef = useRef(budget);
+ const toolRef = useRef(tool);
+ const resetRef = useRef(0);
+
+ useEffect(() => { controlsRef.current = { transmission, recovery, contacts }; }, [transmission, recovery, contacts]);
+ useEffect(() => { pausedRef.current = paused; }, [paused]);
+ useEffect(() => { budgetRef.current = budget; }, [budget]);
+ useEffect(() => { toolRef.current = tool; }, [tool]);
+
+ useEffect(() => {
+ const canvas = canvasRef.current;
+ if (!canvas) return undefined;
+ const context = canvas.getContext('2d');
+ if (!context) return undefined;
+ let width = 0;
+ let height = 0;
+ let animationId = 0;
+ let frameCount = 0;
+ let accumulator = 0;
+ let lastTime = performance.now();
+ let lastReset = -1;
+
+ function resize() {
+ const rect = canvas.getBoundingClientRect();
+ const ratio = Math.min(window.devicePixelRatio || 1, 2);
+ width = Math.max(320, rect.width);
+ height = Math.max(360, rect.height);
+ canvas.width = Math.floor(width * ratio);
+ canvas.height = Math.floor(height * ratio);
+ context.setTransform(ratio, 0, 0, ratio, 0, 0);
+ networkRef.current = buildNetwork(62, width, height, 734);
+ }
+
+ function resetNetwork() {
+ networkRef.current = buildNetwork(62, width, height, Date.now() % 100000);
+ setBudget(12);
+ setStats({ infected: 0, recovered: 0, immunized: 0, peak: 0 });
+ }
+
+ const observer = new ResizeObserver(resize);
+ observer.observe(canvas);
+ resize();
+
+ function onClick(event) {
+ const rect = canvas.getBoundingClientRect();
+ const x = event.clientX - rect.left;
+ const y = event.clientY - rect.top;
+ const nearest = networkRef.current.nodes.reduce((best, candidate) => {
+ const distance = Math.hypot(candidate.x - x, candidate.y - y);
+ return distance < best.distance ? { node: candidate, distance } : best;
+ }, { node: null, distance: Infinity });
+ if (!nearest.node || nearest.distance > 20) return;
+ const node = nearest.node;
+ if (toolRef.current === 'infect' && node.state === 'susceptible') {
+ node.state = 'infected';
+ node.timer = 0;
+ setNotice('Patient zero selected. Switch to immunize and interrupt the graph.');
+ }
+ if (toolRef.current === 'immunize' && node.state === 'susceptible' && budgetRef.current > 0) {
+ node.state = 'immunized';
+ setBudget((value) => value - 1);
+ setNotice('Node immunized. Strategic bridge positions matter more than random coverage.');
+ }
+ }
+
+ canvas.addEventListener('click', onClick);
+
+ function step(delta) {
+ const { nodes, edges } = networkRef.current;
+ const active = controlsRef.current;
+ const newlyInfected = new Set();
+ edges.forEach((edge) => {
+ const a = nodes[edge.a];
+ const b = nodes[edge.b];
+ const attempts = Math.max(1, Math.round(active.contacts));
+ for (let attempt = 0; attempt < attempts; attempt += 1) {
+ const probability = active.transmission * delta * 0.48;
+ if (a.state === 'infected' && b.state === 'susceptible' && Math.random() < probability) newlyInfected.add(b);
+ if (b.state === 'infected' && a.state === 'susceptible' && Math.random() < probability) newlyInfected.add(a);
+ }
+ });
+ newlyInfected.forEach((node) => { node.state = 'infected'; node.timer = 0; });
+ nodes.forEach((node) => {
+ if (node.state === 'infected') {
+ node.timer += delta;
+ if (node.timer > active.recovery) node.state = 'recovered';
+ }
+ });
+ }
+
+ function render(now) {
+ if (lastReset !== resetRef.current) {
+ lastReset = resetRef.current;
+ resetNetwork();
+ }
+ const delta = Math.min(0.05, (now - lastTime) / 1000);
+ lastTime = now;
+ accumulator += delta;
+ if (!pausedRef.current && accumulator > 0.035) {
+ step(accumulator);
+ accumulator = 0;
+ }
+
+ context.fillStyle = '#02050d';
+ context.fillRect(0, 0, width, height);
+ const { nodes, edges } = networkRef.current;
+ edges.forEach((edge) => {
+ const a = nodes[edge.a];
+ const b = nodes[edge.b];
+ context.strokeStyle = a.state === 'infected' || b.state === 'infected' ? 'rgba(255, 88, 115, 0.28)' : 'rgba(125, 249, 255, 0.09)';
+ context.lineWidth = 1;
+ context.beginPath();
+ context.moveTo(a.x, a.y);
+ context.lineTo(b.x, b.y);
+ context.stroke();
+ });
+
+ const palette = { susceptible: '#5a7184', infected: '#ff5873', recovered: '#7df9ff', immunized: '#d9ff65' };
+ nodes.forEach((node) => {
+ context.shadowBlur = node.state === 'infected' ? 18 : 10;
+ context.shadowColor = palette[node.state];
+ context.fillStyle = palette[node.state];
+ context.beginPath();
+ context.arc(node.x, node.y, node.state === 'infected' ? 6 : 4.5, 0, Math.PI * 2);
+ context.fill();
+ context.shadowBlur = 0;
+ });
+
+ if (frameCount % 14 === 0) {
+ const infected = nodes.filter((node) => node.state === 'infected').length;
+ const recoveredCount = nodes.filter((node) => node.state === 'recovered').length;
+ const immunized = nodes.filter((node) => node.state === 'immunized').length;
+ setStats((current) => ({ infected, recovered: recoveredCount, immunized, peak: Math.max(current.peak, infected) }));
+ }
+ frameCount += 1;
+ animationId = window.requestAnimationFrame(render);
+ }
+
+ animationId = window.requestAnimationFrame(render);
+ return () => {
+ observer.disconnect();
+ canvas.removeEventListener('click', onClick);
+ window.cancelAnimationFrame(animationId);
+ };
+ }, []);
+
+ const containment = Math.round(clamp(100 - (stats.peak / 62) * 100 + budget * 1.2, 0, 100));
+
+ function challengeUrl() {
+ const url = new URL(window.location.href);
+ url.searchParams.set('lab', 'outbreak');
+ url.searchParams.set('state', [round(transmission, 2), recovery, contacts].join(','));
+ url.hash = 'playground';
+ return url.toString();
+ }
+
+ return (
+
+
+
+
+
+
Click nodes to {tool === 'infect' ? 'choose patient zero' : 'immunize'} · vaccines left: {budget}
+
+
+
+
setPaused((value) => !value)}
+ onReset={() => { resetRef.current += 1; setNotice('Fresh contact network generated. Choose a new patient zero.'); }}
+ onShare={() => shareChallenge({ title: `I contained GaugeGap outbreak at ${containment}%`, text: `Can you keep the peak below ${stats.peak} with twelve vaccines?`, url: challengeUrl(), setNotice })}
+ onCopy={() => copyChallenge(challengeUrl(), setNotice)}
+ notice={notice}
+ />
+
+ );
+}
diff --git a/brainsnn-r3f-app/src/features/gaugegap/DeepLabChrome.jsx b/brainsnn-r3f-app/src/features/gaugegap/DeepLabChrome.jsx
new file mode 100644
index 0000000..ed3b238
--- /dev/null
+++ b/brainsnn-r3f-app/src/features/gaugegap/DeepLabChrome.jsx
@@ -0,0 +1,69 @@
+import React from 'react';
+import { Copy, Pause, Play, RefreshCw, Share2, Sparkles } from 'lucide-react';
+
+export function clamp(value, min, max) {
+ return Math.min(max, Math.max(min, value));
+}
+
+export function round(value, places = 2) {
+ const factor = 10 ** places;
+ return Math.round(value * factor) / factor;
+}
+
+export function seededRandom(seedValue) {
+ let seed = seedValue >>> 0;
+ return () => {
+ seed += 0x6d2b79f5;
+ let value = seed;
+ value = Math.imul(value ^ (value >>> 15), value | 1);
+ value ^= value + Math.imul(value ^ (value >>> 7), value | 61);
+ return ((value ^ (value >>> 14)) >>> 0) / 4294967296;
+ };
+}
+
+export async function copyChallenge(url, setNotice) {
+ try {
+ await navigator.clipboard.writeText(url);
+ setNotice('Challenge link copied. The exact lab settings travel with it.');
+ } catch {
+ setNotice('Clipboard access was blocked. Use the native Share button.');
+ }
+}
+
+export async function shareChallenge({ title, text, url, setNotice }) {
+ try {
+ if (navigator.share) await navigator.share({ title, text, url });
+ else await copyChallenge(url, setNotice);
+ } catch (error) {
+ if (error?.name !== 'AbortError') setNotice('Sharing was blocked. Copy the challenge link instead.');
+ }
+}
+
+export function DeepLabHeading({ kicker, title, description, scoreLabel, score, scoreNote }) {
+ return (
+
+
+
{kicker}
+
{title}
+
{description}
+
+
+ {scoreLabel}
+ {score}
+ {scoreNote}
+
+
+ );
+}
+
+export function DeepLabActions({ paused, onPause, onReset, onShare, onCopy, notice }) {
+ return (
+
+
{paused ? : }{paused ? 'Resume' : 'Pause'}
+
Reset
+
Share challenge
+
Copy link
+
{notice}
+
+ );
+}
diff --git a/brainsnn-r3f-app/src/features/gaugegap/ExperimentArcade.jsx b/brainsnn-r3f-app/src/features/gaugegap/ExperimentArcade.jsx
index 713bee1..58b984a 100644
--- a/brainsnn-r3f-app/src/features/gaugegap/ExperimentArcade.jsx
+++ b/brainsnn-r3f-app/src/features/gaugegap/ExperimentArcade.jsx
@@ -1,8 +1,11 @@
import React, { useEffect, useMemo, useState } from 'react';
-import { Activity, Dna, Gauge, Shuffle, Sparkles, Waves } from 'lucide-react';
+import { Activity, CircleDot, Dna, Gauge, Orbit, Shield, Shuffle, Sparkles, Waves, Wind } from 'lucide-react';
import { AttractorPlayground } from './AttractorPlayground.jsx';
import { FireflySyncLab, ReactionDiffusionLab, WaveInterferenceLab } from './CollectivePlaygrounds.jsx';
+import { FlockMindLab, OutbreakZeroLab } from './AgentPlaygrounds.jsx';
+import { ChaosTwinsLab, GravityForgeLab } from './PhysicsPlaygrounds.jsx';
import '../../styles/arcade.css';
+import '../../styles/deep-arcade.css';
const EXPERIMENTS = [
{
@@ -45,6 +48,46 @@ const EXPERIMENTS = [
accent: 'pink',
mechanic: 'Create',
},
+ {
+ id: 'gravity',
+ number: '005',
+ title: 'Gravity Forge',
+ short: 'Build a solar system',
+ description: 'Launch planets into a live n-body field and see whether your orbital architecture survives.',
+ icon: Orbit,
+ accent: 'amber',
+ mechanic: 'Construct',
+ },
+ {
+ id: 'flock',
+ number: '006',
+ title: 'Flock Mind',
+ short: 'Steer emergence',
+ description: 'Act as predator or beacon while leaderless agents coordinate through only local rules.',
+ icon: Wind,
+ accent: 'blue',
+ mechanic: 'Influence',
+ },
+ {
+ id: 'outbreak',
+ number: '007',
+ title: 'Outbreak Zero',
+ short: 'Contain the network',
+ description: 'Choose patient zero, spend twelve vaccines and break the transmission graph before it turns red.',
+ icon: Shield,
+ accent: 'red',
+ mechanic: 'Intervene',
+ },
+ {
+ id: 'pendulum',
+ number: '008',
+ title: 'Chaos Twins',
+ short: 'Race two futures',
+ description: 'Start two double pendulums almost identically and measure how quickly their futures separate.',
+ icon: CircleDot,
+ accent: 'orange',
+ mechanic: 'Predict',
+ },
];
function initialExperiment() {
@@ -89,8 +132,8 @@ export function ExperimentArcade() {
GaugeGap science arcade
-
Four experiments. Four different ways to play.
-
Tune chaos, synchronize a crowd, erase a wave or draw living chemistry. Every lab starts in seconds and hides the real model one layer underneath.
+
Eight experiments. Eight different ways to think.
+
Tune, synchronize, explore, draw, construct, influence, intervene and predict. Every lab begins as play, then exposes the scientific model underneath.
Surprise me
@@ -128,15 +171,19 @@ export function ExperimentArcade() {
{active === 'fireflies' ? : null}
{active === 'waves' ? : null}
{active === 'reaction' ? : null}
+ {active === 'gravity' ? : null}
+ {active === 'flock' ? : null}
+ {active === 'outbreak' ? : null}
+ {active === 'pendulum' ? : null}
- What we borrowed from the best interactive-science sites
+ A complete science arcade needs
Immediate motion
- Recognizable presets
- One clear challenge
- Different interaction styles
- A shareable result
+ A clear mission
+ Different interaction verbs
+ A visible model
+ A shareable challenge
);
diff --git a/brainsnn-r3f-app/src/features/gaugegap/PhysicsPlaygrounds.jsx b/brainsnn-r3f-app/src/features/gaugegap/PhysicsPlaygrounds.jsx
new file mode 100644
index 0000000..6dfa461
--- /dev/null
+++ b/brainsnn-r3f-app/src/features/gaugegap/PhysicsPlaygrounds.jsx
@@ -0,0 +1,507 @@
+import React, { useEffect, useMemo, useRef, useState } from 'react';
+import { CircleDot, MousePointer2, Orbit, Target } from 'lucide-react';
+import {
+ clamp,
+ copyChallenge,
+ DeepLabActions,
+ DeepLabHeading,
+ round,
+ shareChallenge,
+} from './DeepLabChrome.jsx';
+
+const ORBIT_PRESETS = {
+ solar: {
+ label: 'Tiny solar system',
+ gravity: 0.75,
+ bodies: [
+ { x: 0.5, y: 0.5, vx: 0, vy: 0, mass: 640, radius: 13, hue: 48 },
+ { x: 0.5, y: 0.29, vx: 1.5, vy: 0, mass: 5, radius: 4.5, hue: 188 },
+ { x: 0.5, y: 0.76, vx: -1.28, vy: 0, mass: 9, radius: 5.5, hue: 310 },
+ { x: 0.23, y: 0.5, vx: 0, vy: -1.34, mass: 3.5, radius: 4, hue: 130 },
+ ],
+ },
+ binary: {
+ label: 'Binary dance',
+ gravity: 0.63,
+ bodies: [
+ { x: 0.39, y: 0.5, vx: 0, vy: -0.56, mass: 330, radius: 11, hue: 42 },
+ { x: 0.61, y: 0.5, vx: 0, vy: 0.56, mass: 330, radius: 11, hue: 205 },
+ { x: 0.5, y: 0.18, vx: 1.02, vy: 0, mass: 4, radius: 4, hue: 300 },
+ ],
+ },
+ slingshot: {
+ label: 'Slingshot test',
+ gravity: 0.92,
+ bodies: [
+ { x: 0.53, y: 0.52, vx: 0, vy: 0, mass: 780, radius: 14, hue: 36 },
+ { x: 0.13, y: 0.67, vx: 1.42, vy: -0.32, mass: 7, radius: 5, hue: 195 },
+ { x: 0.76, y: 0.32, vx: -0.42, vy: 0.65, mass: 22, radius: 6.5, hue: 325 },
+ ],
+ },
+};
+
+function parseGravityState() {
+ if (typeof window === 'undefined') return null;
+ const query = new URLSearchParams(window.location.search);
+ if (query.get('lab') !== 'gravity') return null;
+ const [preset, gravityValue] = (query.get('state') || '').split(',');
+ const gravity = Number(gravityValue);
+ if (!ORBIT_PRESETS[preset] || !Number.isFinite(gravity)) return null;
+ return { preset, gravity: clamp(gravity, 0.2, 1.4) };
+}
+
+export function GravityForgeLab() {
+ const shared = useMemo(() => parseGravityState(), []);
+ const initialPreset = shared?.preset || 'solar';
+ const [preset, setPreset] = useState(initialPreset);
+ const [gravity, setGravity] = useState(shared?.gravity || ORBIT_PRESETS[initialPreset].gravity);
+ const [paused, setPaused] = useState(false);
+ const [bodyCount, setBodyCount] = useState(ORBIT_PRESETS[initialPreset].bodies.length);
+ const [survival, setSurvival] = useState(0);
+ const [notice, setNotice] = useState(shared ? 'Shared orbit loaded. Add a planet and make it yours.' : 'Drag from empty space to launch a new planet.');
+ const canvasRef = useRef(null);
+ const bodiesRef = useRef([]);
+ const trailsRef = useRef([]);
+ const gravityRef = useRef(gravity);
+ const pausedRef = useRef(paused);
+ const launchRef = useRef(null);
+ const resetRef = useRef(0);
+
+ useEffect(() => { gravityRef.current = gravity; }, [gravity]);
+ useEffect(() => { pausedRef.current = paused; }, [paused]);
+
+ function loadPreset(id) {
+ setPreset(id);
+ setGravity(ORBIT_PRESETS[id].gravity);
+ setPaused(false);
+ setNotice(`${ORBIT_PRESETS[id].label} loaded. Drag to launch another body.`);
+ }
+
+ useEffect(() => {
+ const canvas = canvasRef.current;
+ if (!canvas) return undefined;
+ const context = canvas.getContext('2d');
+ if (!context) return undefined;
+ let width = 0;
+ let height = 0;
+ let animationId = 0;
+ let frameCount = 0;
+ let lastReset = -1;
+ let startTime = performance.now();
+
+ function resize() {
+ const rect = canvas.getBoundingClientRect();
+ const ratio = Math.min(window.devicePixelRatio || 1, 2);
+ width = Math.max(320, rect.width);
+ height = Math.max(360, rect.height);
+ canvas.width = Math.floor(width * ratio);
+ canvas.height = Math.floor(height * ratio);
+ context.setTransform(ratio, 0, 0, ratio, 0, 0);
+ }
+
+ function resetBodies() {
+ const base = ORBIT_PRESETS[preset];
+ const scale = Math.min(width, height) * 0.19;
+ bodiesRef.current = base.bodies.map((body) => ({
+ ...body,
+ x: body.x * width,
+ y: body.y * height,
+ vx: body.vx * scale,
+ vy: body.vy * scale,
+ }));
+ trailsRef.current = bodiesRef.current.map(() => []);
+ startTime = performance.now();
+ setBodyCount(bodiesRef.current.length);
+ setSurvival(0);
+ context.fillStyle = '#02040c';
+ context.fillRect(0, 0, width, height);
+ }
+
+ const observer = new ResizeObserver(() => {
+ resize();
+ resetBodies();
+ });
+ observer.observe(canvas);
+ resize();
+ resetBodies();
+
+ function pointerPosition(event) {
+ const rect = canvas.getBoundingClientRect();
+ return { x: event.clientX - rect.left, y: event.clientY - rect.top };
+ }
+
+ function onPointerDown(event) {
+ launchRef.current = pointerPosition(event);
+ canvas.setPointerCapture?.(event.pointerId);
+ }
+
+ function onPointerUp(event) {
+ if (!launchRef.current) return;
+ const start = launchRef.current;
+ const end = pointerPosition(event);
+ launchRef.current = null;
+ const dx = end.x - start.x;
+ const dy = end.y - start.y;
+ const speed = clamp(Math.hypot(dx, dy), 8, 180);
+ const hue = 155 + (bodiesRef.current.length * 47) % 195;
+ bodiesRef.current.push({
+ x: start.x,
+ y: start.y,
+ vx: dx * 0.58,
+ vy: dy * 0.58,
+ mass: 4 + speed * 0.035,
+ radius: 4 + speed * 0.012,
+ hue,
+ });
+ trailsRef.current.push([]);
+ setBodyCount(bodiesRef.current.length);
+ setNotice('Planet launched. A shorter drag makes a slower, tighter orbit.');
+ }
+
+ canvas.addEventListener('pointerdown', onPointerDown);
+ canvas.addEventListener('pointerup', onPointerUp);
+
+ function render(now) {
+ if (lastReset !== resetRef.current) {
+ lastReset = resetRef.current;
+ resetBodies();
+ }
+
+ context.fillStyle = 'rgba(2, 4, 12, 0.22)';
+ context.fillRect(0, 0, width, height);
+ const bodies = bodiesRef.current;
+
+ if (!pausedRef.current) {
+ const dt = 0.0035;
+ const forceScale = gravityRef.current * 112;
+ for (let i = 0; i < bodies.length; i += 1) {
+ for (let j = i + 1; j < bodies.length; j += 1) {
+ const a = bodies[i];
+ const b = bodies[j];
+ const dx = b.x - a.x;
+ const dy = b.y - a.y;
+ const distanceSquared = Math.max(90, dx * dx + dy * dy);
+ const distance = Math.sqrt(distanceSquared);
+ const force = forceScale / distanceSquared;
+ const nx = dx / distance;
+ const ny = dy / distance;
+ a.vx += nx * force * b.mass * dt;
+ a.vy += ny * force * b.mass * dt;
+ b.vx -= nx * force * a.mass * dt;
+ b.vy -= ny * force * a.mass * dt;
+ }
+ }
+ bodies.forEach((body, index) => {
+ body.x += body.vx * dt;
+ body.y += body.vy * dt;
+ const trail = trailsRef.current[index] || [];
+ if (frameCount % 2 === 0) trail.push({ x: body.x, y: body.y });
+ if (trail.length > 120) trail.shift();
+ trailsRef.current[index] = trail;
+ });
+ }
+
+ bodies.forEach((body, index) => {
+ const trail = trailsRef.current[index] || [];
+ if (trail.length > 1) {
+ context.beginPath();
+ context.moveTo(trail[0].x, trail[0].y);
+ for (let point = 1; point < trail.length; point += 1) context.lineTo(trail[point].x, trail[point].y);
+ context.strokeStyle = `hsla(${body.hue}, 88%, 68%, 0.35)`;
+ context.lineWidth = 1;
+ context.stroke();
+ }
+ const gradient = context.createRadialGradient(body.x, body.y, 0, body.x, body.y, body.radius * 3.2);
+ gradient.addColorStop(0, `hsla(${body.hue}, 95%, 78%, 1)`);
+ gradient.addColorStop(0.28, `hsla(${body.hue}, 90%, 58%, 0.9)`);
+ gradient.addColorStop(1, `hsla(${body.hue}, 88%, 55%, 0)`);
+ context.fillStyle = gradient;
+ context.beginPath();
+ context.arc(body.x, body.y, body.radius * 3.2, 0, Math.PI * 2);
+ context.fill();
+ });
+
+ const alive = bodies.filter((body) => body.x > -80 && body.x < width + 80 && body.y > -80 && body.y < height + 80).length;
+ if (frameCount % 30 === 0) {
+ const seconds = Math.floor((now - startTime) / 1000);
+ setSurvival(Math.round(clamp((alive / Math.max(1, bodies.length)) * 70 + Math.min(seconds, 30), 0, 100)));
+ }
+ frameCount += 1;
+ animationId = window.requestAnimationFrame(render);
+ }
+
+ animationId = window.requestAnimationFrame(render);
+ return () => {
+ observer.disconnect();
+ canvas.removeEventListener('pointerdown', onPointerDown);
+ canvas.removeEventListener('pointerup', onPointerUp);
+ window.cancelAnimationFrame(animationId);
+ };
+ }, [preset]);
+
+ function challengeUrl() {
+ const url = new URL(window.location.href);
+ url.searchParams.set('lab', 'gravity');
+ url.searchParams.set('state', `${preset},${round(gravity, 2)}`);
+ url.hash = 'playground';
+ return url.toString();
+ }
+
+ return (
+
+
+
+
+
+
Drag to launch · longer drag = faster planet
+
+
+ Mission Keep five bodies alive long enough to score 90.
+ Gravity {round(gravity, 2)}× setGravity(Number(event.target.value))} />
+
+ {Object.entries(ORBIT_PRESETS).map(([id, item]) => loadPreset(id)}>{item.label} )}
+
+ Model underneath Newtonian n-body gravity Every body pulls on every other body. Tiny launch changes accumulate into radically different futures.
+
+
+
setPaused((value) => !value)}
+ onReset={() => { resetRef.current += 1; setNotice('The system reset to its preset state.'); }}
+ onShare={() => shareChallenge({ title: `My GaugeGap orbit scored ${survival}`, text: `Can you build a more stable ${bodyCount}-body system?`, url: challengeUrl(), setNotice })}
+ onCopy={() => copyChallenge(challengeUrl(), setNotice)}
+ notice={notice}
+ />
+
+ );
+}
+
+function pendulumAcceleration(theta1, theta2, omega1, omega2, gravity) {
+ const mass1 = 1;
+ const mass2 = 1;
+ const length1 = 1;
+ const length2 = 1;
+ const delta = theta1 - theta2;
+ const denominator1 = length1 * (2 * mass1 + mass2 - mass2 * Math.cos(2 * delta));
+ const denominator2 = length2 * (2 * mass1 + mass2 - mass2 * Math.cos(2 * delta));
+ const alpha1 = (-gravity * (2 * mass1 + mass2) * Math.sin(theta1) - mass2 * gravity * Math.sin(theta1 - 2 * theta2) - 2 * Math.sin(delta) * mass2 * (omega2 * omega2 * length2 + omega1 * omega1 * length1 * Math.cos(delta))) / denominator1;
+ const alpha2 = (2 * Math.sin(delta) * (omega1 * omega1 * length1 * (mass1 + mass2) + gravity * (mass1 + mass2) * Math.cos(theta1) + omega2 * omega2 * length2 * mass2 * Math.cos(delta))) / denominator2;
+ return { alpha1, alpha2 };
+}
+
+function parsePendulumState() {
+ if (typeof window === 'undefined') return null;
+ const query = new URLSearchParams(window.location.search);
+ if (query.get('lab') !== 'pendulum') return null;
+ const [angleValue, differenceValue, gravityValue] = (query.get('state') || '').split(',').map(Number);
+ if (![angleValue, differenceValue, gravityValue].every(Number.isFinite)) return null;
+ return {
+ angle: clamp(angleValue, 45, 175),
+ difference: clamp(differenceValue, 0.01, 1.2),
+ gravity: clamp(gravityValue, 0.2, 1.8),
+ };
+}
+
+export function ChaosTwinsLab() {
+ const shared = useMemo(() => parsePendulumState(), []);
+ const [angle, setAngle] = useState(shared?.angle || 122);
+ const [difference, setDifference] = useState(shared?.difference || 0.08);
+ const [gravity, setGravity] = useState(shared?.gravity || 1);
+ const [paused, setPaused] = useState(false);
+ const [divergence, setDivergence] = useState(0);
+ const [timeToSplit, setTimeToSplit] = useState(null);
+ const [notice, setNotice] = useState(shared ? 'Shared chaos race loaded.' : 'Two pendulums begin almost identical. Predict when their paths split.');
+ const canvasRef = useRef(null);
+ const paramsRef = useRef({ angle, difference, gravity });
+ const pausedRef = useRef(paused);
+ const resetRef = useRef(0);
+
+ useEffect(() => { paramsRef.current = { angle, difference, gravity }; }, [angle, difference, gravity]);
+ useEffect(() => { pausedRef.current = paused; }, [paused]);
+
+ useEffect(() => {
+ const canvas = canvasRef.current;
+ if (!canvas) return undefined;
+ const context = canvas.getContext('2d');
+ if (!context) return undefined;
+ let width = 0;
+ let height = 0;
+ let animationId = 0;
+ let frameCount = 0;
+ let lastReset = -1;
+ let startedAt = performance.now();
+ let splitRecorded = false;
+ let twins = [];
+ let trails = [[], []];
+
+ function reset() {
+ const base = paramsRef.current.angle * Math.PI / 180;
+ const offset = paramsRef.current.difference * Math.PI / 180;
+ twins = [
+ { theta1: base, theta2: base * 0.72, omega1: 0, omega2: 0 },
+ { theta1: base + offset, theta2: base * 0.72, omega1: 0, omega2: 0 },
+ ];
+ trails = [[], []];
+ startedAt = performance.now();
+ splitRecorded = false;
+ setDivergence(0);
+ setTimeToSplit(null);
+ }
+
+ function resize() {
+ const rect = canvas.getBoundingClientRect();
+ const ratio = Math.min(window.devicePixelRatio || 1, 2);
+ width = Math.max(320, rect.width);
+ height = Math.max(360, rect.height);
+ canvas.width = Math.floor(width * ratio);
+ canvas.height = Math.floor(height * ratio);
+ context.setTransform(ratio, 0, 0, ratio, 0, 0);
+ reset();
+ }
+
+ const observer = new ResizeObserver(resize);
+ observer.observe(canvas);
+ resize();
+
+ function integrate(state, dt) {
+ const acceleration = pendulumAcceleration(state.theta1, state.theta2, state.omega1, state.omega2, 9.81 * paramsRef.current.gravity);
+ state.omega1 += acceleration.alpha1 * dt;
+ state.omega2 += acceleration.alpha2 * dt;
+ state.theta1 += state.omega1 * dt;
+ state.theta2 += state.omega2 * dt;
+ }
+
+ function points(state) {
+ const length = Math.min(width, height) * 0.19;
+ const originX = width * 0.5;
+ const originY = height * 0.25;
+ const x1 = originX + Math.sin(state.theta1) * length;
+ const y1 = originY + Math.cos(state.theta1) * length;
+ const x2 = x1 + Math.sin(state.theta2) * length;
+ const y2 = y1 + Math.cos(state.theta2) * length;
+ return { originX, originY, x1, y1, x2, y2 };
+ }
+
+ function render(now) {
+ if (lastReset !== resetRef.current) {
+ lastReset = resetRef.current;
+ reset();
+ }
+ context.fillStyle = 'rgba(2, 4, 12, 0.18)';
+ context.fillRect(0, 0, width, height);
+ if (!pausedRef.current) {
+ for (let pass = 0; pass < 5; pass += 1) twins.forEach((state) => integrate(state, 0.0034));
+ }
+ const colors = ['#7df9ff', '#ff5edb'];
+ const endpoints = twins.map(points);
+ endpoints.forEach((point, index) => {
+ const trail = trails[index];
+ if (!pausedRef.current && frameCount % 2 === 0) trail.push({ x: point.x2, y: point.y2 });
+ if (trail.length > 180) trail.shift();
+ if (trail.length > 1) {
+ context.strokeStyle = `${colors[index]}55`;
+ context.lineWidth = 1.2;
+ context.beginPath();
+ context.moveTo(trail[0].x, trail[0].y);
+ trail.slice(1).forEach((item) => context.lineTo(item.x, item.y));
+ context.stroke();
+ }
+ context.strokeStyle = colors[index];
+ context.lineWidth = 2;
+ context.beginPath();
+ context.moveTo(point.originX, point.originY);
+ context.lineTo(point.x1, point.y1);
+ context.lineTo(point.x2, point.y2);
+ context.stroke();
+ context.fillStyle = colors[index];
+ context.beginPath();
+ context.arc(point.x1, point.y1, 6, 0, Math.PI * 2);
+ context.arc(point.x2, point.y2, 8, 0, Math.PI * 2);
+ context.fill();
+ });
+ const distance = Math.hypot(endpoints[0].x2 - endpoints[1].x2, endpoints[0].y2 - endpoints[1].y2);
+ const normalized = Math.round(clamp(distance / Math.min(width, height) * 180, 0, 100));
+ if (frameCount % 10 === 0) {
+ setDivergence(normalized);
+ if (!splitRecorded && normalized > 35) {
+ splitRecorded = true;
+ setTimeToSplit(round((now - startedAt) / 1000, 1));
+ }
+ }
+ context.fillStyle = '#e7fbff';
+ context.beginPath();
+ context.arc(width * 0.5, height * 0.25, 5, 0, Math.PI * 2);
+ context.fill();
+ frameCount += 1;
+ animationId = window.requestAnimationFrame(render);
+ }
+
+ animationId = window.requestAnimationFrame(render);
+ return () => {
+ observer.disconnect();
+ window.cancelAnimationFrame(animationId);
+ };
+ }, []);
+
+ function applyPreset(nextAngle, nextDifference, nextGravity, message) {
+ setAngle(nextAngle);
+ setDifference(nextDifference);
+ setGravity(nextGravity);
+ resetRef.current += 1;
+ setNotice(message);
+ }
+
+ function challengeUrl() {
+ const url = new URL(window.location.href);
+ url.searchParams.set('lab', 'pendulum');
+ url.searchParams.set('state', [angle, difference, round(gravity, 2)].join(','));
+ url.hash = 'playground';
+ return url.toString();
+ }
+
+ return (
+
+
+
+
+
+
Cyan and pink begin only {difference}° apart
+
+
+ Mission Keep the twins under 20% divergence for ten seconds.
+ Starting angle {angle}° { setAngle(Number(event.target.value)); resetRef.current += 1; }} />
+ Initial difference {difference}° { setDifference(Number(event.target.value)); resetRef.current += 1; }} />
+ Gravity {round(gravity, 2)}× { setGravity(Number(event.target.value)); resetRef.current += 1; }} />
+
+ applyPreset(90, 0.02, 0.55, 'Slow split loaded. How long can the twins remain together?')}>Slow split
+ applyPreset(149, 0.08, 1.15, 'Chaos race loaded. Watch the paths separate.')}>Chaos race
+ applyPreset(170, 0.4, 1.55, 'Instant storm loaded. Divergence should arrive fast.')}>Instant storm
+
+ Model underneath Coupled double pendulum equations The result is unpredictable in practice even though no randomness is added after the start.
+
+
+
setPaused((value) => !value)}
+ onReset={() => { resetRef.current += 1; setNotice('Both futures restarted from the same tiny separation.'); }}
+ onShare={() => shareChallenge({ title: `My GaugeGap chaos twins split in ${timeToSplit || 'over 10'} seconds`, text: 'Can you keep two almost-identical pendulums together longer?', url: challengeUrl(), setNotice })}
+ onCopy={() => copyChallenge(challengeUrl(), setNotice)}
+ notice={notice}
+ />
+
+ );
+}
diff --git a/brainsnn-r3f-app/src/styles/deep-arcade.css b/brainsnn-r3f-app/src/styles/deep-arcade.css
new file mode 100644
index 0000000..a4810b9
--- /dev/null
+++ b/brainsnn-r3f-app/src/styles/deep-arcade.css
@@ -0,0 +1,228 @@
+.gg-arcade-card-amber { --arcade-accent: #ffc857; }
+.gg-arcade-card-blue { --arcade-accent: #60a5fa; }
+.gg-arcade-card-red { --arcade-accent: #ff5873; }
+.gg-arcade-card-orange { --arcade-accent: #fb923c; }
+
+.gg-deep-lab {
+ background:
+ radial-gradient(circle at 74% 0%, rgba(96, 165, 250, 0.1), transparent 34%),
+ linear-gradient(180deg, rgba(9, 11, 25, 0.98), rgba(3, 5, 13, 0.99));
+}
+
+.gg-gravity-canvas,
+.gg-flock-canvas,
+.gg-outbreak-canvas,
+.gg-pendulum-canvas {
+ cursor: crosshair;
+}
+
+.gg-deep-mission {
+ display: grid;
+ grid-template-columns: auto 1fr;
+ gap: 5px 10px;
+ padding: 14px;
+ border: 1px solid rgba(125, 249, 255, 0.16);
+ border-radius: 14px;
+ color: #dffcff;
+ background: linear-gradient(135deg, rgba(125, 249, 255, 0.07), rgba(139, 92, 246, 0.04));
+}
+
+.gg-deep-mission svg {
+ grid-row: 1 / span 2;
+ align-self: center;
+ color: var(--bsn-cyan);
+}
+
+.gg-deep-mission span,
+.gg-model-card span {
+ color: var(--bsn-text-muted);
+ font-family: var(--bsn-font-mono);
+ font-size: 0.66rem;
+ letter-spacing: 0.1em;
+ text-transform: uppercase;
+}
+
+.gg-deep-mission strong {
+ color: var(--bsn-text);
+ font-size: 0.82rem;
+ line-height: 1.4;
+}
+
+.gg-deep-presets,
+.gg-deep-toggle {
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 8px;
+}
+
+.gg-deep-presets button,
+.gg-deep-toggle button {
+ min-height: 39px;
+ padding: 7px 9px;
+ border: 1px solid rgba(255, 255, 255, 0.09);
+ border-radius: 10px;
+ color: var(--bsn-text-secondary);
+ background: rgba(255, 255, 255, 0.025);
+ font-size: 0.72rem;
+ font-weight: 800;
+ cursor: pointer;
+}
+
+.gg-deep-presets button:hover,
+.gg-deep-presets button.active,
+.gg-deep-toggle button:hover,
+.gg-deep-toggle button.active {
+ border-color: rgba(125, 249, 255, 0.44);
+ color: #e7ffff;
+ background: rgba(125, 249, 255, 0.08);
+}
+
+.gg-deep-presets button:last-child:nth-child(odd) {
+ grid-column: 1 / -1;
+}
+
+.gg-model-card {
+ padding: 14px;
+ border: 1px solid rgba(255, 255, 255, 0.08);
+ border-radius: 14px;
+ background: rgba(255, 255, 255, 0.025);
+}
+
+.gg-model-card strong {
+ display: block;
+ margin-top: 6px;
+ color: #f3f5ff;
+ font-family: var(--bsn-font-display);
+ font-size: 1rem;
+}
+
+.gg-model-card p {
+ margin: 7px 0 0;
+ color: var(--bsn-text-secondary);
+ font-size: 0.78rem;
+ line-height: 1.5;
+}
+
+.gg-deep-actions {
+ display: flex;
+ align-items: center;
+ gap: 9px;
+ flex-wrap: wrap;
+ margin-top: 17px;
+ padding-top: 16px;
+ border-top: 1px solid rgba(255, 255, 255, 0.07);
+}
+
+.gg-deep-actions button {
+ min-height: 40px;
+ display: inline-flex;
+ align-items: center;
+ gap: 7px;
+ padding: 0 12px;
+ border: 1px solid rgba(255, 255, 255, 0.1);
+ border-radius: 10px;
+ color: var(--bsn-text-secondary);
+ background: rgba(255, 255, 255, 0.025);
+ font-size: 0.75rem;
+ font-weight: 800;
+ cursor: pointer;
+}
+
+.gg-deep-actions button:hover {
+ border-color: rgba(125, 249, 255, 0.42);
+ color: #e7ffff;
+ background: rgba(125, 249, 255, 0.07);
+}
+
+.gg-deep-actions > span {
+ flex: 1 1 260px;
+ color: var(--bsn-text-muted);
+ font-size: 0.78rem;
+ line-height: 1.4;
+ text-align: right;
+}
+
+.gg-outbreak-stats {
+ display: grid;
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ gap: 7px;
+}
+
+.gg-outbreak-stats span {
+ min-width: 0;
+ padding: 10px 8px;
+ border: 1px solid rgba(255, 255, 255, 0.07);
+ border-radius: 10px;
+ color: var(--bsn-text-muted);
+ background: rgba(255, 255, 255, 0.02);
+ font-family: var(--bsn-font-mono);
+ font-size: 0.62rem;
+ text-align: center;
+ text-transform: uppercase;
+}
+
+.gg-outbreak-stats strong {
+ display: block;
+ margin-top: 4px;
+ color: #e7fbff;
+ font-size: 1rem;
+}
+
+.gg-gravity-wrap::before,
+.gg-flock-canvas + .gg-sim-canvas-label::before,
+.gg-outbreak-canvas + .gg-sim-canvas-label::before,
+.gg-pendulum-canvas + .gg-sim-canvas-label::before {
+ content: "";
+ width: 7px;
+ height: 7px;
+ display: inline-block;
+ margin-right: 5px;
+ border-radius: 50%;
+ background: #7df9ff;
+ box-shadow: 0 0 12px rgba(125, 249, 255, 0.8);
+}
+
+@media (max-width: 1050px) {
+ .gg-arcade-selector {
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ }
+}
+
+@media (max-width: 720px) {
+ .gg-deep-actions {
+ align-items: stretch;
+ }
+
+ .gg-deep-actions button {
+ flex: 1 1 calc(50% - 8px);
+ justify-content: center;
+ }
+
+ .gg-deep-actions > span {
+ flex-basis: 100%;
+ text-align: left;
+ }
+
+ .gg-deep-presets,
+ .gg-deep-toggle {
+ grid-template-columns: 1fr;
+ }
+
+ .gg-deep-presets button:last-child:nth-child(odd) {
+ grid-column: auto;
+ }
+}
+
+@media (max-width: 560px) {
+ .gg-arcade-selector {
+ grid-template-columns: 1fr;
+ }
+
+ .gg-arcade-card {
+ min-height: 190px;
+ }
+
+ .gg-outbreak-stats {
+ grid-template-columns: 1fr;
+ }
+}