import './style.css'; import { configureAssetBase } from './data-loader.js'; import { BrainView } from './brain-view.js'; import { FlyScene } from './scene.js'; import { FlyController } from './controller.js'; import { populations } from './stimulus.js'; const assetBase = new URL(import.meta.env.BASE_URL, document.baseURI).href; configureAssetBase(assetBase); const $ = (id) => document.getElementById(id); let scene, view, groups, worker, ready = false, paused = false, pending = false, resetting = false, epoch = 0, generation = 0, watchdog, timer, backend = 'cpu', lastResult = 0, simRate = 0, assetsReady = false, preparing = false, loading = false, disposed = false; const controller = new FlyController(); function openSetup(title, detail, error = false) { $('workspace').inert = true; if (view) view.enabled = false; $('setup').dataset.state = error ? 'error' : 'loading'; $('setup-status').textContent = title; $('setup-detail').textContent = detail; $('setup-button').disabled = !error; $('setup-button-label').textContent = error ? 'Try again' : 'Preparing…'; $('setup-progress').hidden = error; $('setup-progress').removeAttribute('value'); if (!$('setup').open) $('setup').showModal(); if (error) $('setup-button').focus({ preventScroll: true }); } function completeSetup() { $('setup').close(); $('workspace').inert = false; view.enabled = true; $('compute-menu').open = false; $('brain').focus({ preventScroll: true }); } async function setup() { if (preparing || loading || disposed) return; if (!assetsReady) { const loaded = await boot(); if (!loaded) return; } void start(); } function status(title, detail, error = false) { if (preparing || loading) { $('setup-status').textContent = title; $('setup-detail').textContent = detail; } $('status').textContent = title; $('status-detail').textContent = detail; $('compute-detail').textContent = title + ' · ' + detail; $('status-wrap').dataset.state = error ? 'error' : ready || (assetsReady && !loading) ? 'ready' : 'loading'; $('status-wrap').setAttribute('aria-live', ready ? 'off' : 'polite'); $('status-light').className = 'status-light' + (error ? ' error' : ready && !paused ? ' live' : ''); $('runtime-label').textContent = ready ? `${backend === 'gpu' ? 'WebGPU' : 'JavaScript'} · ${paused ? 'paused' : simRate.toFixed(2) + '×'}` : loading ? 'Loading…' : error ? 'Unavailable' : $('backend').value === 'cpu' ? 'JavaScript' : 'WebGPU · auto'; } function selectAction(key) { for (const button of document.querySelectorAll('[data-preset]')) button.setAttribute('aria-pressed', button.dataset.preset === key ? 'true' : 'false'); } function transport() { $('pause-label').textContent = paused ? 'Resume' : 'Pause'; $('pause-icon').setAttribute('href', paused ? '#i-play' : '#i-pause'); } function rangeTrack(input) { input.style.setProperty( '--range', ((Number(input.value) - Number(input.min)) / (Number(input.max) - Number(input.min))) * 100 + '%', ); } function timeout(ms, message) { clearTimeout(watchdog); watchdog = setTimeout(() => fail(message), ms); } function fail(message) { clearTimeout(timer); clearTimeout(watchdog); worker?.terminate(); worker = null; ready = false; pending = false; loading = false; resetting = false; epoch++; $('pause').disabled = $('reset').disabled = true; $('start').hidden = false; $('start').disabled = !assetsReady; $('start').textContent = 'Retry simulation'; $('backend').disabled = false; $('progress').hidden = true; status('Simulation stopped', message, true); openSetup('Setup interrupted', message, true); } function sendPulse(indices, options = {}) { if (!ready || !indices.length) return; $('paint-hint').textContent = `${indices.length.toLocaleString()} neurons selected`; worker.postMessage({ type: 'pulse', indices, strength: Number($('strength').value), profile: options.profile ?? 'paint', replace: options.replace === true, }); request(); } function request() { clearTimeout(timer); if (!ready || paused || pending || resetting || document.hidden) return; pending = true; worker.postMessage({ type: 'step', generation, silenced: $('silence').checked }); timeout(60000, 'The neural calculation stopped responding. Retry with the JavaScript reference.'); } function resetUI() { controller.reset(); view.reset(); scene.reset(); lastResult = 0; simRate = 0; $('behavior').textContent = 'At rest'; $('body-description').textContent = ''; $('walk-rate').textContent = $('escape-rate').textContent = '0 Hz'; $('turn-rate').textContent = '0°/s'; $('turn-rate').title = 'No turn'; $('turn-rate').setAttribute('aria-label', 'No turn'); $('walk-meter').style.width = $('escape-meter').style.width = $('turn-meter').style.width = '0'; $('turn-cursor').style.left = '50%'; document.querySelector('.body-state').dataset.rest = 'true'; selectAction(null); } async function start(forceCPU = false) { if (!assetsReady || loading || disposed) return; openSetup('Loading neural weights', 'Completed downloads are reused'); if (forceCPU) $('backend').value = 'cpu'; backend = 'cpu'; loading = true; ready = false; pending = false; paused = false; resetting = false; generation = 0; const token = ++epoch; clearTimeout(timer); clearTimeout(watchdog); worker?.terminate(); controller.reset(); scene.reset(); view.fc.clearRect(0, 0, view.w, view.h); view.tick = 0; view.pulseTick = 0; lastResult = 0; simRate = 0; $('start').disabled = true; $('backend').disabled = true; $('pause').disabled = $('reset').disabled = true; $('progress').hidden = false; $('progress').value = 0; status('Loading connectome', 'Completed downloads are reused'); try { worker = new Worker(new URL('./worker.js', import.meta.url), { type: 'module' }); } catch (error) { fail(error.message); return; } worker.onerror = (e) => { if (epoch === token) fail(e.message || 'Worker failed to start'); }; worker.onmessageerror = () => { if (epoch === token) fail('Could not decode a simulation response'); }; worker.onmessage = ({ data: m }) => { if (epoch !== token || disposed) return; if (m.type === 'stage') { status('Preparing simulation', m.message); timeout(180000, 'Simulation initialization timed out. Retry with JavaScript.'); } else if (m.type === 'progress') { $('progress').value = m.value; $('setup-progress').value = m.value; status('Loading connectome', Math.round(m.value * 100) + '% · 76 MB, downloaded once'); timeout(180000, 'The data download stalled. Retry to resume saved files.'); } else if (m.type === 'fallback') { status('Using JavaScript', m.message); $('backend').value = 'cpu'; } else if (m.type === 'ready') { clearTimeout(watchdog); ready = true; loading = false; backend = m.backend; $('progress').hidden = $('start').hidden = true; $('pause').disabled = $('reset').disabled = false; transport(); $('backend').disabled = false; status( 'Ready to paint', backend === 'gpu' ? 'WebGPU · @huggingface/kernels' : 'JavaScript reference', ); completeSetup(); if (view.selection.size) { view.pulseAt = performance.now(); worker.postMessage({ type: 'pulse', indices: Uint32Array.from(view.selection), strength: Number($('strength').value), profile: view.pulseProfile, replace: view.replacePulse, }); } request(); } else if (m.type === 'reset') { if (m.generation !== generation) return; clearTimeout(watchdog); pending = false; resetting = false; request(); } else if (m.type === 'result') { if (m.generation !== generation || resetting) return; clearTimeout(watchdog); pending = false; const now = performance.now(), elapsed = lastResult ? now - lastResult : m.wallMs; lastResult = now; const rate = (m.steps * 0.1) / Math.max(elapsed, 0.001); simRate = simRate ? simRate * 0.85 + rate * 0.15 : rate; const pose = controller.advance(m.rates, m.steps * 0.0001); scene.update(pose); view.result(m.firing, m.counts, m.tick); const r = controller.rates; $('behavior').textContent = pose.behavior; document.querySelector('.body-state').dataset.rest = String( paused || pose.behavior === 'At rest', ); $('body-description').textContent = pose.y > 0.05 ? `${pose.y.toFixed(1)} mm high · ${controller.distance.toFixed(1)} mm travelled` : ''; const walking = (r[0] + r[1]) / 2; $('walk-rate').textContent = Math.round(walking) + ' Hz'; $('escape-rate').textContent = Math.round(r[5]) + ' Hz'; const degrees = Math.round(Math.abs((pose.yawRate * 180) / Math.PI)); $('turn-rate').textContent = degrees + '°/s'; $('turn-rate').title = degrees ? `Turning ${pose.yawRate > 0 ? 'left' : 'right'}` : 'No turn'; $('turn-rate').setAttribute( 'aria-label', degrees ? `${degrees} degrees per second ${pose.yawRate > 0 ? 'left' : 'right'}` : 'No turn', ); $('walk-meter').style.width = Math.min(100, (walking / 36) * 100) + '%'; $('escape-meter').style.width = Math.min(100, (r[5] / 200) * 100) + '%'; const turn = Math.max(-50, Math.min(50, (-pose.yawRate / 3.8) * 50)); $('turn-meter').style.width = Math.abs(turn) + '%'; $('turn-meter').style.left = (turn < 0 ? 50 + turn : 50) + '%'; $('turn-cursor').style.left = 50 + turn + '%'; status( paused ? 'Paused' : `${(m.tick * 0.0001).toFixed(2)} s neural time`, `${backend === 'gpu' ? 'WebGPU' : 'JavaScript'} · ${simRate.toFixed(2)}× realtime · ${m.total.toLocaleString()} spikes`, ); timer = setTimeout(request, Math.max(0, 10 - m.wallMs)); } else if (m.type === 'error') { if (backend === 'gpu' && !forceCPU) { fail('WebGPU stopped. Restarting with JavaScript.'); void start(true); } else fail(m.message); } }; timeout(180000, 'Initialization timed out. Retry to reuse saved downloads.'); worker.postMessage({ type: 'init', backend: forceCPU ? 'cpu' : $('backend').value, assetBase }); } async function boot() { preparing = true; openSetup('Preparing the scene', 'Loading brain anatomy and fly geometry'); try { scene?.dispose(); view?.dispose(); scene = new FlyScene($('fly')); scene.onCameraChange = (mode) => { for (const camera of ['side', 'top', 'follow']) $('camera-' + camera).setAttribute('aria-pressed', mode === camera); }; view = new BrainView($('brain'), sendPulse, (n) => { selectAction(null); $('replay').disabled = !n; $('paint-hint').textContent = n ? `${n.toLocaleString()} neurons selected` : 'Paint neurons. Release to stimulate.'; }); view.brush = Number($('size').value) / 2; scene.setDepthOfField($('depth-of-field').checked); const results = await Promise.allSettled([view.load(), scene.load()]); if (disposed) return false; const failed = results.find((r) => r.status === 'rejected'); if (failed) throw failed.reason; groups = populations(results[0].value); assetsReady = true; $('anatomy-status').hidden = true; $('start').disabled = false; return true; } catch (error) { fail(error.message); return false; } finally { preparing = false; } } $('setup-button').onclick = () => void setup(); $('setup').addEventListener('cancel', (event) => event.preventDefault()); $('start').onclick = () => void start(); $('replay').onclick = () => view?.pulse(); $('brush').onclick = () => { if (!view) return; view.mode = 'paint'; $('brush').setAttribute('aria-pressed', 'true'); $('erase').setAttribute('aria-pressed', 'false'); }; $('erase').onclick = () => { if (!view) return; view.mode = 'erase'; $('brush').setAttribute('aria-pressed', 'false'); $('erase').setAttribute('aria-pressed', 'true'); }; $('size').oninput = () => { if (view) view.brush = Number($('size').value) / 2; $('size-label').textContent = $('size').value + ' px'; rangeTrack($('size')); }; $('clear').onclick = () => { view?.clear(); if (ready) worker.postMessage({ type: 'clear' }); }; $('projection').onchange = () => { if (view) { view.projection = $('projection').value; view.layout(); } }; for (const button of document.querySelectorAll('[data-preset]')) button.onclick = () => { if (!ready || !groups) return; const key = button.dataset.preset; view.preset( groups[key === 'escape' ? 'escapeInput' : key], key === 'left' || key === 'right' ? 'turn' : 'paint', ); selectAction(key); }; $('pause').onclick = () => { if (!ready) return; paused = !paused; transport(); document.querySelector('.body-state').dataset.rest = String( paused || $('behavior').textContent === 'At rest', ); if (paused) { clearTimeout(timer); status('Paused', 'Painting will pulse when you resume'); } else { lastResult = 0; status('Resumed', backend === 'gpu' ? 'WebGPU · @huggingface/kernels' : 'JavaScript reference'); request(); } }; $('reset').onclick = () => { if (!ready || resetting) return; resetting = true; generation++; clearTimeout(timer); resetUI(); worker.postMessage({ type: 'reset', generation }); timeout(60000, 'Reset did not complete'); }; $('backend').onchange = () => { if (ready) { ready = false; void start(); } else if (!loading) status('Ready to paint', '166,700 neurons · 25.6 million connections'); }; $('depth-of-field').onchange = () => scene?.setDepthOfField($('depth-of-field').checked); document.addEventListener('click', (event) => { if (!$('compute-menu').contains(event.target)) $('compute-menu').open = false; }); document.addEventListener('keydown', (event) => { if (event.key === 'Escape') $('compute-menu').open = false; }); for (const camera of ['side', 'top', 'follow']) $('camera-' + camera).onclick = () => scene?.setCamera(camera); $('settings-button').onclick = () => $('settings').showModal(); $('close-settings').onclick = () => $('settings').close(); $('settings').addEventListener('click', (e) => { if (e.target === $('settings')) { const r = $('settings').getBoundingClientRect(); if (e.clientX < r.left || e.clientX > r.right || e.clientY < r.top || e.clientY > r.bottom) $('settings').close(); } }); $('strength').oninput = () => { $('strength-label').textContent = $('strength').value + ' Hz'; rangeTrack($('strength')); }; rangeTrack($('size')); rangeTrack($('strength')); document.querySelector('.body-state').dataset.rest = 'true'; document.addEventListener('visibilitychange', () => { lastResult = 0; if (document.hidden) { clearTimeout(timer); clearTimeout(watchdog); } else { if (loading) timeout(180000, 'Initialization did not resume. Retry loading.'); else if (pending || resetting) timeout(60000, 'The simulation did not resume. Retry loading.'); request(); } }); window.addEventListener('pagehide', (event) => { if (!event.persisted) { disposed = true; clearTimeout(timer); clearTimeout(watchdog); worker?.terminate(); view?.dispose(); scene?.dispose(); } }); // The HTML overlay is visible before modules load; upgrade it to a native modal. $('setup').close(); $('setup').showModal();