ergod
Built with Ergod

Ergod's Fly — Living Model

Shared 2026-09-16 · 34 views
About this creation

A creation shared by its maker. The page below runs in a sandbox that cannot read anything of yours or talk to any server. Read the source before you run it anywhere else.

Interactive page
Open full page
Show source
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Ergod's Fly — Living Model (connectome-driven)</title>
<style>
  :root{ --deep:#04121f; --ink:#7fb8d8; --glow:#5ff0d6; --hot:#e8fff8; --threat:#ff5d8f; --eye:#ff3d6e; }
  *{ box-sizing:border-box; }
  body{ margin:0; padding:0; background:radial-gradient(130% 90% at 50% 0%, #0a2a40 0%, #04121f 55%, #020a12 100%);
        color:#cfeeff; font:14px/1.5 ui-monospace, Menlo, Consolas, monospace; min-height:100vh; }
  .wrap{ max-width:1000px; margin:0 auto; padding:20px; }
  h1{ font-size:18px; margin:0 0 2px; color:var(--hot); }
  .sub{ color:var(--ink); font-size:12px; margin-bottom:14px; }
  .stage{ position:relative; border:1px solid #18465e; border-radius:16px; overflow:hidden;
          background:rgba(4,18,31,.55); }
  #scene{ display:block; width:100%; height:460px; }
  .hud{ position:absolute; top:12px; left:14px; font-size:12px; color:var(--ink);
        background:rgba(4,18,31,.55); padding:10px 12px; border-radius:10px; border:1px solid #103848; min-width:190px; }
  .hud .big{ font-size:22px; color:var(--hot); }
  .meter{ height:10px; border-radius:6px; background:#06202f; border:1px solid #18465e; overflow:hidden; margin:5px 0 9px; }
  #bar{ height:100%; width:0%; background:linear-gradient(90deg,#1e9bd1,var(--glow)); transition:width .04s linear; }
  .row{ display:flex; gap:8px; flex-wrap:wrap; margin-top:10px; }
  button{ background:#0c3146; color:var(--glow); border:1px solid #2a6f8c; border-radius:8px;
          padding:7px 12px; font:inherit; cursor:pointer; }
  button:hover{ background:#114056; }
  button.warn{ color:var(--threat); border-color:#7a2c47; }
  input[type=range]{ width:130px; }
  .escape{ position:absolute; top:14px; right:14px; color:var(--threat); font-weight:bold; font-size:15px;
           background:rgba(40,8,20,.6); padding:8px 12px; border-radius:10px; border:1px solid #6a243c;
           opacity:0; transition:opacity .15s; }
  .escape.on{ opacity:1; }
  .controls{ display:flex; gap:10px; align-items:center; flex-wrap:wrap; margin-top:12px; }
  .controls .status{ color:var(--ink); font-size:12px; }
  #trace{ display:block; width:100%; height:120px; margin-top:12px; border:1px solid #18465e; border-radius:12px; background:#04121f; }
  .foot{ color:#5e8aa3; font-size:11px; margin-top:10px; }
  code{ color:var(--glow); }
</style>
</head>
<body>
<div class="wrap">
  <h1>🐙 Ergod's Fly — Living Model</h1>
  <div class="sub">A Drosophila whose motion is driven by the REAL 166,700-neuron connectome run (flybrain v0.1.0).
  Wing-beat, leg tremor and the looming escape jump all read from the recorded CNS firing.</div>

  <div class="stage">
    <canvas id="scene"></canvas>
    <div class="hud">
      <div>CNS firing rate</div>
      <div class="big" id="rate">0</div>
      <div style="font-size:11px;color:var(--ink)">spikes / 20 ms step</div>
      <div class="meter"><div id="bar"></div></div>
      <div id="state" style="color:var(--glow)">idle · wings buzzing</div>
      <div style="margin-top:6px">step <span id="k">0/50</span> · t <span id="t">0.00s</span></div>
      <div>total spikes <span id="tot" style="color:var(--hot)">0</span></div>
    </div>
    <div class="escape" id="esc">⚡ GIANT-FIBER ESCAPE</div>
  </div>

  <div class="controls">
    <button id="play">▶ play</button>
    <button id="restart">↺ restart</button>
    <button id="loom" class="warn">☄ looming stimulus</button>
    <span class="status">speed</span><input id="speed" type="range" min="1" max="24" value="9">
    <span class="status" id="speedv">9× slow-mo</span>
  </div>

  <canvas id="trace"></canvas>
  <div class="foot">Replay of recorded activity (data from <code>bring_fly_to_life.py</code>). The fly's escape at t=0.22 s is the real giant-fiber (DNp01) firing reproduced here as motion.
  For the LIVE Python brain, run that script in this workspace.</div>
</div>

<script>
const ACT = [0,0,0,0,5,17,27,122,225,358,645,1035,1396,2014,2995,4146,6178,
10588,14025,15485,15029,14266,13021,12820,12500,12516,12664,12848,13023,13089,
12888,12741,13187,12968,12859,13765,13822,13561,13699,13776,12211,12252,12385,
12327,12791,13108,12463,12354,12457,12691];
const DT=0.020, LOOM_STEP=10, ESC_STEP=11, PEAK=Math.max(...ACT), BASE=75;

const scene=document.getElementById('scene'), sx=scene.getContext('2d');
const trace=document.getElementById('trace'), tx=trace.getContext('2d');
let W=0,H=0;
function resize(){
  const dpr=devicePixelRatio||1;
  scene.width=scene.clientWidth*dpr; scene.height=scene.clientHeight*dpr; sx.setTransform(dpr,0,0,dpr,0,0);
  trace.width=trace.clientWidth*dpr; trace.height=trace.clientHeight*dpr; tx.setTransform(dpr,0,0,dpr,0,0);
  W=scene.clientWidth; H=scene.clientHeight;
}
window.addEventListener('resize',resize);

// ---- state ----
let k=0, playing=false, total=0, cursor=0, wingPhase=0, legPhase=0, last=0;
let jumping=false, jumpT=0, escapedThisLoop=false, idleWing=0;

function reset(){ k=0; cursor=0; total=0; jumping=false; jumpT=0; escapedThisLoop=false; }

// ---- draw the Drosophila ----
function drawFly(cx,cy,rot,scale,wingAng,legJit,lit){
  sx.save(); sx.translate(cx,cy); sx.rotate(rot); sx.scale(scale,scale);
  // ABDOMEN — 6 tapering segments (A1..A6), behind thorax (+x)
  for(let i=0;i<6;i++){
    const x0=-6+i*8, h=Math.max(2.5, 16-i*2.0);
    sx.beginPath(); sx.ellipse(x0,0,4.5,h/2,0,0,7);
    const ag=sx.createLinearGradient(x0-4,0,x0+4,0);
    ag.addColorStop(0,'#0a1c26'); ag.addColorStop(1,'#123845');
    sx.fillStyle=ag; sx.fill();
    sx.strokeStyle='rgba(95,240,214,'+(0.10+lit*0.32)+')'; sx.lineWidth=0.8; sx.stroke();
  }
  // THORAX — robust center; head at -x, abdomen at +x
  sx.beginPath(); sx.ellipse(-25,0,20,16,0,0,7);
  const th=sx.createRadialGradient(-28,-5,2,-25,0,22);
  th.addColorStop(0,'#1c4a5c'); th.addColorStop(1,'#0a1c26');
  sx.fillStyle=th; sx.fill();
  sx.strokeStyle='rgba(95,240,214,'+(0.15+lit*0.5)+')'; sx.lineWidth=1; sx.stroke();
  sx.strokeStyle='rgba(95,240,214,'+(0.10+lit*0.3)+')'; sx.lineWidth=0.8;
  sx.beginPath(); sx.moveTo(-40,-6); sx.quadraticCurveTo(-25,8,-10,-6); sx.stroke();
  // HEAD — forward at -x (neck to thorax)
  sx.beginPath(); sx.ellipse(-50,-1,11,12,0,0,7); sx.fillStyle='#0c232e'; sx.fill();
  sx.strokeStyle='rgba(95,240,214,'+(0.12+lit*0.4)+')'; sx.lineWidth=0.8; sx.stroke();
  // compound eyes (glowing hemispheres)
  for(const s of [-1,1]){
    const g=sx.createRadialGradient(-50,s*7,1,-50,s*7,8);
    g.addColorStop(0,'#ff7da3'); g.addColorStop(0.6,'#ff3d6e'); g.addColorStop(1,'#7a0f33');
    sx.beginPath(); sx.ellipse(-51,s*7,6,7,0,0,7); sx.fillStyle=g; sx.fill();
  }
  // antennae with feathery arista
  for(const s of [-1,1]){
    sx.strokeStyle='#1c4a5c'; sx.lineWidth=1.4;
    sx.beginPath(); sx.moveTo(-58,s*2); sx.lineTo(-64,s*7); sx.stroke();
    sx.strokeStyle='rgba(160,245,255,0.5)'; sx.lineWidth=0.6;
    for(let b=-3;b<=3;b++){ sx.beginPath(); sx.moveTo(-64,s*7); sx.lineTo(-64+b, s*7+b*1.4); sx.stroke(); }
  }
  // WINGS — one pair, membrane + veins, rooted at MESOTHORAX (mid-thorax)
  for(const s of[-1,1]){
    sx.save(); sx.translate(-25,s*5); sx.rotate(s*wingAng);
    sx.beginPath(); sx.ellipse(42,0,44,15,0,0,7);
    sx.fillStyle='rgba(120,230,255,'+(0.10+lit*0.18)+')'; sx.fill();
    sx.strokeStyle='rgba(160,245,255,'+(0.25+lit*0.4)+')'; sx.lineWidth=1; sx.stroke();
    sx.strokeStyle='rgba(160,245,255,'+(0.12+lit*0.2)+')'; sx.lineWidth=0.6;
    for(let v=0;v<4;v++){ sx.beginPath(); sx.moveTo(2,0); sx.lineTo(84, s*(2+v*4)); sx.stroke(); }
    sx.restore();
  }
  // HALTERES — one pair of drumstick balancers at METATHORAX (behind wing roots)
  for(const s of[-1,1]){
    sx.save(); sx.translate(-18,s*6);
    sx.strokeStyle='#2a6076'; sx.lineWidth=1.6;
    sx.beginPath(); sx.moveTo(0,0); sx.lineTo(0,s*5); sx.stroke();
    sx.beginPath(); sx.ellipse(0,s*6,2.6,4.5,s*0.4,0,7); sx.fillStyle='#5ff0d6'; sx.fill();
    sx.restore();
  }
  // LEGS — 3 pairs, each rooted at a distinct coxa (fore/mid/hind along thorax)
  sx.strokeStyle='#2a6076'; sx.lineWidth=1.4;
  const coxae=[[-40,1],[-28,1],[-16,1],[-40,-1],[-28,-1],[-16,-1]]; // fore/mid/hind x, side
  coxae.forEach(([lx,side],i)=>{
    const j=(Math.sin(legPhase*2+i)*legJit);
    const cy=side*6;
    const cx2=lx+(side*4), cy2=cy+side*9+j;
    const fx=lx+(side*9), fy=cy+side*20+j*1.3;
    const tx=lx+(side*6), ty=cy+side*30+j*1.7;
    sx.beginPath(); sx.moveTo(lx,cy); sx.lineTo(cx2,cy2); sx.lineTo(fx,fy); sx.lineTo(tx,ty); sx.stroke();
  });
  sx.restore();
}

function frameRate(rate){ return 5 + rate/220; } // wing Hz-ish (visual)

function drawScene(rate,lit){
  sx.clearRect(0,0,W,H);
  // connectome glow behind
  const g=sx.createRadialGradient(W/2,H*0.62,10,W/2,H*0.62,260);
  g.addColorStop(0,'rgba(95,240,214,'+(0.05+lit*0.22)+')');
  g.addColorStop(1,'rgba(4,18,31,0)');
  sx.fillStyle=g; sx.fillRect(0,0,W,H);

  // floor reflection line
  sx.strokeStyle='rgba(40,90,110,0.4)'; sx.lineWidth=1;
  sx.beginPath(); sx.moveTo(0,H*0.86); sx.lineTo(W,H*0.86); sx.stroke();

  // fly position + jump
  const baseY=H*0.6, baseX=W*0.5;
  let px=baseX, py=baseY, rot=0, scale=1.05;
  if(jumping){
    const p=Math.min(1,jumpT/0.7);
    const arc=Math.sin(p*Math.PI);
    py=baseY - arc*150;                 // launch arc
    px=baseX + Math.sin(p*Math.PI)*40;  // little drift
    rot=Math.sin(p*Math.PI*2)*0.5;      // tumble
    scale=1.05 + arc*0.25;
  }
  const wingAng=Math.sin(wingPhase)*(0.35+lit*0.55) + (jumping?0.6:0);
  const legJit=Math.min(3, rate/2500);
  drawFly(px,py,rot,scale,wingAng,legJit,lit);

  // motion trail during escape
  if(jumping){
    for(let i=1;i<=5;i++){
      sx.globalAlpha=0.10*(1-i/6);
      drawFly(px, py+i*14, rot, scale*(1-i*0.02), wingAng, legJit, lit*0.5);
    }
    sx.globalAlpha=1;
  }
}

function drawTrace(){
  const w=trace.clientWidth, h=trace.clientHeight;
  tx.clearRect(0,0,w,h);
  const lx=(LOOM_STEP/ACT.length)*w;
  tx.fillStyle='rgba(255,93,143,0.07)'; tx.fillRect(lx,0,w-lx,h);
  tx.strokeStyle='#103848'; for(let gy=0;gy<=4;gy++){ const y=h*gy/4; tx.beginPath(); tx.moveTo(0,y); tx.lineTo(w,y); tx.stroke(); }
  tx.beginPath(); tx.strokeStyle='#5ff0d6'; tx.lineWidth=1.6;
  for(let i=0;i<=Math.floor(cursor);i++){ const x=(i/ACT.length)*w, y=h-(ACT[i]/PEAK)*h; i?tx.lineTo(x,y):tx.moveTo(x,y); }
  tx.stroke();
  const hx=(cursor/ACT.length)*w; tx.strokeStyle='#e8fff8'; tx.beginPath(); tx.moveTo(hx,0); tx.lineTo(hx,h); tx.stroke();
}

// ---- loop ----
function tick(now){
  if(!last) last=now; const dt=Math.min(0.05,(now-last)/1000); last=now;
  const rate = ACT[Math.floor(cursor)]||0;
  const lit = Math.min(1, rate/PEAK);
  // wing + leg phases advance with activity
  wingPhase += frameRate(rate)*6.28*dt*(playing?1:0.35);
  legPhase  += dt*(playing?6:2);
  if(playing){
    cursor += (parseInt(speed.value)/10)*dt;   // steps per real second scaled by speed
    if(cursor>=ACT.length){ cursor=0; total=0; escapedThisLoop=false; }
    k=Math.floor(cursor);
    total += (cursor - (k>0?k-1:0))>0 ? rate : 0;
    if(k===ESC_STEP && !escapedThisLoop){ jumping=true; jumpT=0; escapedThisLoop=true; document.getElementById('esc').classList.add('on'); }
  }
  if(jumping){ jumpT+=dt; if(jumpT>0.7){ jumping=false; document.getElementById('esc').classList.remove('on'); } }

  drawScene(rate,lit); drawTrace();
  // HUD
  document.getElementById('rate').textContent=rate;
  document.getElementById('bar').style.width=(rate/PEAK*100)+'%';
  document.getElementById('k').textContent=k+'/'+ACT.length;
  document.getElementById('t').textContent=(k*DT).toFixed(2)+'s';
  document.getElementById('tot').textContent=total.toLocaleString();
  let st='idle · wings buzzing';
  if(k>=LOOM_STEP) st='⚠ looming detected · tensing';
  if(jumping) st='⚡ ESCAPE JUMP!';
  else if(rate>3000) st='CNS firing · legs jittering';
  document.getElementById('state').textContent=st;

  requestAnimationFrame(tick);
}

const speed=document.getElementById('speed');
document.getElementById('play').onclick=function(){
  playing=!playing; this.textContent=playing?'⏸ pause':'▶ play';
};
document.getElementById('restart').onclick=function(){ reset(); cursor=0; playing=true; document.getElementById('play').textContent='⏸ pause'; };
document.getElementById('loom').onclick=function(){ if(cursor<LOOM_STEP){ total=ACT.slice(0,LOOM_STEP).reduce((a,b)=>a+b,0); cursor=LOOM_STEP; } };
speed.oninput=function(){ document.getElementById('speedv').textContent=this.value+'× slow-mo'; };

resize(); reset(); cursor=0; playing=true;
document.getElementById('play').textContent='⏸ pause';
requestAnimationFrame(tick);
</script>
</body>
</html>