fly-brain-sim / index.html
VGclaude's picture
Upload index.html
4b62433 verified
Raw History Blame Contribute Delete
78 kB
<!doctype html> <html lang="en"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Fly Brain Sim</title> <style> :root { --bg:#060914; --bg2:#0b1020; --card:rgba(17,23,46,.78); --card2:rgba(24,32,62,.7); --line:rgba(130,150,215,.17); --text:#e8ecff; --dim:#8f99ba; --faint:#5d6788; --violet:#8b6cff; --blue:#3ea0ff; --teal:#2bd9c5; --amber:#ffb44c; --red:#ff6b6b; --grad:linear-gradient(90deg,#8b6cff,#3ea0ff); --mono:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace; } * { box-sizing:border-box; } html, body { height:100%; } body { margin:0; background:var(--bg); color:var(--text); font:14px/1.45 -apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif; display:flex; flex-direction:column; overflow:hidden; } button { font:inherit; color:inherit; cursor:pointer; } small { color:var(--dim); } .mono { font-family:var(--mono); letter-spacing:.08em; text-transform:uppercase; font-size:10px; color:var(--faint); } /* ---- top bar ---- */ #top { height:52px; flex:none; display:flex; align-items:center; gap:14px; padding:0 14px; background:linear-gradient(180deg,#0c1226,#080c1a); border-bottom:1px solid var(--line); } .brand { display:flex; align-items:center; gap:10px; cursor:pointer; text-decoration:none; color:inherit; } .logo { width:30px; height:30px; border-radius:9px; background:linear-gradient(135deg,#8b6cff,#3ea0ff 60%,#2bd9c5); display:grid; place-items:center; box-shadow:0 0 18px rgba(110,120,255,.45); } .logo svg { width:18px; height:18px; } .brand b { font-size:15px; letter-spacing:.2px; } .brand .mono { display:block; margin-top:-2px; } .spacer { flex:1; } .tb { display:flex; gap:8px; align-items:center; } .pill { background:rgba(255,255,255,.04); border:1px solid var(--line); border-radius:999px; padding:6px 12px; font-size:13px; color:var(--text); transition:.15s; } .pill:hover { border-color:rgba(139,108,255,.65); background:rgba(139,108,255,.12); } .pill.primary { background:var(--grad); border-color:transparent; color:#0a0d1c; font-weight:600; } .pill.on { border-color:var(--teal); background:rgba(43,217,197,.12); } .menu { position:relative; } .dropdown { position:absolute; right:0; top:42px; width:340px; z-index:30; background:rgba(10,14,30,.97); border:1px solid var(--line); border-radius:16px; padding:12px; box-shadow:0 24px 60px rgba(0,0,0,.6); backdrop-filter:blur(10px); } .dropdown.hidden, .hidden { display:none; } .menu-head { display:flex; justify-content:space-between; align-items:center; margin-bottom:8px; } .menu-head b { font-size:14px; } .switch { display:flex; gap:6px; align-items:center; font-size:12px; color:var(--dim); } .slot { display:grid; grid-template-columns:1fr auto auto auto; gap:6px; align-items:center; padding:8px; border:1px solid var(--line); border-radius:12px; margin-bottom:6px; background:var(--card2); } .slot b { font-size:13px; } .slot small { display:block; font-size:11px; line-height:1.3; } .slot button { background:rgba(255,255,255,.05); border:1px solid var(--line); border-radius:8px; padding:4px 9px; font-size:12px; } .slot button:hover { border-color:var(--violet); } .slot button[disabled] { opacity:.35; cursor:default; } .menu-foot { display:flex; gap:6px; flex-wrap:wrap; margin-top:8px; } .menu-foot .pill { font-size:12px; padding:5px 10px; } /* ---- layout ---- */ main { flex:1; display:flex; min-height:0; } #dock { flex:0 0 118px; overflow-y:auto; padding:10px 8px; background:linear-gradient(180deg,#0a0f20,#070a16); border-right:1px solid var(--line); } .dock-group { margin-bottom:14px; } .dock-group .mono { display:block; margin:0 4px 6px; } .tool { width:100%; display:flex; align-items:center; gap:8px; padding:7px 8px; margin-bottom:5px; text-align:left; background:rgba(255,255,255,.03); border:1px solid var(--line); border-radius:12px; font-size:13px; transition:.15s; } .tool:hover { border-color:rgba(139,108,255,.6); } .tool.on { border-color:var(--teal); background:rgba(43,217,197,.1); box-shadow:0 0 0 1px rgba(43,217,197,.25) inset; } .tool .dot { width:12px; height:12px; border-radius:50%; background:var(--c); box-shadow:0 0 10px var(--c); flex:none; } .tool kbd { margin-left:auto; font:10px var(--mono); color:var(--faint); } .swatches { display:flex; gap:5px; padding:2px 6px 8px; flex-wrap:wrap; } .sw { width:20px; height:20px; border-radius:50%; border:2px solid transparent; background:hsl(var(--h),90%,62%); box-shadow:0 0 8px hsla(var(--h),90%,60%,.7); padding:0; } .sw.sel { border-color:#fff; transform:scale(1.12); } #stage { position:relative; flex:1; min-width:0; } #world { position:absolute; left:0; top:0; width:100%; height:100%; display:block; cursor:crosshair; background:radial-gradient(ellipse at 50% 40%,#10172e 0%,#090d1c 70%,#060914 100%); } aside { flex:0 0 372px; overflow-y:auto; padding:10px; background:linear-gradient(180deg,#0a0f20,#070a16); border-left:1px solid var(--line); } aside.hidden { display:none; } .card { background:var(--card); border:1px solid var(--line); border-radius:16px; padding:10px 12px; margin-bottom:10px; } .card h4 { margin:0 0 8px; font:600 10px var(--mono); letter-spacing:.12em; text-transform:uppercase; color:var(--dim); display:flex; justify-content:space-between; } .card canvas { width:100%; height:auto; border-radius:10px; background:#080c18; display:block; } .bars { display:grid; gap:7px; } .bar { display:grid; grid-template-columns:122px 1fr 40px; gap:8px; align-items:center; font-size:12px; color:var(--dim); } .bar i { display:block; height:7px; background:rgba(255,255,255,.07); border-radius:5px; overflow:hidden; } .bar b { display:block; height:100%; width:0; border-radius:5px; transition:width .12s; } .bar span { font:11px var(--mono); text-align:right; color:var(--text); } #state { font-size:15px; font-weight:600; } #stats { font-size:11.5px; color:var(--dim); line-height:1.5; } #err { position:absolute; left:140px; top:64px; z-index:5; margin:0; padding:8px 12px; border:1px solid rgba(139,108,255,.5); border-radius:12px; background:rgba(12,16,36,.92); font-size:12.5px; display:none; } #toast { position:fixed; left:50%; bottom:22px; transform:translateX(-50%); z-index:60; padding:8px 14px; border-radius:999px; background:rgba(14,20,42,.96); border:1px solid var(--line); font-size:13px; opacity:0; pointer-events:none; transition:.25s; } #toast.show { opacity:1; } /* ---- landing page ---- */ #landing { position:fixed; inset:0; z-index:50; overflow-y:auto; overflow-x:hidden; scroll-behavior:smooth; background:radial-gradient(1200px 700px at 78% 8%,rgba(110,90,255,.22),transparent 60%), radial-gradient(900px 600px at 5% 70%,rgba(43,217,197,.10),transparent 60%),#050813; transition:opacity .5s, visibility .5s; } #landing.gone { opacity:0; visibility:hidden; pointer-events:none; } #heroCanvas { position:absolute; right:0; top:0; width:68%; height:100vh; min-height:620px; pointer-events:none; } .wrap { position:relative; max-width:1180px; margin:0 auto; padding:0 28px; } .lnav { position:sticky; top:0; z-index:5; height:64px; display:flex; align-items:center; gap:22px; background:linear-gradient(180deg,rgba(5,8,19,.92),rgba(5,8,19,.55)); backdrop-filter:blur(8px); border-bottom:1px solid rgba(130,150,215,.08); } .lnav a.l { color:var(--dim); text-decoration:none; font-size:14px; } .lnav a.l:hover { color:#fff; } .hero { position:relative; min-height:calc(100vh - 64px); padding:64px 0 40px; display:flex; flex-direction:column; justify-content:center; } .hero h1 { font-size:68px; line-height:1.02; margin:0 0 18px; font-weight:700; letter-spacing:-1.5px; } .hero h1 em { font-style:italic; font-weight:600; background:linear-gradient(90deg,#8b6cff,#3ea0ff 60%,#2bd9c5); -webkit-background-clip:text; background-clip:text; color:transparent; } .hero p.lead { font-size:21px; color:#b8c0e0; max-width:560px; margin:0 0 26px; } .cta { display:flex; gap:12px; flex-wrap:wrap; align-items:center; } .btn { display:inline-flex; align-items:center; gap:8px; padding:12px 22px; border-radius:999px; font-weight:600; font-size:15px; border:1px solid var(--line); background:rgba(255,255,255,.05); color:#fff; text-decoration:none; } .btn.primary { background:var(--grad); color:#080b18; border-color:transparent; box-shadow:0 8px 30px rgba(110,110,255,.35); } .btn.primary:hover { filter:brightness(1.1); } .boot { margin-top:20px; max-width:560px; padding:12px 14px; border:1px solid rgba(255,196,80,.35); border-radius:14px; background:rgba(255,190,70,.06); font-size:13px; } .boot .track { height:5px; border-radius:4px; background:rgba(255,255,255,.08); margin-top:8px; overflow:hidden; } .boot .fill { height:100%; width:0; background:var(--grad); transition:width .2s; } .boot.ready { border-color:rgba(43,217,197,.45); background:rgba(43,217,197,.07); } .stats { display:flex; gap:34px; flex-wrap:wrap; margin-top:34px; padding-top:22px; border-top:1px solid var(--line); max-width:640px; } .stat b { display:block; font-size:30px; font-weight:700; background:linear-gradient(90deg,#8b6cff,#3ea0ff); -webkit-background-clip:text; background-clip:text; color:transparent; } .stat span { font-size:12px; color:var(--dim); } section.blk { position:relative; padding:56px 0; } section.blk h2 { font-size:30px; margin:0 0 22px; letter-spacing:-.5px; } .grid3 { display:grid; grid-template-columns:repeat(3,1fr); gap:18px; } .gcard { background:var(--card); border:1px solid var(--line); border-radius:22px; padding:22px; } .gcard .ic { width:38px; height:38px; border-radius:11px; display:grid; place-items:center; background:rgba(62,160,255,.14); border:1px solid rgba(62,160,255,.3); margin-bottom:14px; } .gcard h3 { margin:0 0 8px; font-size:17px; } .gcard p { margin:0; color:var(--dim); font-size:14px; } .steps { display:grid; grid-template-columns:repeat(3,1fr); gap:16px 28px; } .step { display:flex; gap:12px; color:#b8c0e0; font-size:14px; } .step i { flex:none; width:28px; height:24px; border-radius:7px; border:1px solid var(--line); display:grid; place-items:center; font:11px var(--mono); color:var(--blue); background:rgba(62,160,255,.08); font-style:normal; } .note { border:1px solid rgba(255,196,80,.3); background:rgba(255,190,70,.05); border-radius:16px; padding:14px 16px; color:#cfc7a8; font-size:13.5px; } footer.lf { padding:30px 0 60px; color:var(--faint); font-size:12.5px; border-top:1px solid var(--line); } footer.lf a { color:var(--blue); text-decoration:none; } @media (max-width:900px) { .hero h1 { font-size:46px; } #heroCanvas { width:100%; opacity:.5; } .grid3, .steps { grid-template-columns:1fr; } #dock { flex-basis:92px; } aside { flex-basis:300px; } } </style> </head> <body> <header id="top"> <a class="brand" id="homeBtn" title="Back to the start page"> <span class="logo"><svg viewBox="0 0 24 24" fill="none" stroke="#08101f" stroke-width="2.2" stroke-linecap="round"><path d="M12 3c-4 0-7 3-7 7 0 2 .8 3.4 2 4.5V18a3 3 0 0 0 3 3h4a3 3 0 0 0 3-3v-3.5c1.2-1.1 2-2.5 2-4.5 0-4-3-7-7-7z"/><path d="M12 3v18M7 9h3M14 9h3M7 14h3M14 14h3"/></svg></span> <span><b>Fly Brain Sim</b><span class="mono">Drosophila connectome</span></span> </a> <div class="spacer"></div> <div class="tb"> <button class="pill" id="pause">Pause</button> <button class="pill" id="speed">Life cycle ×1</button> <button class="pill" id="clear">Clear</button> <button class="pill" id="respawn">New fly</button> <button class="pill" id="panels">Hide panels</button> <div class="menu"> <button class="pill primary" id="savesBtn">Saves ▾</button> <div class="dropdown hidden" id="savesMenu"> <div class="menu-head"><b>Saves</b><label class="switch"><input type="checkbox" id="autosave" checked> Autosave</label></div> <div class="slot" data-slot="auto"><div><b>Autosave</b><small id="slotInfoauto">empty</small></div><span></span><button data-act="load" disabled>Load</button><span></span></div> <div class="slot" data-slot="1"><div><b>Slot 1</b><small id="slotInfo1">empty</small></div><button data-act="save">Save</button><button data-act="load" disabled>Load</button><button data-act="del" disabled>✕</button></div> <div class="slot" data-slot="2"><div><b>Slot 2</b><small id="slotInfo2">empty</small></div><button data-act="save">Save</button><button data-act="load" disabled>Load</button><button data-act="del" disabled>✕</button></div> <div class="slot" data-slot="3"><div><b>Slot 3</b><small id="slotInfo3">empty</small></div><button data-act="save">Save</button><button data-act="load" disabled>Load</button><button data-act="del" disabled>✕</button></div> <div class="menu-foot"> <button class="pill" id="exportBtn">Export file</button> <label class="pill" style="cursor:pointer">Import file<input type="file" id="importFile" accept=".json,application/json" hidden></label> <button class="pill" id="forget">Forget learned memory</button> </div> <small style="display:block;margin-top:8px">Saves stay in this browser. A save includes the world, her body and what she has learned.</small> </div> </div> </div> </header> <div id="err">Loading…</div> <main> <nav id="dock"> <div class="dock-group"><span class="mono">Food &amp; cues</span> <button class="tool on" data-tool="food" style="--c:#6cc04a"><i class="dot"></i>Food</button> <button class="tool" data-tool="lamp" style="--c:#6aa9ff"><i class="dot"></i>Lamp</button> <div class="swatches" id="swatches"> <button class="sw sel" style="--h:210" data-hue="210" title="Blue"></button> <button class="sw" style="--h:170" data-hue="170" title="Cyan"></button> <button class="sw" style="--h:120" data-hue="120" title="Green"></button> <button class="sw" style="--h:50" data-hue="50" title="Yellow"></button> <button class="sw" style="--h:280" data-hue="280" title="Violet"></button> <button class="sw" style="--h:0" data-hue="0" title="Red"></button> </div> <input type="hidden" id="lampColor" value="210"> <button class="tool" data-tool="poison" style="--c:#a45be0"><i class="dot"></i>Poison</button> </div> <div class="dock-group"><span class="mono">Hazards</span> <button class="tool" data-tool="flame" style="--c:#ff7a2f"><i class="dot"></i>Flame</button> <button class="tool" data-tool="zap" style="--c:#9fd0ff"><i class="dot"></i>Zap</button> <button class="tool" data-tool="spider" style="--c:#d8506a"><i class="dot"></i>Spider</button> </div> <div class="dock-group"><span class="mono">Creatures</span> <button class="tool" data-tool="male" style="--c:#c9a063"><i class="dot"></i>Male fly</button> </div> <div class="dock-group"><span class="mono">Build (drag)</span> <button class="tool" data-tool="glass" style="--c:#8fd8ff"><i class="dot"></i>Glass</button> <button class="tool" data-tool="wall" style="--c:#8a94a8"><i class="dot"></i>Wall</button> </div> </nav> <div id="stage"><canvas id="world"></canvas></div> <aside> <div class="card"><h4><span>Brain · live neurons</span><span id="spk"></span></h4><canvas id="brain" width="360" height="300"></canvas></div> <div class="card"><h4><span>Senses &amp; drives</span></h4> <div class="bars"> <div class="bar">Smell neurons<i><b id="bOdor" style="background:#5ea0e6"></b></i><span id="vOdor"></span></div> <div class="bar">Feeding neurons<i><b id="bFeed" style="background:#e0b44c"></b></i><span id="vFeed"></span></div> <div class="bar">Alarm neurons<i><b id="bAlarm" style="background:#e5534b"></b></i><span id="vAlarm"></span></div> <div class="bar">Escape neurons<i><b id="bEscape" style="background:#e5834b"></b></i><span id="vEscape"></span></div> <div class="bar">Lamp memory<i><b id="bMem" style="background:#6bd16b"></b></i><span id="vMem"></span></div> </div></div> <div class="card"><h4><span>Reproduction</span></h4> <canvas id="repro" width="360" height="290" style="margin-bottom:8px"></canvas> <div class="bars"> <div class="bar">Mating hub pC1<i><b id="bRecept" style="background:#d96ac0"></b></i><span id="vRecept"></span></div> <div class="bar">Vaginal plate DNp13<i><b id="bVpo" style="background:#c98ae0"></b></i><span id="vVpo"></span></div> <div class="bar">Egg-laying DNs<i><b id="bLay" style="background:#e8e2c8"></b></i><span id="vLay"></span></div> </div></div> <div class="card"><h4><span>Body</span></h4> <div class="bars"> <div class="bar">Health<i><b id="bHealth" style="background:#6bd16b"></b></i><span id="vHealth"></span></div> <div class="bar">Energy<i><b id="bEnergy" style="background:#c9a23c"></b></i><span id="vEnergy"></span></div> </div> <div id="state" style="margin-top:10px"></div></div> <div class="card"><div id="stats"></div> <small style="display:block;margin-top:8px;font-size:11px">Dots are real neurons at their scanned positions; lit dots spike now, orange ones are descending neurons that command the body.</small></div> </aside> </main> <div id="toast"></div> <section id="landing"> <canvas id="heroCanvas"></canvas> <div class="wrap"> <div class="lnav"> <a class="brand"><span class="logo"><svg viewBox="0 0 24 24" fill="none" stroke="#08101f" stroke-width="2.2" stroke-linecap="round"><path d="M12 3c-4 0-7 3-7 7 0 2 .8 3.4 2 4.5V18a3 3 0 0 0 3 3h4a3 3 0 0 0 3-3v-3.5c1.2-1.1 2-2.5 2-4.5 0-4-3-7-7-7z"/><path d="M12 3v18M7 9h3M14 9h3M7 14h3M14 14h3"/></svg></span> <span><b>Fly Brain Sim</b><span class="mono">Drosophila connectome</span></span></a> <div class="spacer"></div> <a class="l" href="#how">How it works</a><a class="l" href="#start">Quickstart</a><a class="l" href="#limits">What is real</a><a class="l" href="#credits">Credits</a> <button class="pill primary" id="enterTop">Launch simulation</button> </div> <div class="hero"> <span class="mono" style="margin-bottom:14px">Real connectome · running in your browser</span> <h1>Welcome to<br><em>Fly Brain Sim</em></h1> <p class="lead">A fruit fly whose brain is the real scanned connectome. Feed her, scare her, teach her, and watch 138,639 neurons decide what she does.</p> <div class="cta"> <button class="btn primary" id="enterBtn">Enter the simulation &rarr;</button> <a class="btn" href="#how">How it works</a> </div> <div class="boot" id="boot"><span id="bootText">Starting…</span><div class="track"><div class="fill" id="bootFill"></div></div></div> <div class="stats"> <div class="stat"><b>138,639</b><span>neurons simulated</span></div> <div class="stat"><b>54.5 M</b><span>synapses</span></div> <div class="stat"><b>8,846</b><span>neuron types</span></div> <div class="stat"><b>1,303</b><span>descending neurons</span></div> </div> </div> <section class="blk" id="how"><h2>How it works</h2> <div class="grid3"> <div class="gcard"><div class="ic">🧠</div><h3>The real wiring</h3><p>Every neuron and every synapse count comes from the FlyWire scan of an adult female fruit fly. Senses go into real sensory neurons and her actions are read from real descending neurons.</p></div> <div class="gcard"><div class="ic">🪰</div><h3>Teach her</h3><p>Place a lamp next to food or a flame. Dopamine neurons weaken the real mushroom-body synapses, and she learns to approach or avoid it.</p></div> <div class="gcard"><div class="ic">🥚</div><h3>A whole life</h3><p>Mating, egg laying, larvae and new flies, with a reproductive tract driven by her real mating and egg-laying neurons.</p></div> </div> </section> <section class="blk" id="start"><h2>Quickstart</h2> <div class="steps"> <div class="step"><i>01</i><span>Pick a thing from the dock on the left and click the arena to place it.</span></div> <div class="step"><i>02</i><span>Put food down and watch the smell neurons pull her toward it.</span></div> <div class="step"><i>03</i><span>Add a lamp beside food a few times. Later, she goes to the lamp alone.</span></div> <div class="step"><i>04</i><span>Put the lamp beside a flame instead and she learns to keep away.</span></div> <div class="step"><i>05</i><span>Drop a male fly, then glass and walls, and see mating and egg laying.</span></div> <div class="step"><i>06</i><span>Open Saves to keep a world, or export it as a file.</span></div> </div> </section> <section class="blk" id="limits"><h2>What is real, and what isn't</h2> <div class="note" style="margin-bottom:18px"><b>Time scale.</b> One fly-day lasts 2 minutes at life cycle ×1. Anything that takes minutes or longer in a real fly (starvation, egg development, ageing, memory fading) runs on that clock. Walking, reflexes and neurons run in real time. Courtship and copulation are shortened so you can watch them.</div> <div class="grid3"> <div class="gcard"><div class="ic">✅</div><h3>Real: from the scan</h3> <p> • 138,639 neurons and 54.5 million synapses, and every dot at its scanned position.<br> • Smell, taste, touch, heat, hearing and looming go into real sensory neurons.<br> • Her actions (approach, feeding, alarm, escape) are read from real descending neurons.<br> • Mating uses the real pC1 hub and the vaginal-plate neuron DNp13. Egg laying uses the real egg-laying neurons.<br> • Learning weakens real Kenyon cell → MBON synapses, gated by real dopamine neurons.</p></div> <div class="gcard"><div class="ic">🛠</div><h3>Approximated or hand-made</h3> <p> • <b>Steering and legs:</b> a hand-made reflex, because the scan has no nerve cord.<br> • <b>Wandering:</b> a random wobble I wrote. The brain is silent with no input, while real brains are always spontaneously active. She walks at one constant speed, never pauses, grooms or follows walls, and hunger doesn't drive exploring.<br> • <b>Training:</b> real mushroom-body mechanism, but one cue class, so lamp colour is cosmetic and she can't learn a specific colour. The lamp enters through olfactory-style inputs, not vision. There is no short/long-term memory split, extinction or spaced-trial effect. Eating is a fixed reward switch and the approach is a turning rule.<br> • <b>Egg laying:</b> one input neuron (SMP550) is driven directly.<br> • <b>Body:</b> ovaries, sperm, development and ageing are my own models with real timings.<br> • <b>Brain settings:</b> synaptic gain is 0.34 of the published value, 1 ms steps, about 0.65× real time.</p></div> <div class="gcard"><div class="ic">⚠️</div><h3>Known unrealistic or missing</h3> <p> • A food blob is eaten in about 3 seconds. A real meal takes minutes.<br> • Odour is a smooth gradient, not turbulent plumes.<br> • A flat 2D arena: no flight, climbing, sleep, grooming, day and night, or temperature.<br> • Newly emerged females only wander. They never mate or lay.<br> • Colour vision, specific colour learning and real visual steering are not modelled.</p></div> </div> </section> <footer class="lf" id="credits">Connectome and annotations: FlyWire (Dorkenwald et al. 2024; Schlegel et al. 2024; Matsliah et al. 2024). Neuron model after Shiu et al. 2024 (MIT). Browse these neurons at <a href="https://www.virtualflybrain.org/" target="_blank" rel="noopener">Virtual Fly Brain</a>. This is an independent project and is not affiliated with VFB or FlyWire. Anonymous visit counts (page views and which features are used) are collected with GoatCounter (goatcounter.com); no cookies, no personal data. Your fly and saves stay in your browser.</footer> </div> </section> <script> // ---- anonymous visit analytics (GoatCounter: no cookies, no personal data) ---- // Set the code from goatcounter.com (the part before .goatcounter.com). Empty = analytics off. const GC_CODE = 'vgg'; const seenEv = new Set(); function gcHit(path, title, ev) { if (!GC_CODE || GC_CODE.startsWith('__')) return; try { const q = new URLSearchParams({ p: path, t: title || path, r: document.referrer || '', s: screen.width + ',' + screen.height + ',1', rnd: Math.random().toString(36).slice(2) }); if (ev) q.set('e', 'true'); new Image().src = 'https://' + GC_CODE + '.goatcounter.com/count?' + q.toString(); } catch (e) {} } function track(name, once) { if (once) { if (seenEv.has(name)) return; seenEv.add(name); } gcHit('event/' + name, name, true); } gcHit(location.pathname || '/', document.title, false); // The brain runs on the Python server: a spiking simulation of the FlyWire v783 // whole-brain connectome. This page is the body and the world. The brain scan has // no nerve cord, so turning toward/away from a side is a simple body reflex that // the brain's descending neurons gate (go / feed / flee); see README. let W = 860, H = 560; const world = document.getElementById('world'), g = world.getContext('2d'); function resizeWorld() { // the arena fills the whole page const r = world.parentElement.getBoundingClientRect(); W = world.width = Math.max(320, Math.floor(r.width)); H = world.height = Math.max(240, Math.floor(r.height)); if (typeof fly !== 'undefined' && fly) { fly.x = clamp(fly.x, 24, W - 24); fly.y = clamp(fly.y, 24, H - 24); } } const bcv = document.getElementById('brain'), bg = bcv.getContext('2d'); const $ = id => document.getElementById(id); const clamp = (x, a = 0, b = 1) => Math.max(a, Math.min(b, x)); const rnd = (a, b) => a + Math.random() * (b - a); // ---- brain link ----------------------------------------------- let info = null, pos = null, dnSet = new Set(); let out = { odor: 0, feed: 0, alarm: 0, escape: 0, lay: 0, recept: 0, vpo: 0, spikes_per_ms: 0, spiking: [], top_dn: [], compute_ms: 0 }; let smooth = { odor: 0, feed: 0, alarm: 0, escape: 0, lay: 0, recept: 0, vpo: 0 }; let stim = {}; let lit = new Map(); // neuron index -> brightness let online = false, busy = false; // The brain runs in a Web Worker in this page: the connectome is downloaded once (~37 MB) // and simulated on the visitor's own machine. Nothing runs on a server. const BUILD = '1791905613'; const worker = new Worker('brain-worker.js?v=' + BUILD); const post = (url, obj) => { if (url === '/reset') worker.postMessage({ type: 'reset' }); return Promise.resolve(); }; let stepId = 0, pendingStep = null, brainRunning = false, memResolve = null; const fileFrac = {}; const FILE_MB = { 'indptr.dat': 0.28, 'delta0.dat': 7.3, 'delta1.dat': 7.3, 'delta2.dat': 7.3, 'delta3.dat': 7.3, 'weight.dat': 7.7 }; const brainMemory = { get: () => new Promise(res => { memResolve = res; worker.postMessage({ type: 'getMemory' }); setTimeout(() => { if (memResolve === res) { memResolve = null; res(null); } }, 2000); }), set: (u8) => worker.postMessage({ type: 'setMemory', data: u8 }), forget: () => worker.postMessage({ type: 'forget' }), clock: (s) => worker.postMessage({ type: 'clock', speed: s }), }; worker.onmessage = (e) => { const m = e.data; if (m.type === 'progress') { fileFrac[m.file] = m.frac; let got = 0; for (const [f, mb] of Object.entries(FILE_MB)) got += mb * (fileFrac[f] || 0); const frac = got / 37.2, pct = Math.round(frac * 100); setLoadStatus(`Loading the fly connectome… ${pct}% (one-time ~37 MB download)`, frac, false); $('err').style.display = 'block'; $('err').textContent = `Loading the fly connectome… ${pct}%`; } else if (m.type === 'status') { setLoadStatus(m.text, 0.98, false); $('err').style.display = 'block'; $('err').textContent = m.text; } else if (m.type === 'ready') { info = m.info; pos = info.pos; dnSet = new Set(info.dn); online = true; $('err').style.display = 'none'; brainRunning = true; setLoadStatus('Brain ready · 138,639 neurons loaded', 1, true); spawnFly(); lit.clear(); loadState(); brainMemory.clock(lifeSpeed); // restore the last autosave, if there is one } else if (m.type === 'memory') { if (memResolve) { const r = memResolve; memResolve = null; r(m.data); } } else if (m.type === 'out' && pendingStep) { pendingStep(m.out); pendingStep = null; } else if (m.type === 'error') { $('err').style.display = 'block'; $('err').textContent = 'Could not load the brain: ' + m.text; } }; async function brainLoop() { setLoadStatus('Starting…', 0, false); worker.postMessage({ type: 'init', v: BUILD }); while (!brainRunning) await new Promise(r => setTimeout(r, 200)); while (true) { const t0 = performance.now(); out = await new Promise(res => { pendingStep = res; worker.postMessage({ type: 'step', id: ++stepId, stim: (paused || landingOpen) ? {} : stim }); }); for (const i of out.spiking) lit.set(i, 1); await new Promise(r => setTimeout(r, Math.max(0, 30 - (performance.now() - t0)))); } } // ---- world ---------------------------------------------------- let items = [], tool = 'food', paused = false, fly; function spawnFly() { fly = { x: 120, y: H / 2, th: 0, speed: 0, legPhase: 0, wingPhase: 0, health: 100, energy: 0.8, alive: true, sgn: Math.random() < 0.5 ? -1 : 1, noise: 0, ouch: 0, label: '', buzz: 0, bump: 0 }; F = newFemale(); males = []; brood = []; npcF = []; smooth = { odor: 0, feed: 0, alarm: 0, escape: 0, lay: 0, recept: 0, vpo: 0 }; post('/reset', {}).catch(() => {}); } function addItem(type, x, y) { track('spawn-' + type); if (type === 'food') items.push({ type, x, y, r: 10, amount: 1 }); if (type === 'flame') items.push({ type, x, y, r: 22 }); if (type === 'zap') items.push({ type, x, y, r: 14, life: 0.4 }); if (type === 'spider') items.push({ type, x, y, r: 12, th: 0, phase: 0 }); if (type === 'lamp') items.push({ type, x, y, r: 12, hue: +$('lampColor').value }); if (type === 'poison') items.push({ type, x, y, r: 10 }); if (type === 'male') males.push({ x, y, th: Math.random() * 6.28, state: 'seek', t: 0, tryT: 0, phase: 0, wing: 0 }); } function addSegment(type, x1, y1, x2, y2) { if (Math.hypot(x2 - x1, y2 - y1) < 14) { y1 -= 40; y2 += 40; x2 = x1; } // a click makes a short vertical pane items.push({ type, x1, y1, x2, y2 }); } const isSeg = it => it.type === 'glass' || it.type === 'wall'; function nearestOnSeg(px, py, it) { const dx = it.x2 - it.x1, dy = it.y2 - it.y1, l2 = dx * dx + dy * dy || 1; const t = clamp(((px - it.x1) * dx + (py - it.y1) * dy) / l2); const x = it.x1 + t * dx, y = it.y1 + t * dy; return { x, y, d: Math.hypot(px - x, py - y) }; } function ccw(ax, ay, bx, by, cx, cy) { return (cy - ay) * (bx - ax) > (by - ay) * (cx - ax); } function segCross(ax, ay, bx, by, it) { return ccw(ax, ay, it.x1, it.y1, it.x2, it.y2) !== ccw(bx, by, it.x1, it.y1, it.x2, it.y2) && ccw(ax, ay, bx, by, it.x1, it.y1) !== ccw(ax, ay, bx, by, it.x2, it.y2); } // solid walls block smell, glass does not function occlusion(ax, ay, bx, by) { for (const it of items) if (it.type === 'wall' && segCross(ax, ay, bx, by, it)) return 0.25; return 1; } // Smell and routes go around obstacles, never through them. Glass blocks smell and movement // but not sight; walls block all three. function blockers(ax, ay, bx, by, opaqueOnly) { return items.filter(it => isSeg(it) && (!opaqueOnly || it.type === 'wall') && segCross(ax, ay, bx, by, it)); } function segEnds(it) { // the two tips, nudged outward so paths clear the pane const dx = it.x2 - it.x1, dy = it.y2 - it.y1, l = Math.hypot(dx, dy) || 1, ux = dx / l, uy = dy / l, pad = 16; return [[it.x1 - ux * pad, it.y1 - uy * pad], [it.x2 + ux * pad, it.y2 + uy * pad]]; } function detour(ax, ay, bx, by) { // shortest way round the blocking panes const bl = blockers(ax, ay, bx, by, false); if (!bl.length) return null; let best = null, bd = Infinity; for (const it of bl) for (const e of segEnds(it)) { const d = Math.hypot(e[0] - ax, e[1] - ay) + Math.hypot(bx - e[0], by - e[1]); if (d < bd) { bd = d; best = e; } } return { x: best[0], y: best[1], len: bd + 60 * (bl.length - 1) }; } function odorDist(ax, ay, bx, by) { // how far the odor molecules really travel const r = detour(ax, ay, bx, by); return r ? r.len : Math.hypot(bx - ax, by - ay); } function routeTarget(ax, ay, bx, by) { // where to head to reach b const r = detour(ax, ay, bx, by); return r ? { x: r.x, y: r.y, blocked: true } : { x: bx, y: by, blocked: false }; } // push a body out of every pane/wall; returns true if it touched one function collide(o, rad) { let hit = false; for (const it of items) { if (!isSeg(it)) continue; const n = nearestOnSeg(o.x, o.y, it); if (n.d < rad) { const nx = n.d > 0.01 ? (o.x - n.x) / n.d : 1, ny = n.d > 0.01 ? (o.y - n.y) / n.d : 0; o.x = n.x + nx * rad; o.y = n.y + ny * rad; hit = true; } } return hit; } // what the body's senses report this frame; the result is sent to the brain function sense() { const f = { x: Math.cos(fly.th), y: Math.sin(fly.th) }; const r = { x: -f.y, y: f.x }; const head = { x: fly.x + f.x * 13, y: fly.y + f.y * 13 }; const aL = { x: head.x + f.x * 4 - r.x * 9, y: head.y + f.y * 4 - r.y * 9 }; const aR = { x: head.x + f.x * 4 + r.x * 9, y: head.y + f.y * 4 + r.y * 9 }; let fL = 0, fR = 0, hL = 0, hR = 0, noL = 0, noR = 0, heat = 0, taste = null, I = 0, loom = 0, vis = 0, lamp = null; for (const it of items) { if (it.type === 'food') { fL += Math.exp(-odorDist(aL.x, aL.y, it.x, it.y) / 150); fR += Math.exp(-odorDist(aR.x, aR.y, it.x, it.y) / 150); if (Math.hypot(head.x - it.x, head.y - it.y) < it.r + 8) taste = it; } else if (it.type === 'poison') { // smells like food, hurts when touched fL += Math.exp(-odorDist(aL.x, aL.y, it.x, it.y) / 150); fR += Math.exp(-odorDist(aR.x, aR.y, it.x, it.y) / 150); if (Math.hypot(head.x - it.x, head.y - it.y) < it.r + 8) { I = Math.max(I, 0.6); it.hit = 0.6; } } else if (it.type === 'lamp') { // a coloured light she can see (walls hide it, glass does not) const d = Math.hypot(it.x - fly.x, it.y - fly.y); if (!blockers(fly.x, fly.y, it.x, it.y, true).length) { const v = clamp(1 - d / 320); if (v > vis) { vis = v; lamp = it; } } } else if (isSeg(it)) { continue; } else if (it.type === 'spider') { const d = Math.hypot(it.x - fly.x, it.y - fly.y); if (!blockers(fly.x, fly.y, it.x, it.y, true).length) // walls hide it; glass does not loom = Math.max(loom, clamp(1 - (d - 20) / 220)); // grows as it closes in if (d < it.r + 10) { I = Math.max(I, 1); it.hit = 1; } // bite } else { if (it.type === 'flame') { // flames give off CO2 and heat hL += Math.exp(-odorDist(aL.x, aL.y, it.x, it.y) / 80); hR += Math.exp(-odorDist(aR.x, aR.y, it.x, it.y) / 80); heat = Math.max(heat, Math.exp(-Math.max(0, Math.hypot(fly.x - it.x, fly.y - it.y) - it.r) / 30)); } const dx = it.x - fly.x, dy = it.y - fly.y, d = Math.hypot(dx, dy) || 1; const k = clamp(1 - (d - it.r) / 24); if (k > 0) { let lat = (dx * r.x + dy * r.y) / d; if (Math.abs(lat) < 0.15) lat += fly.sgn * 0.3; noL += k * clamp(0.5 - 0.5 * lat) * 2; noR += k * clamp(0.5 + 0.5 * lat) * 2; I = Math.max(I, k); it.hit = k; } } } const gain = 0.35 + 0.65 * (1 - fly.energy); // hunger raises odor sensitivity const sL = clamp(fL * 2 * gain), sR = clamp(fR * 2 * gain); const cL = clamp(hL * 1.5), cR = clamp(hR * 1.5); stim = { smell_L: sL, smell_R: sR, co2_L: cL, co2_R: cR, sugar: taste ? 1 : 0, touch: Math.max(I * I, fly.bump * 0.15), heat: Math.max(heat, I * 0.7), loom, mating_state: matingDrive(), uterus: layDrive(), cva: maleSmell(), song: maleSong(), contact: maleContact(), cue: vis, reward: taste ? 1 : 0, punish: I > 0.15 ? clamp(I * 1.3) : 0 }; return { sL, sR, cL, cR, noL, noR, taste, loom, vis, lamp }; } function tick(dt) { for (const it of items) if (it.type === 'zap') it.life -= dt; items = items.filter(it => !(it.type === 'zap' && it.life <= 0) && !(it.type === 'food' && it.amount <= 0)); const lifeDt = dt; if (!fly.alive) { // a dead fly has a silent brain stim = {}; if (!fly.silenced) { fly.silenced = true; lit.clear(); post('/reset', {}).catch(() => {}); } for (const n in smooth) smooth[n] = 0; for (const n of ['odor', 'feed', 'alarm', 'escape', 'lay', 'recept', 'vpo']) out[n] = 0; updateMales(lifeDt); updateBrood(lifeDt); return; } const s = sense(); const k = Math.min(1, dt * 6); // smooth the brain readouts for (const n of ['odor', 'feed', 'alarm', 'escape', 'lay', 'recept', 'vpo']) smooth[n] += (out[n] - smooth[n]) * k; const approach = clamp(smooth.odor * 1.6), feeding = smooth.feed > 0.12 && s.taste, alarmB = clamp(smooth.alarm * 1.3), esc = clamp(smooth.escape * 1.5), alarm = Math.max(alarmB, esc); // locomotion: the brain's descending neurons set go / feed / flee, a reflex steers fly.noise += (-fly.noise * 1.5 + rnd(-1, 1) * 6) * dt; let turn = fly.noise * 0.5 * (1 - 0.8 * approach), speed = 0.18 + 0.7 * approach; const odorSum = s.sL + s.sR + 1e-6; turn += approach * 4 * clamp(8 * (s.sR - s.sL) / odorSum, -1, 1) * clamp(odorSum * 4); // toward food odor const co2Sum = s.cL + s.cR; if (co2Sum > 0.05) { turn -= 4 * (s.cR - s.cL) / (co2Sum + 1e-6) * clamp(co2Sum * 3) + fly.sgn * clamp(co2Sum - 0.3) * 2; speed *= 0.8; } if (alarm > 0.05) { // flee: away from the side that hurts turn += alarmB * 7 * ((s.noL - s.noR) / (s.noL + s.noR + 1e-6)); speed = Math.max(speed, 0.4 + 0.9 * alarm); } if (esc > 0.05) { // escape: away from the nearest spider const sp = items.filter(i => i.type === 'spider').sort((a, b) => Math.hypot(a.x - fly.x, a.y - fly.y) - Math.hypot(b.x - fly.x, b.y - fly.y))[0]; if (sp) { const away = Math.atan2(fly.y - sp.y, fly.x - sp.x); turn += esc * 8 * Math.atan2(Math.sin(away - fly.th), Math.cos(away - fly.th)); } speed = Math.max(speed, 0.6 + 0.9 * esc); } if (s.vis > 0.15) { // learned valence of the lamp, read from the real MBONs const a0 = out.mb_base_approach || 1, v0 = out.mb_base_avoid || 1; const val = ((out.mb_approach - out.mb_avoid) - (a0 - v0)) / (a0 + v0 + 1e-6); if (Number.isFinite(val)) F.mem += (clamp(val, -1, 1) - F.mem) * Math.min(1, dt / 1.2); const k = clamp(F.mem * 1.6, -1, 1); if (Math.abs(k) > 0.12 && s.lamp) { // + approach the lamp, - avoid it const bearing = Math.atan2(s.lamp.y - fly.y, s.lamp.x - fly.x); const dAng = Math.atan2(Math.sin(bearing - fly.th), Math.cos(bearing - fly.th)); turn += k * 4 * (Math.abs(dAng) < 0.1 ? fly.sgn * 0.5 : Math.sign(dAng)) * clamp(Math.abs(dAng) * 1.5 + 0.3); speed = Math.max(speed, 0.25 + 0.7 * Math.abs(k)); } } { // a receptive female slows down for a courting male const m = nearestMale(); if (m && m.state === 'court' && Math.hypot(m.x - fly.x, m.y - fly.y) < 70) speed *= 1 - 0.85 * clamp(F.pc1 / 0.12); } if (feeding || F.cop > 0) speed = 0; if (F.reject > 0) { // kicks the male away and runs const m = nearestMale(); if (m) { const away = Math.atan2(fly.y - m.y, fly.x - m.x); turn += 6 * Math.atan2(Math.sin(away - fly.th), Math.cos(away - fly.th)); } speed = Math.max(speed, 0.9); } fly.th += clamp(turn, -4, 4) * dt; fly.speed = speed * 120; fly.x += Math.cos(fly.th) * fly.speed * dt; fly.y += Math.sin(fly.th) * fly.speed * dt; if (collide(fly, 9)) { // bumped into glass or a wall fly.bump = 1; fly.th += fly.sgn * 2.5 * dt; fly.bumped = 0.6; } else fly.bump = Math.max(0, fly.bump - dt * 3); fly.bumped = Math.max(0, (fly.bumped || 0) - dt); // spiders hunt the fly (a bit slower than a running fly) for (const it of items) if (it.type === 'spider') { const rt = routeTarget(it.x, it.y, fly.x, fly.y); const want = Math.atan2(rt.y - it.y, rt.x - it.x); it.th += Math.atan2(Math.sin(want - it.th), Math.cos(want - it.th)) * Math.min(1, 3 * dt); it.x += Math.cos(it.th) * 70 * dt; it.y += Math.sin(it.th) * 70 * dt; it.phase += dt * 14; collide(it, it.r); it.x = clamp(it.x, 12, W - 12); it.y = clamp(it.y, 12, H - 12); } const m = 24; if (fly.x < m || fly.x > W - m || fly.y < m || fly.y > H - m) { fly.x = clamp(fly.x, m, W - m); fly.y = clamp(fly.y, m, H - m); const to = Math.atan2(H / 2 - fly.y, W / 2 - fly.x); fly.th += Math.atan2(Math.sin(to - fly.th), Math.cos(to - fly.th)) * Math.min(1, 6 * dt); } fly.legPhase += fly.speed * dt * 0.35; fly.buzz = alarm; fly.wingPhase += dt * (30 + 80 * alarm); // metabolism if (feeding) { s.taste.amount -= dt * 0.35; fly.energy = clamp(fly.energy + dt * 0.2); } if (s.vis > 0.15 && (feeding || stim.punish > 0.3)) { F.trained += dt; track('training-started', true); } reproTick(dt, feeding); updateMales(dt); updateBrood(dt); { // metabolism on the fly-day clock: starving flies last ~1.5-3 days const act = clamp(fly.speed / 120); // walking burns more than resting fly.energy = clamp(fly.energy - (dt * lifeSpeed / DAY) * (0.40 + 0.35 * act)); } let dmg = 0; for (const it of items) if (it.hit) { dmg += it.hit * it.hit * (it.type === 'spider' ? 30 : 18); it.hit = 0; } if (fly.energy <= 0) dmg += 200 * lifeSpeed / DAY; // dies about half a fly-day after running out if (dmg > 0) fly.health -= dmg * dt; else if (fly.energy > 0.3 && alarm < 0.15) fly.health = Math.min(100, fly.health + 1.5 * dt); if (alarmB > 0.45) fly.ouch = 0.8; fly.ouch = Math.max(0, fly.ouch - dt); { // ageing: Gompertz mortality, median adult lifespan ~50 fly-days const dDay = dt * lifeSpeed / DAY; if (Math.random() < 0.0008 * Math.exp(0.09 * F.age) * dDay) { fly.cause = 'old age'; fly.health = 0; } } if (fly.health <= 0) { fly.health = 0; fly.alive = false; track('fly-died'); fly.cause = fly.cause || (fly.energy <= 0 ? 'starved' : 'injury'); } fly.label = !online ? 'brain offline' : !fly.alive ? 'dead (' + (fly.cause || 'injury') + ')' : F.cop > 0 ? 'mating' : F.reject > 0 ? 'rejecting a male' : F.layFlash > 0 ? 'laying an egg' : esc > 0.35 ? 'spider! escaping' : alarmB > 0.35 ? 'in pain, fleeing' : fly.bumped > 0 ? 'bumped into something' : feeding ? 'eating' : approach > 0.3 && Math.abs(s.sL - s.sR) > 0.0 ? 'smells food, approaching' : s.cL + s.cR > 0.1 ? 'smells CO2, avoiding' : fly.energy < 0.35 ? 'hungry, searching' : 'wandering'; } // ---- reproduction --------------------------------------------- // Female Drosophila reproductive tract: 2 ovaries x 16 ovarioles (oogenesis stages 1-14, // stage-8 nutrient checkpoint), uterus, 2 spermathecae + seminal receptacle (sperm storage), // sex-peptide post-mating response, oviposition site choice, then egg -> larva -> pupa -> adult. // The brain side is the real FlyWire circuit: the abdomen's mating state reaches the pC1 hub // through ascending neurons AN_SMP_2, and egg-laying is read from the oviDNa/oviDNb neurons. // One "fly-day" lasts DAY seconds at life cycle x1 (the arena clock is much faster than a fly's). const DAY = 120; let lifeSpeed = 1; let F = null, males = [], brood = [], npcF = []; function newFemale() { const ov = []; for (let i = 0; i < 32; i++) ov.push({ stage: 1 + Math.random() * 11, arrest: 0, mature: false }); return { age: 1.5, protein: 0.5, mated: false, spDays: 0, sperm: 0, uterus: 0, laid: 0, resorbed: 0, ovulated: 0, matings: 0, cop: 0, copMale: null, reject: 0, layCool: 0, layFlash: 0, ov, born: 0, pc1: 0, layAvg: 0, mem: 0, trained: 0 }; } // body -> brain signals function matingDrive() { // body-state signal into the ascending neurons AN_SMP_2 if (F.age < 0.3) return 0; // not yet sexually mature let d = 0.4; // receptive virgin d *= 0.3 + 0.7 * clamp(F.protein / 0.5); // poorly fed females are less receptive if (F.mated) d *= 0.1 + 0.9 * clamp(F.spDays / 7); // sex peptide: refuses males, wears off over ~a week return d; } function maleSong() { // a courting male's wing song, heard by the Johnston's organ let m = 0; for (const b of males) if (b.state === 'court' && b.wing > 0) { const d = Math.hypot(b.x - fly.x, b.y - fly.y); m = Math.max(m, 0.5 * clamp(1 - d / 70)); } return m; } function maleContact() { // a male touching / mounting her for (const b of males) if ((b.state === 'court' || b.state === 'cop') && Math.hypot(b.x - fly.x, b.y - fly.y) < 26) return 1; return 0; } function layDrive() { // egg in uterus x quality of the substrate here if (F.uterus <= 0) return 0; let q = 0.15; for (const it of items) { if (it.type === 'food' && Math.hypot(it.x - fly.x, it.y - fly.y) < 70) q = 1; if ((it.type === 'flame' && Math.hypot(it.x - fly.x, it.y - fly.y) < 90) || (it.type === 'spider' && Math.hypot(it.x - fly.x, it.y - fly.y) < 160)) q *= 0.2; } return (F.mated ? 1 : 0.3) * q; } function maleSmell() { let m = 0; for (const b of males) m = Math.max(m, clamp(1 - odorDist(b.x, b.y, fly.x, fly.y) / 120)); return m; } function nearestMale() { let best = null, bd = 1e9; for (const b of males) { const d = Math.hypot(b.x - fly.x, b.y - fly.y); if (d < bd) { bd = d; best = b; } } return best; } function reproTick(dt, feeding) { const d = dt * lifeSpeed / DAY; // fly-days elapsed F.age += d; if (feeding) F.protein = clamp(F.protein + dt * 0.5); F.protein = clamp(F.protein - d * 0.45); if (F.mated) F.spDays += d; F.sperm = Math.max(0, F.sperm * (1 - 0.01 * d)); // oogenesis // oogenesis: stage 1-14 takes ~3 days at 25 C unmated; mating (sex peptide, ecdysone) speeds it ~1.6x; // egg production is lower in very young and in old females const ageF = F.age < 1 ? 0.5 : F.age < 3 ? 0.8 : F.age < 20 ? 1 : Math.max(0.3, 1 - (F.age - 20) * 0.025); const speed = 5 * (0.25 + 0.75 * F.protein) * (F.mated ? 1.6 : 1) * ageF; for (const o of F.ov) { if (o.mature) { if (F.uterus < 3 && Math.random() < (F.mated ? 6 : 0.4) * d) { o.mature = false; o.stage = 1; F.uterus++; F.ovulated++; } continue; } if (F.age < 0.4) continue; if (o.stage >= 7.5 && o.stage < 8.5 && F.protein < 0.22) { // stage-8 checkpoint o.arrest += d; if (o.arrest > 1.0) { o.stage = 1; o.arrest = 0; F.resorbed++; } // egg chamber is resorbed continue; } o.arrest = 0; o.stage += speed * d; if (o.stage >= 14) { o.stage = 14; o.mature = true; } } F.pc1 += (smooth.recept - F.pc1) * Math.min(1, dt / 1.0); // pC1 activity averaged over about a second F.reject = Math.max(0, F.reject - dt); F.layFlash = Math.max(0, F.layFlash - dt); F.layCool = Math.max(0, F.layCool - dt); // copulation if (F.cop > 0) { F.cop -= dt; if (F.cop <= 0) { F.mated = true; F.spDays = 0; F.sperm = Math.min(600, F.sperm + 400); F.matings++; const m = F.copMale; F.copMale = null; if (m) { m.state = 'rest'; m.t = Math.max(4, 120 / 1440 * DAY / lifeSpeed); m.courtT = 0; m.need = rnd(25, 90); m.x -= Math.cos(fly.th) * 28; m.y -= Math.sin(fly.th) * 28; } } } // oviposition: the real oviDN neurons decide F.layAvg += (smooth.lay - F.layAvg) * Math.min(1, dt / 0.5); // egg-laying neuron activity over ~0.5 s if (F.uterus > 0 && F.layAvg > 0.35 && F.layCool <= 0 && F.cop <= 0) { F.uterus--; F.laid++; F.layCool = 2.5; F.layFlash = 0.9; const fert = F.sperm >= 1; if (fert) F.sperm -= 1; brood.push({ type: 'egg', x: fly.x - Math.cos(fly.th) * 12, y: fly.y - Math.sin(fly.th) * 12, age: 0, fert, th: Math.random() * 6.28 }); } } function updateMales(dt) { for (const m of males) { m.phase += dt * 12; if (m.state === 'cop') { m.x = fly.x - Math.cos(fly.th) * 14; m.y = fly.y - Math.sin(fly.th) * 14; m.th = fly.th; m.wing = 0; continue; } if (m.state === 'rest') { m.t -= dt; m.wing = 0; m.th += rnd(-1, 1) * dt * 3; m.x += Math.cos(m.th) * 12 * dt; m.y += Math.sin(m.th) * 12 * dt; if (m.t <= 0) m.state = 'seek'; } else { const dx = fly.x - m.x, dy = fly.y - m.y, d = Math.hypot(dx, dy); const rt = routeTarget(m.x, m.y, fly.x, fly.y); let want = Math.atan2(rt.y - m.y, rt.x - m.x), sp = 95; if (!fly.alive) { want = m.th + rnd(-1, 1); sp = 20; m.state = 'seek'; m.wing = 0; } else if (d > 50 || rt.blocked) { m.state = 'seek'; m.wing = 0; } else { m.state = 'court'; m.wing = 1; sp = d > 22 ? 60 : 10; } // wing extension = courtship song m.th += Math.atan2(Math.sin(want - m.th), Math.cos(want - m.th)) * Math.min(1, 4 * dt); m.x += Math.cos(m.th) * sp * dt; m.y += Math.sin(m.th) * sp * dt; if (m.state === 'court') m.courtT = (m.courtT || 0) + dt; if (m.need === undefined) m.need = rnd(25, 90); if (m.state === 'court' && d < 24 && F.cop <= 0 && F.age > 0.3 && m.courtT >= m.need) { m.tryT += dt; if (m.tryT > 1.5) { m.tryT = 0; if (F.pc1 > 0.07 && smooth.vpo > 0.3) { F.cop = Math.max(2.5, 20 / 1440 * DAY / lifeSpeed); F.copMale = m; m.state = 'cop'; } // pC1 active and DNp13 opens the vaginal plate else { F.reject = 0.8; m.state = 'rest'; m.t = 25; m.courtT = 0; m.need = rnd(25, 90); } // ovipositor extrusion; he gives up for a while (courtship conditioning) } } } collide(m, 7); m.x = clamp(m.x, 10, W - 10); m.y = clamp(m.y, 10, H - 10); } for (const b of npcF) { // new adult females just wander b.th += rnd(-1, 1) * dt * 4; b.phase += dt * 10; b.x += Math.cos(b.th) * 30 * dt; b.y += Math.sin(b.th) * 30 * dt; collide(b, 7); b.x = clamp(b.x, 10, W - 10); b.y = clamp(b.y, 10, H - 10); if (b.x <= 10 || b.x >= W - 10 || b.y <= 10 || b.y >= H - 10) b.th += 3.14; } } function updateBrood(dt) { const d = dt * lifeSpeed / DAY; for (const b of brood) { b.age += d; if (b.type === 'egg') { if (!b.fert && b.age > 0.4) { b.dead = true; continue; } // unfertilized eggs do not develop if (b.age >= 1) { b.type = 'larva'; b.hunger = 0; } } else if (b.type === 'larva') { let f = null, fd = 260; for (const it of items) if (it.type === 'food') { const dd = Math.hypot(it.x - b.x, it.y - b.y); if (dd < fd) { fd = dd; f = it; } } if (f && fd > 10) { const rt = routeTarget(b.x, b.y, f.x, f.y); const want = Math.atan2(rt.y - b.y, rt.x - b.x); b.th += Math.atan2(Math.sin(want - b.th), Math.cos(want - b.th)) * Math.min(1, 5 * dt); b.x += Math.cos(b.th) * 14 * dt; b.y += Math.sin(b.th) * 14 * dt; } if (f && fd <= 14) { f.amount -= 0.006 * dt; b.hunger = 0; } else b.hunger += d; if (b.hunger > 1.3) { b.dead = true; continue; } // starved if (b.age >= 5) b.type = 'pupa'; } else if (b.type === 'pupa' && b.age >= 9) { b.dead = true; F.born++; track('offspring-born'); const male = Math.random() < 0.5; const a = { x: b.x, y: b.y, th: Math.random() * 6.28, phase: 0 }; if (male) males.push({ ...a, state: 'rest', t: 3, tryT: 0, wing: 0 }); else npcF.push(a); } } brood = brood.filter(b => !b.dead); } function simpleFly(x, y, th, body, scale, wingOut, phase) { g.save(); g.translate(x, y); g.rotate(th); g.scale(scale, scale); g.strokeStyle = '#2d2015'; g.lineWidth = 1.3; for (let i = 0; i < 6; i++) { const side = i % 2 ? 1 : -1, row = Math.floor(i / 2), ph = Math.sin(phase + i * 2.1) * 4, bx = 6 - row * 6; g.beginPath(); g.moveTo(bx, 0); g.lineTo(bx + ph, side * 11); g.lineTo(bx + ph * 0.4, side * 16); g.stroke(); } g.fillStyle = 'rgba(190,220,255,0.35)'; g.save(); g.translate(6, -4); g.rotate(-0.5 - (wingOut ? 0.9 + Math.sin(phase * 2.5) * 0.25 : 0)); g.beginPath(); g.ellipse(-10, 0, 15, 4.5, 0, 0, 7); g.fill(); g.restore(); g.save(); g.translate(6, 4); g.rotate(0.5); g.beginPath(); g.ellipse(-10, 0, 15, 4.5, 0, 0, 7); g.fill(); g.restore(); g.fillStyle = body; g.beginPath(); g.ellipse(-10, 0, 11, 6, 0, 0, 7); g.fill(); g.fillStyle = '#15100a'; g.beginPath(); g.ellipse(-18, 0, 4, 5.5, 0, 0, 7); g.fill(); // dark abdomen tip g.fillStyle = body; g.beginPath(); g.ellipse(4, 0, 7, 5.5, 0, 0, 7); g.fill(); g.beginPath(); g.ellipse(12, 0, 4.8, 4.8, 0, 0, 7); g.fill(); g.fillStyle = '#c01f2b'; g.beginPath(); g.arc(13, -3.5, 2.6, 0, 7); g.arc(13, 3.5, 2.6, 0, 7); g.fill(); g.restore(); } function drawMales() { for (const m of males) simpleFly(m.x, m.y, m.th, '#5a3f26', 0.85, m.wing > 0, m.phase); for (const b of npcF) simpleFly(b.x, b.y, b.th, '#7b5a35', 1.0, false, b.phase); for (const m of males) if (m.wing > 0 && m.state === 'court') { // song g.strokeStyle = 'rgba(160,210,255,0.35)'; g.lineWidth = 1; for (let k = 1; k <= 2; k++) { g.beginPath(); g.arc(m.x, m.y, 12 + k * 7 + (m.phase % 3), 0, 7); g.stroke(); } } } function drawBrood() { for (const b of brood) { if (b.type === 'egg') { g.save(); g.translate(b.x, b.y); g.rotate(b.th); g.fillStyle = b.fert ? '#f4efe0' : '#8a8a80'; g.beginPath(); g.ellipse(0, 0, 4.5, 2.3, 0, 0, 7); g.fill(); g.restore(); } else if (b.type === 'larva') { const len = 5 + b.age * 1.6; g.save(); g.translate(b.x, b.y); g.rotate(b.th); g.fillStyle = '#efe6c8'; for (let i = 0; i < 5; i++) { g.beginPath(); g.ellipse(-i * len / 5, 0, len / 6 + 0.8, 2 + b.age * 0.2, 0, 0, 7); g.fill(); } g.restore(); } else { g.save(); g.translate(b.x, b.y); g.rotate(b.th); g.fillStyle = b.age > 8 ? '#3b2a1a' : '#8a5a2b'; g.beginPath(); g.ellipse(0, 0, 6, 3.2, 0, 0, 7); g.fill(); g.restore(); } } } function drawRepro() { const c = $('repro').getContext('2d'), w = 360, h = 290; c.clearRect(0, 0, w, h); c.font = '10px system-ui'; c.textBaseline = 'middle'; c.fillStyle = '#8a94a3'; c.fillText('Reproductive tract (female)', 10, 10); // ovarioles: germarium at the top, oldest chamber at the bottom for (let side = 0; side < 2; side++) { const x0 = side ? 200 : 30; for (let j = 0; j < 16; j++) { const o = F.ov[side * 16 + j], x = x0 + j * 8.2; c.strokeStyle = 'rgba(120,140,170,0.45)'; c.lineWidth = 1; c.beginPath(); c.moveTo(x, 24); c.lineTo(x, 102); c.stroke(); const y = 24 + (o.stage / 14) * 74, r = 1.8 + o.stage * 0.28; c.fillStyle = o.mature ? '#f6f3e6' : o.stage >= 7 ? '#e6c75a' : '#7fa8d6'; c.beginPath(); c.ellipse(x, y, r * 0.8, r, 0, 0, 7); c.fill(); if (o.arrest > 0) { c.strokeStyle = '#e5534b'; c.lineWidth = 1.5; c.stroke(); } } } // ducts c.strokeStyle = '#6f7d92'; c.lineWidth = 2; c.beginPath(); c.moveTo(94, 106); c.quadraticCurveTo(110, 135, 178, 142); c.moveTo(264, 106); c.quadraticCurveTo(250, 135, 182, 142); c.moveTo(180, 142); c.lineTo(180, 170); c.stroke(); // uterus + eggs waiting c.fillStyle = '#242c3a'; c.strokeStyle = '#6f7d92'; c.lineWidth = 1.5; c.beginPath(); c.ellipse(180, 190, 30, 18, 0, 0, 7); c.fill(); c.stroke(); c.fillStyle = '#f6f3e6'; for (let i = 0; i < F.uterus; i++) { c.beginPath(); c.ellipse(164 + i * 16, 190, 6, 3.4, 0, 0, 7); c.fill(); } // spermathecae + seminal receptacle const fill = clamp(F.sperm / 600); c.strokeStyle = '#6f7d92'; c.lineWidth = 1.2; for (const [sx, sy] of [[132, 162], [228, 162]]) { c.beginPath(); c.moveTo(sx > 180 ? 205 : 155, 178); c.lineTo(sx, sy + 6); c.stroke(); c.fillStyle = '#1b1f2a'; c.beginPath(); c.arc(sx, sy, 9, 0, 7); c.fill(); c.stroke(); c.fillStyle = `rgba(200,120,230,${0.15 + 0.85 * fill})`; c.beginPath(); c.arc(sx, sy, 3 + 5 * fill, 0, 7); c.fill(); } c.beginPath(); c.moveTo(180, 207); c.bezierCurveTo(165, 222, 195, 222, 180, 236); c.stroke(); c.fillStyle = `rgba(200,120,230,${0.2 + 0.8 * fill})`; c.beginPath(); c.arc(180, 238, 4, 0, 7); c.fill(); c.fillStyle = '#8a94a3'; c.textAlign = 'left'; c.fillText('spermathecae', 76, 166); c.fillText('uterus', 218, 196); c.fillText('receptacle', 198, 238); // text const mature = F.ov.filter(o => o.mature).length; const status = F.age < 0.3 ? 'immature' : !F.mated ? 'virgin, receptive' : `mated ${F.spDays.toFixed(1)} d ago, sperm ${Math.round(F.sperm)}`; const eggs = brood.filter(b => b.type === 'egg').length, lar = brood.filter(b => b.type === 'larva').length, pup = brood.filter(b => b.type === 'pupa').length; c.fillStyle = '#d7dde6'; c.fillText(`${status} · age ${F.age.toFixed(1)} d · protein ${Math.round(F.protein * 100)}%`, 10, 258); c.fillText(`mature eggs ${mature} · in uterus ${F.uterus} · laid ${F.laid} · resorbed ${F.resorbed}`, 10, 272); c.fillText(`offspring: ${eggs} eggs · ${lar} larvae · ${pup} pupae · ${F.born} adults (${males.length} ♂ now)`, 10, 286 - 0); } // ---- drawing -------------------------------------------------- function drawItems(t) { for (const it of items) { if (it.type === 'lamp') { const gl = 1 + 0.06 * Math.sin(t * 5 + it.x); const grd = g.createRadialGradient(it.x, it.y, 2, it.x, it.y, 70 * gl); grd.addColorStop(0, `hsla(${it.hue},90%,65%,0.55)`); grd.addColorStop(1, `hsla(${it.hue},90%,60%,0)`); g.fillStyle = grd; g.beginPath(); g.arc(it.x, it.y, 70 * gl, 0, 7); g.fill(); g.fillStyle = `hsl(${it.hue},90%,70%)`; g.beginPath(); g.arc(it.x, it.y, 8, 0, 7); g.fill(); g.strokeStyle = 'rgba(255,255,255,0.8)'; g.lineWidth = 1.5; g.stroke(); continue; } if (it.type === 'poison') { g.fillStyle = '#7a3fa0'; g.beginPath(); g.arc(it.x, it.y, 10, 0, 7); g.fill(); g.strokeStyle = '#c9a0e8'; g.lineWidth = 1.5; g.stroke(); g.fillStyle = '#e8d0f8'; g.font = '10px system-ui'; g.fillText('x', it.x - 3, it.y + 3); continue; } if (it.type === 'glass') { g.lineCap = 'round'; g.strokeStyle = 'rgba(150,215,255,0.28)'; g.lineWidth = 9; g.beginPath(); g.moveTo(it.x1, it.y1); g.lineTo(it.x2, it.y2); g.stroke(); g.strokeStyle = 'rgba(220,245,255,0.75)'; g.lineWidth = 1.5; g.beginPath(); g.moveTo(it.x1, it.y1); g.lineTo(it.x2, it.y2); g.stroke(); continue; } if (it.type === 'wall') { g.lineCap = 'round'; g.strokeStyle = '#59616e'; g.lineWidth = 9; g.beginPath(); g.moveTo(it.x1, it.y1); g.lineTo(it.x2, it.y2); g.stroke(); g.strokeStyle = '#8a94a3'; g.lineWidth = 1.5; g.beginPath(); g.moveTo(it.x1, it.y1); g.lineTo(it.x2, it.y2); g.stroke(); continue; } if (it.type === 'spider') { g.save(); g.translate(it.x, it.y); g.rotate(it.th); g.strokeStyle = '#2a2230'; g.lineWidth = 2; for (let i = 0; i < 8; i++) { const side = i % 2 ? 1 : -1, k = Math.floor(i / 2), a = 0.6 + k * 0.5 + Math.sin(it.phase + i) * 0.2; g.beginPath(); g.moveTo(2 - k * 3, 0); g.lineTo(2 - k * 3 + Math.cos(a) * 14, side * Math.sin(a) * 16); g.lineTo(2 - k * 3 + Math.cos(a) * 24, side * Math.sin(a) * 12); g.stroke(); } g.fillStyle = '#3a2d40'; g.beginPath(); g.ellipse(-6, 0, 11, 8, 0, 0, 7); g.fill(); g.beginPath(); g.ellipse(7, 0, 7, 6, 0, 0, 7); g.fill(); g.fillStyle = '#e5534b'; g.beginPath(); g.arc(11, -2, 1.6, 0, 7); g.arc(11, 2, 1.6, 0, 7); g.fill(); g.restore(); continue; } if (it.type === 'food') { const grd = g.createRadialGradient(it.x, it.y, 2, it.x, it.y, 90); grd.addColorStop(0, 'rgba(120,220,120,0.16)'); grd.addColorStop(1, 'rgba(120,220,120,0)'); g.fillStyle = grd; g.beginPath(); g.arc(it.x, it.y, 90, 0, 7); g.fill(); g.fillStyle = '#6cc04a'; g.beginPath(); g.arc(it.x, it.y, 4 + 6 * it.amount, 0, 7); g.fill(); g.strokeStyle = '#b9f08c'; g.lineWidth = 1.5; g.stroke(); } else if (it.type === 'flame') { const fl = 1 + 0.12 * Math.sin(t * 14 + it.x); const grd = g.createRadialGradient(it.x, it.y, 2, it.x, it.y, (it.r + 24) * fl); grd.addColorStop(0, 'rgba(255,200,60,0.95)'); grd.addColorStop(0.5, 'rgba(240,90,30,0.7)'); grd.addColorStop(1, 'rgba(240,60,20,0)'); g.fillStyle = grd; g.beginPath(); g.arc(it.x, it.y, (it.r + 24) * fl, 0, 7); g.fill(); } else { g.strokeStyle = `rgba(150,200,255,${clamp(it.life * 2.5)})`; g.lineWidth = 2; g.beginPath(); g.arc(it.x, it.y, it.r * (1.4 - it.life), 0, 7); g.stroke(); } } } function drawFly() { const hurt = fly.ouch > 0, dead = !fly.alive; g.save(); g.translate(fly.x, fly.y); g.rotate(fly.th); const body = dead ? '#555' : hurt ? '#a8482f' : '#6b4a2b'; g.strokeStyle = dead ? '#444' : '#2d2015'; g.lineWidth = 1.4; for (let i = 0; i < 6; i++) { const side = i % 2 ? 1 : -1, row = Math.floor(i / 2); const ph = Math.sin(fly.legPhase + i * 2.1) * (dead ? 0 : 5); const bx = 6 - row * 6; g.beginPath(); g.moveTo(bx, 0); g.lineTo(bx + ph, side * 12); g.lineTo(bx + ph * 0.4, side * 18); g.stroke(); } const flap = dead ? 0 : Math.sin(fly.wingPhase) * 0.5 * (0.3 + fly.buzz); g.fillStyle = dead ? 'rgba(150,150,150,0.25)' : 'rgba(190,220,255,0.35)'; for (const side of [-1, 1]) { g.save(); g.translate(6, side * 4); g.rotate(side * (0.5 + flap)); g.beginPath(); g.ellipse(-10, 0, 17, 5, 0, 0, 7); g.fill(); g.restore(); } g.fillStyle = body; g.beginPath(); g.ellipse(-12, 0, 13, 7, 0, 0, 7); g.fill(); g.strokeStyle = '#1b130b'; g.lineWidth = 1.5; for (let i = -20; i < -4; i += 5) { g.beginPath(); g.moveTo(i, -6); g.lineTo(i, 6); g.stroke(); } g.fillStyle = body; g.beginPath(); g.ellipse(4, 0, 8, 6.5, 0, 0, 7); g.fill(); g.beginPath(); g.ellipse(13, 0, 5.5, 5.5, 0, 0, 7); g.fill(); g.fillStyle = dead ? '#333' : '#c01f2b'; g.beginPath(); g.arc(14, -4, 3, 0, 7); g.arc(14, 4, 3, 0, 7); g.fill(); g.strokeStyle = '#2d2015'; g.lineWidth = 1.2; g.beginPath(); g.moveTo(17, -2); g.lineTo(21, -9); g.moveTo(17, 2); g.lineTo(21, 9); g.stroke(); g.restore(); if (hurt && !dead) { g.fillStyle = '#ffd7d2'; g.font = 'bold 13px system-ui'; g.fillText('ouch!', fly.x - 14, fly.y - 26); } } function drawWorld(t) { g.clearRect(0, 0, W, H); g.strokeStyle = 'rgba(255,255,255,0.03)'; g.lineWidth = 1; for (let x = 0; x < W; x += 40) { g.beginPath(); g.moveTo(x, 0); g.lineTo(x, H); g.stroke(); } for (let y = 0; y < H; y += 40) { g.beginPath(); g.moveTo(0, y); g.lineTo(W, y); g.stroke(); } drawItems(t); drawBrood(); drawMales(); drawFly(); if (dragStart && (tool === 'glass' || tool === 'wall')) { g.strokeStyle = 'rgba(255,255,255,0.4)'; g.setLineDash([6, 6]); g.lineWidth = 2; g.beginPath(); g.moveTo(dragStart.x, dragStart.y); g.lineTo(dragNow.x, dragNow.y); g.stroke(); g.setLineDash([]); } } // real neuron positions; lit when spiking const BW = 360, BH = 300; let baseLayer = null; function bx(i) { return 20 + pos[2 * i] / 1000 * (BW - 40); } function by(i) { return 14 + pos[2 * i + 1] / 1000 * (BH - 28); } function drawBrain() { bg.clearRect(0, 0, BW, BH); if (!pos) { bg.fillStyle = '#8a94a3'; bg.font = '12px system-ui'; bg.fillText('loading the connectome…', 20, 30); return; } if (!baseLayer) { baseLayer = document.createElement('canvas'); baseLayer.width = BW; baseLayer.height = BH; const c = baseLayer.getContext('2d'); c.fillStyle = 'rgba(120,140,170,0.16)'; for (let i = 0; i < pos.length / 2; i += 2) if (pos[2 * i] >= 0) c.fillRect(bx(i), by(i), 1, 1); } bg.drawImage(baseLayer, 0, 0); for (const [i, b] of lit) { const a = b; bg.fillStyle = dnSet.has(i) ? `rgba(255,170,60,${a})` : `rgba(110,230,215,${a * 0.9})`; const s = dnSet.has(i) ? 3 : 2; bg.fillRect(bx(i) - s / 2, by(i) - s / 2, s, s); if (b < 0.06) lit.delete(i); else lit.set(i, b * 0.86); } } function drawHud() { const set = (k, v, txt) => { $('b' + k).style.width = (clamp(v) * 100) + '%'; $('v' + k).textContent = txt; }; set('Odor', smooth.odor, smooth.odor.toFixed(2)); set('Feed', smooth.feed, smooth.feed.toFixed(2)); set('Alarm', smooth.alarm, smooth.alarm.toFixed(2)); set('Escape', smooth.escape, smooth.escape.toFixed(2)); set('Recept', smooth.recept, smooth.recept.toFixed(2)); set('Lay', smooth.lay, smooth.lay.toFixed(2)); set('Vpo', smooth.vpo, smooth.vpo.toFixed(2)); { const m = F.mem; $('bMem').style.width = (clamp(Math.abs(m)) * 100) + '%'; $('bMem').style.background = m >= 0 ? '#6bd16b' : '#e5534b'; $('vMem').textContent = (m >= 0 ? '+' : '') + m.toFixed(2); } set('Health', fly.health / 100, Math.round(fly.health)); set('Energy', fly.energy, Math.round(fly.energy * 100) + '%'); $('state').textContent = 'Fly is ' + fly.label; if (info) { $('spk').textContent = Math.round(out.spikes_per_ms * 1000).toLocaleString() + ' spikes/s'; const top = !fly.alive ? 'none (brain silent)' : out.top_dn.map(([n, c]) => `${n} ${c}`).join(' · ') || 'none'; $('stats').innerHTML = `${info.neurons.toLocaleString()} neurons · ${info.synapses.toLocaleString()} synapses<br>` + `${Math.round(out.spikes_per_ms * 1000).toLocaleString()} spikes/s now · compute ${out.compute_ms} ms per 30 ms<br>` + `most active descending neurons: ${top}<br>` + `learning: ${((out.depressed || 0) * 100).toFixed(1)}% of Kenyon cell → MBON synapses weakened`; } } // ---- input / loop --------------------------------------------- let mouseDown = false, zapTimer = 0; function pointer(e) { const r = world.getBoundingClientRect(); return { x: (e.clientX - r.left) * W / r.width, y: (e.clientY - r.top) * H / r.height }; } let dragStart = null, dragNow = null; world.addEventListener('mousedown', e => { mouseDown = true; const p = pointer(e); if (tool === 'glass' || tool === 'wall') { dragStart = p; dragNow = p; } else addItem(tool, p.x, p.y); }); window.addEventListener('mouseup', () => { mouseDown = false; if (dragStart) { addSegment(tool, dragStart.x, dragStart.y, dragNow.x, dragNow.y); dragStart = null; } }); world.addEventListener('mousemove', e => { const p = pointer(e); if (dragStart) dragNow = p; if (mouseDown && tool === 'zap' && zapTimer <= 0) { addItem('zap', p.x, p.y); zapTimer = 0.15; } }); function setTool(t) { tool = t; document.querySelectorAll('[data-tool]').forEach(b => b.classList.toggle('on', b.dataset.tool === t)); } document.querySelectorAll('[data-tool]').forEach(b => b.onclick = () => setTool(b.dataset.tool)); $('clear').onclick = () => { items = []; }; $('respawn').onclick = () => { track('respawn'); spawnFly(); }; $('pause').onclick = () => { paused = !paused; $('pause').textContent = paused ? 'Resume' : 'Pause'; $('pause').classList.toggle('on', paused); }; const togglePanels = () => { const h = document.querySelector('aside').classList.toggle('hidden'); $('panels').textContent = h ? 'Show panels' : 'Hide panels'; resizeWorld(); }; $('panels').onclick = () => { track('panels-toggle'); togglePanels(); }; // ---- saves (slots, autosave, export / import) ----------------- const b64 = u8 => { let s = ''; for (let i = 0; i < u8.length; i += 8192) s += String.fromCharCode.apply(null, u8.subarray(i, i + 8192)); return btoa(s); }; const unb64 = t => Uint8Array.from(atob(t), c => c.charCodeAt(0)); const slotKey = s => s === 'auto' ? 'flybrain-save-v1' : 'flybrain-slot-' + s; let lastMem = null, toastT = null, autosaveOn = true; try { autosaveOn = localStorage.getItem('flybrain-autosave') !== '0'; } catch (e) {} function toast(msg) { const t = $('toast'); t.textContent = msg; t.classList.add('show'); clearTimeout(toastT); toastT = setTimeout(() => t.classList.remove('show'), 1900); } async function snapshot() { const mem = await brainMemory.get(); if (mem) lastMem = mem; return { v: 1, items: items.filter(i => i.type !== 'zap'), males, brood, npcF, F: { ...F, copMale: null, cop: 0 }, fly, lifeSpeed, mem: lastMem ? b64(lastMem) : null, t: Date.now() }; } async function saveSlot(slot, quiet) { try { localStorage.setItem(slotKey(slot), JSON.stringify(await snapshot())); if (!quiet) toast(slot === 'auto' ? 'Autosaved' : 'Saved to slot ' + slot); } catch (e) { if (!quiet) toast('Could not save (browser storage is full or blocked)'); } renderSlots(); } function applySnapshot(d) { if (!d || !d.F || !d.fly || !Array.isArray(d.items)) return false; items = d.items; males = d.males || []; brood = d.brood || []; npcF = d.npcF || []; F = d.F; fly = d.fly; lifeSpeed = d.lifeSpeed || lifeSpeed; if (!Number.isFinite(F.mem)) F.mem = 0; if (!Number.isFinite(F.trained)) F.trained = 0; for (const k of ['age', 'protein', 'spDays', 'sperm', 'pc1', 'layAvg']) if (!Number.isFinite(F[k])) F[k] = 0; fly.x = clamp(fly.x, 24, W - 24); fly.y = clamp(fly.y, 24, H - 24); fly.silenced = false; $('speed').textContent = 'Life cycle ×' + lifeSpeed; if (brainMemory.clock) brainMemory.clock(lifeSpeed); if (d.mem) brainMemory.set(unb64(d.mem)); else brainMemory.forget(); return true; } function loadSlot(slot, quiet) { try { const t = localStorage.getItem(slotKey(slot)); if (!t) { if (!quiet) toast('Nothing saved there yet'); return false; } const ok = applySnapshot(JSON.parse(t)); if (ok && !quiet) toast(slot === 'auto' ? 'Autosave loaded' : 'Loaded slot ' + slot); return ok; } catch (e) { if (!quiet) toast('That save could not be read'); return false; } } function loadState() { return loadSlot('auto', true); } function slotSummary(slot) { try { const t = localStorage.getItem(slotKey(slot)); if (!t) return null; const d = JSON.parse(t), when = new Date(d.t); const mem = Number.isFinite(d.F.mem) ? d.F.mem : 0; return `${when.toLocaleDateString([], { month: 'short', day: 'numeric' })} ${when.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}` + ` · age ${d.F.age.toFixed(1)} d · ${d.F.matings || 0} mating${d.F.matings === 1 ? '' : 's'} · ${d.F.laid || 0} eggs · lamp memory ${mem >= 0 ? '+' : ''}${mem.toFixed(2)}`; } catch (e) { return 'unreadable'; } } function renderSlots() { for (const slot of ['auto', '1', '2', '3']) { const row = document.querySelector(`.slot[data-slot="${slot}"]`); if (!row) continue; const s = slotSummary(slot); $('slotInfo' + slot).textContent = s || 'empty'; for (const b of row.querySelectorAll('button')) if (b.dataset.act === 'load' || b.dataset.act === 'del') b.disabled = !s; } } async function exportFile() { const d = await snapshot(); const a = document.createElement('a'); a.href = URL.createObjectURL(new Blob([JSON.stringify(d)], { type: 'application/json' })); a.download = 'fly-brain-save.json'; a.click(); setTimeout(() => URL.revokeObjectURL(a.href), 3000); toast('Exported fly-brain-save.json'); } $('savesBtn').onclick = (e) => { e.stopPropagation(); renderSlots(); $('savesMenu').classList.toggle('hidden'); }; document.addEventListener('click', (e) => { if (!e.target.closest('.menu')) $('savesMenu').classList.add('hidden'); }); $('savesMenu').addEventListener('click', async (e) => { const btn = e.target.closest('button[data-act]'); if (!btn) return; const slot = btn.closest('.slot').dataset.slot, act = btn.dataset.act; if (act === 'save') { track('save-slot'); await saveSlot(slot, false); } else if (act === 'load') { track('load-slot'); loadSlot(slot, false); } else if (act === 'del') { try { localStorage.removeItem(slotKey(slot)); } catch (er) {} renderSlots(); toast('Slot ' + slot + ' cleared'); } }); $('exportBtn').onclick = () => { track('export-save'); exportFile(); }; $('importFile').onchange = (e) => { const f = e.target.files[0]; if (!f) return; const r = new FileReader(); r.onload = () => { try { toast(applySnapshot(JSON.parse(r.result)) ? 'Imported ' + f.name : 'That file is not a Fly Brain save'); } catch (er) { toast('That file is not a Fly Brain save'); } }; r.readAsText(f); e.target.value = ''; }; $('forget').onclick = () => { track('forget-memory'); brainMemory.forget(); F.mem = 0; F.trained = 0; toast('Learned memory erased'); }; $('autosave').checked = autosaveOn; $('autosave').onchange = (e) => { autosaveOn = e.target.checked; try { localStorage.setItem('flybrain-autosave', autosaveOn ? '1' : '0'); } catch (er) {} }; setInterval(() => { if (autosaveOn && !paused && !landingOpen && fly && fly.alive) saveSlot('auto', true); }, 15000); // lamp colour swatches document.querySelectorAll('.sw').forEach(b => b.onclick = () => { document.querySelectorAll('.sw').forEach(x => x.classList.toggle('sel', x === b)); $('lampColor').value = b.dataset.hue; setTool('lamp'); }); $('speed').onclick = () => { const o = [1, 3, 10, 30]; lifeSpeed = o[(o.indexOf(lifeSpeed) + 1) % o.length]; $('speed').textContent = 'Life cycle ×' + lifeSpeed; if (brainMemory.clock) brainMemory.clock(lifeSpeed); }; window.addEventListener('keydown', e => { if (landingOpen) return; if (e.key === '1') setTool('food'); if (e.key === '2') setTool('flame'); if (e.key === '3') setTool('zap'); if (e.key === '4') setTool('glass'); if (e.key === '5') setTool('wall'); if (e.key === '6') setTool('spider'); if (e.key === '7') setTool('male'); if (e.key === '8') setTool('lamp'); if (e.key === '9') setTool('poison'); if (e.key === 'c' || e.key === 'C') items = []; if (e.key === 'r' || e.key === 'R') spawnFly(); if (e.key === ' ') { $('pause').click(); e.preventDefault(); } if (e.key === 'Tab') { togglePanels(); e.preventDefault(); } }); // ---- landing page ---------------------------------------------- let landingOpen = true; function setLoadStatus(text, frac, ready) { $('bootText').textContent = text; $('bootFill').style.width = Math.round((frac || 0) * 100) + '%'; $('boot').classList.toggle('ready', !!ready); } function enterSim() { track('enter-sim'); landingOpen = false; $('landing').classList.add('gone'); resizeWorld(); } function showHome() { landingOpen = true; $('landing').classList.remove('gone'); $('landing').scrollTop = 0; } $('enterBtn').onclick = enterSim; $('enterTop').onclick = enterSim; $('homeBtn').onclick = showHome; if (location.hash === '#sim') { landingOpen = false; $('landing').classList.add('gone'); } (async function hero() { const cv = $('heroCanvas'), cx = cv.getContext('2d'); let P, C; try { const r = await fetch('brain3d.json?v=' + (typeof BUILD !== 'undefined' ? BUILD : '0')); const d = await r.json(); P = Float32Array.from(d.p, v => v / 1000); C = d.c; } catch (e) { return; } const n = C.length, base = [[111, 140, 255], [63, 224, 200], [255, 180, 76], [255, 111, 168]]; const cols = base.map(c => Array.from({ length: 8 }, (_, l) => `rgba(${c[0]},${c[1]},${c[2]},${((l + 1) / 8 * 0.85).toFixed(2)})`)); let ang = 0.6, mx = 0, my = 0; window.addEventListener('mousemove', e => { mx = e.clientX / innerWidth - 0.5; my = e.clientY / innerHeight - 0.5; }); function frame(t) { requestAnimationFrame(frame); if (!landingOpen) return; const dpr = Math.min(2, window.devicePixelRatio || 1), w = cv.clientWidth, h = cv.clientHeight; if (cv.width !== Math.round(w * dpr)) { cv.width = Math.round(w * dpr); cv.height = Math.round(h * dpr); } cx.setTransform(dpr, 0, 0, dpr, 0, 0); cx.clearRect(0, 0, w, h); cx.globalCompositeOperation = 'lighter'; ang += 0.0032; const ca = Math.cos(ang), sa = Math.sin(ang), tilt = 0.32 + my * 0.35, ct = Math.cos(tilt), st = Math.sin(tilt); const S = Math.min(w * 0.31, h * 0.52), ox = w * 0.5 + mx * 36, oy = h * 0.5; for (let i = 0; i < n; i++) { const x = P[3 * i], y = P[3 * i + 1], z = P[3 * i + 2]; const x1 = x * ca + z * sa, z1 = -x * sa + z * ca; const y2 = y * ct - z1 * st, z2 = y * st + z1 * ct; const f = 2.8 / (2.8 - z2 * 0.8); const depth = clamp(0.5 + z2 * 0.5); const tw = 0.65 + 0.35 * Math.sin(t * 0.0017 + i * 1.7); const lvl = Math.min(7, Math.floor((0.2 + 0.8 * depth) * tw * 8)); cx.fillStyle = cols[C[i]][lvl]; const r = 0.9 + 1.7 * depth * f; cx.fillRect(ox + x1 * S * f - r / 2, oy + y2 * S * f - r / 2, r, r); } } requestAnimationFrame(frame); })(); resizeWorld(); window.addEventListener('resize', resizeWorld); spawnFly(); addItem('food', W * 0.72, H * 0.3); brainLoop(); let last = performance.now(); function frame(now) { const dt = Math.min(0.05, (now - last) / 1000); last = now; try { if (!paused && !landingOpen) { zapTimer -= dt; tick(dt); } drawWorld(now / 1000); drawBrain(); drawRepro(); drawHud(); } catch (e) { console.error('frame error', e); } requestAnimationFrame(frame); } requestAnimationFrame(frame); </script> </body> </html>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Fly Brain Sim</title>
<style>
:root {
--bg:#060914; --bg2:#0b1020; --card:rgba(17,23,46,.78); --card2:rgba(24,32,62,.7); --line:rgba(130,150,215,.17);
--text:#e8ecff; --dim:#8f99ba; --faint:#5d6788; --violet:#8b6cff; --blue:#3ea0ff; --teal:#2bd9c5; --amber:#ffb44c; --red:#ff6b6b;
--grad:linear-gradient(90deg,#8b6cff,#3ea0ff);
--mono:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
}
* { box-sizing:border-box; }
html, body { height:100%; }
body { margin:0; background:var(--bg); color:var(--text); font:14px/1.45 -apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif;
display:flex; flex-direction:column; overflow:hidden; }
button { font:inherit; color:inherit; cursor:pointer; }
small { color:var(--dim); }
.mono { font-family:var(--mono); letter-spacing:.08em; text-transform:uppercase; font-size:10px; color:var(--faint); }
/* ---- top bar ---- */
#top { height:52px; flex:none; display:flex; align-items:center; gap:14px; padding:0 14px;
background:linear-gradient(180deg,#0c1226,#080c1a); border-bottom:1px solid var(--line); }
.brand { display:flex; align-items:center; gap:10px; cursor:pointer; text-decoration:none; color:inherit; }
.logo { width:30px; height:30px; border-radius:9px; background:linear-gradient(135deg,#8b6cff,#3ea0ff 60%,#2bd9c5);
display:grid; place-items:center; box-shadow:0 0 18px rgba(110,120,255,.45); }
.logo svg { width:18px; height:18px; }
.brand b { font-size:15px; letter-spacing:.2px; }
.brand .mono { display:block; margin-top:-2px; }
.spacer { flex:1; }
.tb { display:flex; gap:8px; align-items:center; }
.pill { background:rgba(255,255,255,.04); border:1px solid var(--line); border-radius:999px; padding:6px 12px; font-size:13px; color:var(--text); transition:.15s; }
.pill:hover { border-color:rgba(139,108,255,.65); background:rgba(139,108,255,.12); }
.pill.primary { background:var(--grad); border-color:transparent; color:#0a0d1c; font-weight:600; }
.pill.on { border-color:var(--teal); background:rgba(43,217,197,.12); }
.menu { position:relative; }
.dropdown { position:absolute; right:0; top:42px; width:340px; z-index:30; background:rgba(10,14,30,.97); border:1px solid var(--line);
border-radius:16px; padding:12px; box-shadow:0 24px 60px rgba(0,0,0,.6); backdrop-filter:blur(10px); }
.dropdown.hidden, .hidden { display:none; }
.menu-head { display:flex; justify-content:space-between; align-items:center; margin-bottom:8px; }
.menu-head b { font-size:14px; }
.switch { display:flex; gap:6px; align-items:center; font-size:12px; color:var(--dim); }
.slot { display:grid; grid-template-columns:1fr auto auto auto; gap:6px; align-items:center; padding:8px; border:1px solid var(--line);
border-radius:12px; margin-bottom:6px; background:var(--card2); }
.slot b { font-size:13px; }
.slot small { display:block; font-size:11px; line-height:1.3; }
.slot button { background:rgba(255,255,255,.05); border:1px solid var(--line); border-radius:8px; padding:4px 9px; font-size:12px; }
.slot button:hover { border-color:var(--violet); }
.slot button[disabled] { opacity:.35; cursor:default; }
.menu-foot { display:flex; gap:6px; flex-wrap:wrap; margin-top:8px; }
.menu-foot .pill { font-size:12px; padding:5px 10px; }
/* ---- layout ---- */
main { flex:1; display:flex; min-height:0; }
#dock { flex:0 0 118px; overflow-y:auto; padding:10px 8px; background:linear-gradient(180deg,#0a0f20,#070a16); border-right:1px solid var(--line); }
.dock-group { margin-bottom:14px; }
.dock-group .mono { display:block; margin:0 4px 6px; }
.tool { width:100%; display:flex; align-items:center; gap:8px; padding:7px 8px; margin-bottom:5px; text-align:left;
background:rgba(255,255,255,.03); border:1px solid var(--line); border-radius:12px; font-size:13px; transition:.15s; }
.tool:hover { border-color:rgba(139,108,255,.6); }
.tool.on { border-color:var(--teal); background:rgba(43,217,197,.1); box-shadow:0 0 0 1px rgba(43,217,197,.25) inset; }
.tool .dot { width:12px; height:12px; border-radius:50%; background:var(--c); box-shadow:0 0 10px var(--c); flex:none; }
.tool kbd { margin-left:auto; font:10px var(--mono); color:var(--faint); }
.swatches { display:flex; gap:5px; padding:2px 6px 8px; flex-wrap:wrap; }
.sw { width:20px; height:20px; border-radius:50%; border:2px solid transparent; background:hsl(var(--h),90%,62%); box-shadow:0 0 8px hsla(var(--h),90%,60%,.7); padding:0; }
.sw.sel { border-color:#fff; transform:scale(1.12); }
#stage { position:relative; flex:1; min-width:0; }
#world { position:absolute; left:0; top:0; width:100%; height:100%; display:block; cursor:crosshair;
background:radial-gradient(ellipse at 50% 40%,#10172e 0%,#090d1c 70%,#060914 100%); }
aside { flex:0 0 372px; overflow-y:auto; padding:10px; background:linear-gradient(180deg,#0a0f20,#070a16); border-left:1px solid var(--line); }
aside.hidden { display:none; }
.card { background:var(--card); border:1px solid var(--line); border-radius:16px; padding:10px 12px; margin-bottom:10px; }
.card h4 { margin:0 0 8px; font:600 10px var(--mono); letter-spacing:.12em; text-transform:uppercase; color:var(--dim); display:flex; justify-content:space-between; }
.card canvas { width:100%; height:auto; border-radius:10px; background:#080c18; display:block; }
.bars { display:grid; gap:7px; }
.bar { display:grid; grid-template-columns:122px 1fr 40px; gap:8px; align-items:center; font-size:12px; color:var(--dim); }
.bar i { display:block; height:7px; background:rgba(255,255,255,.07); border-radius:5px; overflow:hidden; }
.bar b { display:block; height:100%; width:0; border-radius:5px; transition:width .12s; }
.bar span { font:11px var(--mono); text-align:right; color:var(--text); }
#state { font-size:15px; font-weight:600; }
#stats { font-size:11.5px; color:var(--dim); line-height:1.5; }
#err { position:absolute; left:140px; top:64px; z-index:5; margin:0; padding:8px 12px; border:1px solid rgba(139,108,255,.5);
border-radius:12px; background:rgba(12,16,36,.92); font-size:12.5px; display:none; }
#toast { position:fixed; left:50%; bottom:22px; transform:translateX(-50%); z-index:60; padding:8px 14px; border-radius:999px;
background:rgba(14,20,42,.96); border:1px solid var(--line); font-size:13px; opacity:0; pointer-events:none; transition:.25s; }
#toast.show { opacity:1; }
/* ---- landing page ---- */
#landing { position:fixed; inset:0; z-index:50; overflow-y:auto; overflow-x:hidden; scroll-behavior:smooth;
background:radial-gradient(1200px 700px at 78% 8%,rgba(110,90,255,.22),transparent 60%),
radial-gradient(900px 600px at 5% 70%,rgba(43,217,197,.10),transparent 60%),#050813;
transition:opacity .5s, visibility .5s; }
#landing.gone { opacity:0; visibility:hidden; pointer-events:none; }
#heroCanvas { position:absolute; right:0; top:0; width:68%; height:100vh; min-height:620px; pointer-events:none; }
.wrap { position:relative; max-width:1180px; margin:0 auto; padding:0 28px; }
.lnav { position:sticky; top:0; z-index:5; height:64px; display:flex; align-items:center; gap:22px;
background:linear-gradient(180deg,rgba(5,8,19,.92),rgba(5,8,19,.55)); backdrop-filter:blur(8px); border-bottom:1px solid rgba(130,150,215,.08); }
.lnav a.l { color:var(--dim); text-decoration:none; font-size:14px; }
.lnav a.l:hover { color:#fff; }