← back to Model Arena

data/artifacts/1c80df76af60/claude-code.html

830 lines

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Midnight Woodland Jam — An Artistic Rhythm Simulation</title>
<style>
  :root{
    --bg:#080a17;
    --surface:rgba(14,17,34,.72);
    --surface-solid:#0e1122;
    --text:#f3eee4;
    --muted:#9aa3c4;
    --line:rgba(180,196,255,.16);
    --accent:#8fb8ff;
    --accent-warm:#ffb27a;
    --focus:#ffe9a8;
    --radius:14px;
    --shadow:0 8px 24px rgba(0,0,0,.45);
    --hover:0 16px 48px rgba(0,0,0,.55);
    --glow:0 0 24px rgba(143,184,255,.35);
    --serif:'Didot','Bodoni MT','Playfair Display',Georgia,serif;
    --sans:-apple-system,'Segoe UI','Helvetica Neue',Arial,sans-serif;
  }
  *{box-sizing:border-box}
  html,body{height:100%}
  body{
    margin:0;background:var(--bg);color:var(--text);font-family:var(--sans);
    overflow:hidden;-webkit-font-smoothing:antialiased;
  }
  #stage{position:fixed;inset:0;display:block}

  /* ---- Header ---- */
  .masthead{
    position:fixed;top:0;left:0;right:0;z-index:5;
    padding:24px 32px;pointer-events:none;
    display:flex;justify-content:space-between;align-items:flex-start;gap:24px;
    animation:fadeUp .9s cubic-bezier(.22,1,.36,1) both;
  }
  .masthead h1{
    font-family:var(--serif);font-weight:500;letter-spacing:.01em;
    font-size:clamp(28px,4.4vw,50px);line-height:1;margin:0;
    text-shadow:0 2px 30px rgba(0,0,0,.7);
  }
  .masthead .kicker{
    font-size:12px;letter-spacing:.18em;text-transform:uppercase;color:var(--muted);
    margin:0 0 10px;
  }
  .live-tag{
    pointer-events:none;font-size:12px;letter-spacing:.15em;text-transform:uppercase;
    color:var(--muted);text-align:right;line-height:1.7;
    text-shadow:0 1px 12px rgba(0,0,0,.8);
  }
  .live-tag strong{color:var(--text);font-weight:600}

  /* ---- Control dock ---- */
  .dock{
    position:fixed;left:0;right:0;bottom:0;z-index:6;
    padding:16px 20px calc(16px + env(safe-area-inset-bottom));
    display:flex;flex-wrap:wrap;gap:16px;align-items:stretch;justify-content:center;
    background:linear-gradient(to top,rgba(6,8,20,.92),rgba(6,8,20,.4) 70%,transparent);
    animation:fadeUp 1s .15s cubic-bezier(.22,1,.36,1) both;
  }
  fieldset{
    border:1px solid var(--line);border-radius:var(--radius);
    background:var(--surface);backdrop-filter:blur(14px);-webkit-backdrop-filter:blur(14px);
    margin:0;padding:12px 14px 14px;box-shadow:var(--shadow);
    display:flex;flex-direction:column;gap:10px;min-width:0;
  }
  legend{
    font-size:12px;letter-spacing:.15em;text-transform:uppercase;color:var(--muted);
    padding:0 6px;
  }
  .row{display:flex;gap:8px;flex-wrap:wrap}

  button.ctl,.seg button{
    font-family:var(--sans);font-size:13px;color:var(--text);
    background:rgba(255,255,255,.04);border:1px solid var(--line);
    border-radius:10px;padding:9px 13px;cursor:pointer;
    transition:background .18s ease,border-color .18s ease,transform .12s ease,box-shadow .18s ease;
    letter-spacing:.02em;white-space:nowrap;
  }
  button.ctl:hover,.seg button:hover{background:rgba(255,255,255,.09);transform:translateY(-1px)}
  .seg button[aria-pressed="true"],button.ctl[aria-pressed="true"]{
    background:linear-gradient(180deg,rgba(143,184,255,.28),rgba(143,184,255,.12));
    border-color:rgba(143,184,255,.55);box-shadow:var(--glow);color:#fff;
  }
  .seg{display:flex;gap:6px;flex-wrap:wrap}
  .movement-seg button{min-width:112px;text-align:center;flex-direction:column;display:flex;gap:2px}
  .movement-seg button small{display:block;font-size:10px;letter-spacing:.12em;text-transform:uppercase;color:var(--muted)}
  .movement-seg button[aria-pressed="true"] small{color:rgba(255,255,255,.75)}

  label.slider{display:flex;flex-direction:column;gap:7px;font-size:12px;color:var(--muted);letter-spacing:.06em}
  label.slider .val{color:var(--text);font-variant-numeric:tabular-nums}
  input[type="range"]{
    -webkit-appearance:none;appearance:none;width:170px;height:4px;border-radius:4px;
    background:rgba(255,255,255,.18);outline:none;cursor:pointer;margin:4px 0;
  }
  input[type="range"]::-webkit-slider-thumb{
    -webkit-appearance:none;width:18px;height:18px;border-radius:50%;
    background:var(--accent);border:2px solid #fff;box-shadow:var(--glow);
  }
  input[type="range"]::-moz-range-thumb{
    width:16px;height:16px;border-radius:50%;background:var(--accent);border:2px solid #fff;
  }

  /* Focus visibility — strong states */
  :focus-visible{outline:3px solid var(--focus);outline-offset:2px;border-radius:6px}
  button:focus-visible,input:focus-visible,summary:focus-visible{
    outline:3px solid var(--focus);outline-offset:2px;box-shadow:0 0 0 5px rgba(255,233,168,.18);
  }

  /* ---- Curator note ---- */
  .note{
    position:fixed;left:20px;bottom:calc(100% );z-index:6;
  }
  details.note-panel{
    position:fixed;right:20px;top:88px;z-index:6;width:min(340px,calc(100vw - 40px));
    background:var(--surface);border:1px solid var(--line);border-radius:var(--radius);
    backdrop-filter:blur(14px);-webkit-backdrop-filter:blur(14px);box-shadow:var(--shadow);
    animation:fadeUp 1.1s .3s cubic-bezier(.22,1,.36,1) both;
  }
  details.note-panel summary{
    list-style:none;cursor:pointer;padding:12px 16px;
    font-size:12px;letter-spacing:.15em;text-transform:uppercase;color:var(--muted);
    display:flex;justify-content:space-between;align-items:center;
  }
  details.note-panel summary::-webkit-details-marker{display:none}
  details.note-panel summary .chev{transition:transform .25s ease}
  details.note-panel[open] summary .chev{transform:rotate(180deg)}
  .note-body{padding:0 16px 16px;font-size:14px;line-height:1.6;color:var(--text)}
  .note-body p{margin:0 0 12px}
  .note-body .honest{
    font-size:12.5px;color:var(--muted);border-top:1px solid var(--line);padding-top:12px;line-height:1.55;
  }
  .note-body .honest strong{color:var(--accent)}

  @keyframes fadeUp{from{opacity:0;transform:translateY(28px)}to{opacity:1;transform:none}}

  .sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0 0 0 0);border:0}

  @media (max-width:720px){
    .masthead{padding:16px 18px}
    details.note-panel{top:auto;bottom:200px;right:12px;left:12px;width:auto}
    .dock{gap:12px;padding:12px 12px calc(12px + env(safe-area-inset-bottom))}
    input[type="range"]{width:130px}
  }
  @media (prefers-reduced-motion:reduce){
    .masthead,.dock,details.note-panel{animation-duration:.01ms}
  }
</style>
</head>
<body>
  <canvas id="stage" role="img"
    aria-label="An animated moonlit forest clearing where stylized human silhouettes, deer, bears, wolves, frogs and fireflies move to a simulated instrumental pulse around an abstract cluster of speaker-like geometric forms."></canvas>

  <header class="masthead">
    <div>
      <p class="kicker">A living tableau</p>
      <h1>Midnight Woodland Jam</h1>
    </div>
    <p class="live-tag" aria-hidden="true">
      Movement · <strong id="tagMovement">Gathering</strong><br>
      Pulse · <strong id="tagState">Playing</strong>
    </p>
  </header>

  <details class="note-panel">
    <summary>Curator's Note <span class="chev">▾</span></summary>
    <div class="note-body">
      <p>Under a low moon, a clearing keeps a rhythm no one can hear. Figures spin and bend; deer turn their heads; bears sway; wolves loop the tree line; frogs answer the mushroom-light; fireflies braid themselves into brief spirals. Three movements — <em>Gathering</em>, <em>Deep&nbsp;Jam</em>, <em>Dawn&nbsp;Return</em> — shift the palette, the crowd, the camera, and the swarm.</p>
      <p class="honest"><strong>Honest note:</strong> These visuals are an <strong>artistic rhythm simulation</strong>. The "pulse" is a beat generated entirely in code to choreograph motion. <strong>No audio is played, recorded, streamed, or analyzed</strong>, and the central shapes are abstract — not a real stage, brand, or logo.</p>
    </div>
  </details>

  <div class="dock" role="region" aria-label="Tableau controls">
    <fieldset>
      <legend>Movement</legend>
      <div class="seg movement-seg" role="group" aria-label="Choose a visual movement">
        <button class="ctl" data-move="0" aria-pressed="true">Gathering<small>dusk · sparse</small></button>
        <button class="ctl" data-move="1" aria-pressed="false">Deep Jam<small>peak · dense</small></button>
        <button class="ctl" data-move="2" aria-pressed="false">Dawn Return<small>pale · rising</small></button>
      </div>
    </fieldset>

    <fieldset>
      <legend>Choreography</legend>
      <div class="seg" role="group" aria-label="Isolate choreography">
        <button class="ctl" id="isoBoth" aria-pressed="true">Everyone</button>
        <button class="ctl" id="isoHumans" aria-pressed="false">Humans only</button>
        <button class="ctl" id="isoAnimals" aria-pressed="false">Animals only</button>
      </div>
    </fieldset>

    <fieldset>
      <legend>Playback</legend>
      <div class="row">
        <button class="ctl" id="pauseBtn" aria-pressed="false">Pause everything</button>
        <button class="ctl" id="reduceBtn" aria-pressed="false">Reduced motion</button>
      </div>
    </fieldset>

    <fieldset>
      <legend>Visual tempo</legend>
      <label class="slider" for="tempo">
        <span>Simulated pulse speed · <span class="val" id="tempoVal">1.0×</span></span>
        <input type="range" id="tempo" min="0.3" max="2" step="0.05" value="1" aria-describedby="tempoHelp">
        <span id="tempoHelp" class="sr-only">Adjusts how fast the imagined instrumental pulse drives all movement.</span>
      </label>
    </fieldset>
  </div>

  <div class="sr-only" aria-live="polite" id="announce"></div>

<script>
(function(){
  "use strict";
  const canvas = document.getElementById('stage');
  const ctx = canvas.getContext('2d');
  let W=0,H=0,dpr=1;

  /* ---------- utilities ---------- */
  const clamp=(v,a,b)=>v<a?a:v>b?b:v;
  const lerp=(a,b,t)=>a+(b-a)*t;
  function mulberry32(a){return function(){a|=0;a=a+0x6D2B79F5|0;let t=Math.imul(a^a>>>15,1|a);t=t+Math.imul(t^t>>>7,61|t)^t;return((t^t>>>14)>>>0)/4294967296;}}
  function hash(i){let x=Math.sin(i*12.9898)*43758.5453;return x-Math.floor(x);}
  function noise(x){const i=Math.floor(x),f=x-i;const u=f*f*(3-2*f);return lerp(hash(i),hash(i+1),u);}
  function hex(h){h=h.replace('#','');return [parseInt(h.slice(0,2),16),parseInt(h.slice(2,4),16),parseInt(h.slice(4,6),16)];}
  const rgba=(c,a)=>`rgba(${c[0]|0},${c[1]|0},${c[2]|0},${a})`;
  const lerpC=(a,b,t)=>[lerp(a[0],b[0],t),lerp(a[1],b[1],t),lerp(a[2],b[2],t)];

  /* ---------- movement themes ---------- */
  const THEMES=[
    { // Gathering — cool indigo dusk
      skyTop:hex('#070a1a'), skyBot:hex('#182247'), moon:hex('#d3e3ff'), glow:hex('#5f78c8'),
      accentA:hex('#7fa8e6'), accentB:hex('#a8e0d6'), sil:hex('#0a0d1e'), tree:hex('#0b1024'),
      ground:hex('#141d3f'), fire:hex('#bcd2ff'),
      zoom:0.9, horizon:0.60, density:0.55, spiral:0.0, mush:0.9
    },
    { // Deep Jam — warm magenta/amber peak
      skyTop:hex('#140a24'), skyBot:hex('#3a1544'), moon:hex('#ffd9c0'), glow:hex('#c85aa0'),
      accentA:hex('#ff6f9d'), accentB:hex('#ffb257'), sil:hex('#120817'), tree:hex('#180a26'),
      ground:hex('#3a1a44'), fire:hex('#ffd18a'),
      zoom:1.16, horizon:0.55, density:1.0, spiral:1.0, mush:1.0
    },
    { // Dawn Return — pale rose gold rising
      skyTop:hex('#2a2440'), skyBot:hex('#d8a79c'), moon:hex('#fff0d2'), glow:hex('#f0b79f'),
      accentA:hex('#f2b6a6'), accentB:hex('#ffe6b0'), sil:hex('#241a2e'), tree:hex('#3a2c45'),
      ground:hex('#c79a94'), fire:hex('#fff0cf'),
      zoom:1.0, horizon:0.5, density:0.7, spiral:0.35, mush:0.6
    }
  ];
  // live (interpolated) theme
  let theme=JSON.parse(JSON.stringify(THEMES[0]));
  let target=THEMES[0];
  let curMove=0;

  function setMovement(i){
    curMove=i; target=THEMES[i];
    document.querySelectorAll('.movement-seg button').forEach(b=>b.setAttribute('aria-pressed', b.dataset.move==i?'true':'false'));
    const names=['Gathering','Deep Jam','Dawn Return'];
    document.getElementById('tagMovement').textContent=names[i];
    announce('Movement changed to '+names[i]);
  }
  function tickTheme(dt){
    const s=clamp(dt*1.4,0,1);
    for(const k of ['skyTop','skyBot','moon','glow','accentA','accentB','sil','tree','ground','fire']){
      theme[k]=lerpC(theme[k],target[k],s);
    }
    for(const k of ['zoom','horizon','density','spiral','mush']){
      theme[k]=lerp(theme[k],target[k],s);
    }
  }

  /* ---------- creatures ---------- */
  const rnd=mulberry32(1337);
  const creatures=[];
  const HUMAN_BEHAV=['spin','bend','clap','drift'];

  // cluster anchors around the speaker (fractional, within clearing ellipse)
  const groups=[
    {x:0.30,y:0.74},{x:0.68,y:0.75},{x:0.42,y:0.83},{x:0.60,y:0.68}
  ];

  function inClearing(fx,fy){
    const dx=(fx-0.5)/0.30, dy=(fy-0.75)/0.16;
    return dx*dx+dy*dy<=1;
  }
  function scatter(cx,cy,r){
    let fx,fy,g=0;
    do{ const a=rnd()*Math.PI*2, rr=Math.sqrt(rnd())*r;
      fx=cx+Math.cos(a)*rr*0.9; fy=cy+Math.sin(a)*rr*0.34;
    }while(!inClearing(fx,fy) && g++<12);
    return {fx,fy};
  }

  // Humans (kind:'human')
  for(let i=0;i<15;i++){
    const gi=i%groups.length;
    const p=scatter(groups[gi].x,groups[gi].y,0.16);
    creatures.push({
      kind:'human', side:'human',
      fx:p.fx, fy:p.fy, home:{...p},
      behav:HUMAN_BEHAV[i%4],
      off:rnd()*10, seed:rnd()*100,
      scale:lerp(0.85,1.15,rnd()),
      driftG:gi, driftTarget:(gi+1)%groups.length
    });
  }
  // Deer
  for(let i=0;i<5;i++){const p=scatter(lerp(0.2,0.8,rnd()),lerp(0.66,0.84,rnd()),0.2);
    creatures.push({kind:'deer',side:'animal',fx:p.fx,fy:p.fy,off:rnd()*10,seed:rnd()*100,scale:lerp(.9,1.1,rnd()),face:rnd()<.5?1:-1});}
  // Bears
  for(let i=0;i<3;i++){const p=scatter(lerp(0.15,0.85,rnd()),lerp(0.7,0.86,rnd()),0.25);
    creatures.push({kind:'bear',side:'animal',fx:p.fx,fy:p.fy,off:rnd()*10,seed:rnd()*100,scale:lerp(1,1.25,rnd()),face:rnd()<.5?1:-1});}
  // Wolves (loop runs) — anchor + loop radius
  for(let i=0;i<4;i++){const cx=lerp(0.18,0.82,rnd()),cy=lerp(0.72,0.85,rnd());
    creatures.push({kind:'wolf',side:'animal',fx:cx,fy:cy,cx,cy,off:rnd()*10,seed:rnd()*100,scale:lerp(.85,1.05,rnd()),loopR:lerp(0.05,0.09,rnd()),dir:rnd()<.5?1:-1});}
  // Mushrooms + frogs beside them
  const mushrooms=[];
  for(let i=0;i<7;i++){const p=scatter(lerp(0.16,0.84,rnd()),lerp(0.72,0.9,rnd()),0.3);
    mushrooms.push({fx:p.fx,fy:p.fy,scale:lerp(.7,1.2,rnd()),seed:rnd()*100});
    if(i<6){creatures.push({kind:'frog',side:'animal',fx:p.fx+0.02,fy:p.fy+0.005,off:rnd()*10,seed:rnd()*100,scale:lerp(.8,1.1,rnd()),face:rnd()<.5?1:-1});}
  }

  // Fireflies
  const fireflies=[];
  for(let i=0;i<95;i++){
    fireflies.push({fx:rnd(),fy:lerp(0.25,0.92,rnd()),seed:rnd()*1000,
      spGroup:i%5, phase:rnd()*Math.PI*2, sr:lerp(0.04,0.13,rnd()), vx:0,vy:0,
      x:0,y:0,init:false});
  }
  const spiralCenters=[{fx:0.5,fy:0.5},{fx:0.32,fy:0.44},{fx:0.68,fy:0.46},{fx:0.42,fy:0.58},{fx:0.6,fy:0.6}];

  // stars
  const stars=[];
  for(let i=0;i<130;i++){stars.push({fx:rnd(),fy:rnd()*0.55,r:rnd()*1.3+0.2,tw:rnd()*6});}

  // trees (silhouette line)
  const treesFar=[],treesMid=[];
  for(let i=0;i<26;i++){treesFar.push({fx:i/25+ (rnd()-.5)*0.02, h:lerp(0.1,0.22,rnd()), w:lerp(0.02,0.05,rnd()),seed:rnd()*10});}
  for(let i=0;i<18;i++){treesMid.push({fx:i/17+(rnd()-.5)*0.03, h:lerp(0.18,0.34,rnd()), w:lerp(0.03,0.07,rnd()),seed:rnd()*10});}

  /* ---------- state ---------- */
  const state={ paused:false, reduce:false, tempo:1, show:{human:true,animal:true}, t:0, last:0, energy:0 };
  // pulse: returns per-offset envelope 0..1
  function beatEnv(phase,off){
    const p=(phase+off);
    let b=p-Math.floor(p);
    // two-hit shuffle for a more musical, less metronomic feel
    const e1=Math.pow(1-b,2.2);
    const e2=b>0.5?Math.pow(1-(b-0.5)*2,2.8)*0.55:0;
    return clamp(e1+e2,0,1);
  }

  /* ---------- resize ---------- */
  function resize(){
    dpr=Math.min(window.devicePixelRatio||1,2);
    W=window.innerWidth;H=window.innerHeight;
    canvas.width=W*dpr;canvas.height=H*dpr;canvas.style.width=W+'px';canvas.style.height=H+'px';
    ctx.setTransform(dpr,0,0,dpr,0,0);
  }
  window.addEventListener('resize',resize);resize();

  /* ---------- drawing helpers ---------- */
  function drawSky(){
    const hz=theme.horizon;
    const g=ctx.createLinearGradient(0,0,0,H);
    g.addColorStop(0,rgba(theme.skyTop,1));
    g.addColorStop(clamp(hz,0.05,0.95),rgba(theme.skyBot,1));
    g.addColorStop(1,rgba(theme.ground,1));
    ctx.fillStyle=g;ctx.fillRect(0,0,W,H);
  }
  function drawStars(t){
    for(const s of stars){
      const tw=0.4+0.6*Math.abs(Math.sin(t*0.7+s.tw));
      const a=tw*(0.6- theme.horizon*0.3)* (curMove===2?0.4:1);
      if(a<=0.02)continue;
      ctx.fillStyle=rgba(theme.moon,a*0.8);
      ctx.beginPath();ctx.arc(s.fx*W,s.fy*H,s.r,0,7);ctx.fill();
    }
  }
  function drawMoon(t){
    const mx=W*0.78,my=H*0.2;
    const pulse=0.9+state.energy*0.25;
    const R=Math.min(W,H)*0.07;
    // glow
    const gg=ctx.createRadialGradient(mx,my,R*0.3,mx,my,R*6*pulse);
    gg.addColorStop(0,rgba(theme.glow,0.5));
    gg.addColorStop(0.4,rgba(theme.glow,0.16));
    gg.addColorStop(1,rgba(theme.glow,0));
    ctx.fillStyle=gg;ctx.fillRect(0,0,W,H);
    // disc
    const dg=ctx.createRadialGradient(mx-R*0.3,my-R*0.3,R*0.2,mx,my,R);
    dg.addColorStop(0,rgba(theme.moon,1));
    dg.addColorStop(1,rgba(theme.moon,0.82));
    ctx.fillStyle=dg;ctx.beginPath();ctx.arc(mx,my,R,0,7);ctx.fill();
  }
  function drawTrees(t){
    const hz=theme.horizon*H;
    const zf=1+(theme.zoom-1)*0.25;
    // far
    ctx.fillStyle=rgba(theme.tree,0.65);
    for(const tr of treesFar){drawTree(tr.fx*W,hz+6,tr.h*H*zf,tr.w*W*zf,theme.tree,0.6);}
    ctx.fillStyle=rgba(theme.tree,0.9);
    for(const tr of treesMid){drawTree(tr.fx*W,hz+18,tr.h*H*zf*1.1,tr.w*W*zf*1.15,theme.sil,0.92);}
  }
  function drawTree(x,base,h,w,col,a){
    ctx.fillStyle=rgba(col,a);
    // trunk
    ctx.fillRect(x-w*0.12,base-h*0.5,w*0.24,h*0.5);
    // conical canopy
    ctx.beginPath();
    ctx.moveTo(x,base-h);
    ctx.lineTo(x+w*0.6,base-h*0.35);
    ctx.lineTo(x+w*0.3,base-h*0.4);
    ctx.lineTo(x+w*0.75,base-h*0.05);
    ctx.lineTo(x-w*0.75,base-h*0.05);
    ctx.lineTo(x-w*0.3,base-h*0.4);
    ctx.lineTo(x-w*0.6,base-h*0.35);
    ctx.closePath();ctx.fill();
  }
  function drawGround(){
    const cy=0.75*H;
    const g=ctx.createRadialGradient(0.5*W,cy,10,0.5*W,cy,0.5*W);
    g.addColorStop(0,rgba(theme.glow,0.22+state.energy*0.12));
    g.addColorStop(0.5,rgba(theme.ground,0.5));
    g.addColorStop(1,rgba(theme.ground,0));
    ctx.save();
    ctx.translate(0.5*W,cy);ctx.scale(1,0.34);
    ctx.fillStyle=g;ctx.beginPath();ctx.arc(0,0,0.5*W,0,7);ctx.fill();
    ctx.restore();
  }

  function stageTransform(){
    const cx=0.5*W, cy=0.68*H, z=theme.zoom;
    ctx.translate(cx,cy);ctx.scale(z,z);ctx.translate(-cx,-cy);
  }

  // Abstract speaker cluster + hand-painted light (NOT a real stage/logo)
  function drawSpeakers(t){
    const cx=0.5*W, cy=0.63*H;
    const s=Math.min(W,H)*0.09;
    const e=state.energy;
    ctx.save();ctx.translate(cx,cy);
    // painted light aura
    const ag=ctx.createRadialGradient(0,0,s*0.3,0,0,s*4);
    ag.addColorStop(0,rgba(theme.accentA,0.28+e*0.25));
    ag.addColorStop(0.5,rgba(theme.accentB,0.10+e*0.1));
    ag.addColorStop(1,rgba(theme.accentA,0));
    ctx.fillStyle=ag;ctx.fillRect(-s*4,-s*4,s*8,s*8);

    // hand-painted light rays (brush strokes)
    const rays=7;
    for(let i=0;i<rays;i++){
      const a=(i/rays)*Math.PI*2 + t*0.15 + noise(i*3+t*0.2)*0.6;
      const len=s*(2.2+ e*2.2 + noise(i*7+t)*0.8);
      ctx.save();ctx.rotate(a);
      const lg=ctx.createLinearGradient(0,0,0,-len);
      lg.addColorStop(0,rgba(i%2?theme.accentB:theme.accentA,0.35+e*0.3));
      lg.addColorStop(1,rgba(theme.accentA,0));
      ctx.fillStyle=lg;
      ctx.beginPath();ctx.moveTo(-s*0.16,0);
      ctx.quadraticCurveTo(-s*0.05,-len*0.6,0,-len);
      ctx.quadraticCurveTo(s*0.05,-len*0.6,s*0.16,0);
      ctx.closePath();ctx.fill();
      ctx.restore();
    }

    // geometric speaker forms — stacked prisms/cones (abstract)
    const bob=e*s*0.12;
    const sil=rgba(theme.sil,0.95);
    // left & right monoliths
    for(const dxs of [-1,1]){
      ctx.save();ctx.translate(dxs*s*1.5,bob*(dxs>0?1:0.6));
      ctx.fillStyle=sil;
      ctx.strokeStyle=rgba(theme.accentA,0.5);ctx.lineWidth=1.4;
      roundRect(-s*0.5,-s*1.6,s,s*2.2,6);ctx.fill();ctx.stroke();
      // cone driver circles pulsing
      for(let k=0;k<3;k++){
        const yy=-s*1.15+k*s*0.75;
        ctx.beginPath();ctx.arc(0,yy,s*0.30*(1+ (k===1?e*0.15:0)),0,7);
        ctx.fillStyle=rgba(theme.accentB,0.18+ (k===1?e*0.35:0.1));ctx.fill();
        ctx.strokeStyle=rgba(theme.accentA,0.6);ctx.lineWidth=1.2;ctx.stroke();
      }
      ctx.restore();
    }
    // center pillar — triangular prism
    ctx.save();ctx.translate(0,-bob);
    ctx.fillStyle=sil;ctx.strokeStyle=rgba(theme.accentB,0.55);ctx.lineWidth=1.6;
    ctx.beginPath();
    ctx.moveTo(0,-s*2.0);ctx.lineTo(s*0.85,-s*0.2);ctx.lineTo(-s*0.85,-s*0.2);ctx.closePath();
    ctx.fill();ctx.stroke();
    // pulsing core
    ctx.beginPath();ctx.arc(0,-s*0.9,s*0.34*(0.8+e*0.5),0,7);
    ctx.fillStyle=rgba(theme.accentA,0.3+e*0.5);ctx.fill();
    ctx.restore();

    ctx.restore();
  }
  function roundRect(x,y,w,h,r){ctx.beginPath();ctx.moveTo(x+r,y);ctx.arcTo(x+w,y,x+w,y+h,r);ctx.arcTo(x+w,y+h,x,y+h,r);ctx.arcTo(x,y+h,x,y,r);ctx.arcTo(x,y,x+w,y,r);ctx.closePath();}

  function drawMushrooms(t){
    for(const m of mushrooms){
      const x=m.fx*W,y=m.fy*H,s=m.scale*Math.min(W,H)*0.012;
      const glow=(0.5+0.5*Math.sin(t*1.4+m.seed))*theme.mush;
      // glow
      const gg=ctx.createRadialGradient(x,y-s,0,x,y-s,s*5);
      gg.addColorStop(0,rgba(theme.accentB,0.4*glow*theme.mush));
      gg.addColorStop(1,rgba(theme.accentB,0));
      ctx.fillStyle=gg;ctx.fillRect(x-s*5,y-s*6,s*10,s*10);
      // stem
      ctx.fillStyle=rgba(theme.sil,0.9);ctx.fillRect(x-s*0.28,y-s*1.1,s*0.56,s*1.2);
      // cap
      ctx.fillStyle=rgba(theme.accentB,0.55+0.4*glow);
      ctx.beginPath();ctx.ellipse(x,y-s*1.1,s*1.05,s*0.7,0,Math.PI,0);ctx.fill();
    }
  }

  /* ---- creature drawing ---- */
  function drawHuman(a,t,phase){
    const x=a.fx*W,y=a.fy*H;
    const s=a.scale*Math.min(W,H)*0.028*theme.density*0.9+a.scale*Math.min(W,H)*0.02;
    const e=beatEnv(phase,a.off)*(state.reduce?0.12:1);
    const sway=noise(a.seed+t*0.4)-0.5;
    ctx.save();ctx.translate(x,y);
    let rot=0, sqz=1, armUp=0, hip=0, lift=0;
    if(a.behav==='spin'){ rot=(t*0.9+a.off)*1.1*(state.reduce?0.1:1)+ sway*0.4; armUp=0.3+e*0.3;}
    else if(a.behav==='bend'){ sqz=1-0.22*e; lift=e*s*0.05; armUp=0.15+e*0.2;}
    else if(a.behav==='clap'){ armUp=0.55+e*0.55; lift=-e*s*0.14;}
    else { rot=sway*0.25; armUp=0.2+ (0.5+0.5*Math.sin(t+a.off))*0.2; }
    ctx.rotate(rot*0.12 + sway*0.05);
    ctx.translate(0,lift);
    ctx.strokeStyle=rgba(theme.sil,0.95);
    ctx.fillStyle=rgba(theme.sil,0.95);
    ctx.lineWidth=s*0.26;ctx.lineCap='round';ctx.lineJoin='round';
    const hipY=-s*sqz*1.0, shY=-s*sqz*1.9, headY=shY-s*0.5;
    // legs
    const kneeSpread=s*0.28*(1+ (a.behav==='bend'?e*0.6:0));
    ctx.beginPath();
    ctx.moveTo(0,hipY);ctx.lineTo(-kneeSpread,-s*0.1);ctx.moveTo(0,hipY);ctx.lineTo(kneeSpread,-s*0.1);
    ctx.stroke();
    // torso
    ctx.beginPath();ctx.moveTo(0,hipY);ctx.lineTo(0,shY);ctx.stroke();
    // arms
    const ang=lerp(2.5,1.25,clamp(armUp,0,1)); // from down to overhead
    const aL=Math.PI+ang, aR=-ang;
    const armLen=s*0.9;
    ctx.beginPath();
    ctx.moveTo(0,shY);ctx.lineTo(Math.cos(aL)*armLen*0+ -Math.sin(ang)*armLen, shY-Math.cos(ang)*armLen*(armUp>0.5?1:0.4)*0 - armLen*Math.sin(clamp(armUp,0,1)*1.2)*0);
    ctx.stroke();
    // simpler explicit arms:
    ctx.beginPath();
    const handY=shY - Math.sin(clamp(armUp,0,1)*1.4)*armLen;
    const handX=Math.cos(clamp(armUp,0,1)*1.4)*armLen*0.7;
    ctx.moveTo(0,shY);ctx.lineTo(-handX,handY);
    ctx.moveTo(0,shY);ctx.lineTo(handX,handY);
    ctx.stroke();
    if(a.behav==='clap'&&armUp>0.9){ // clap flash overhead
      ctx.fillStyle=rgba(theme.accentB,0.4*e);
      ctx.beginPath();ctx.arc(0,handY,s*0.4,0,7);ctx.fill();
    }
    // head
    ctx.fillStyle=rgba(theme.sil,0.97);
    ctx.beginPath();ctx.arc(0,headY,s*0.32,0,7);ctx.fill();
    ctx.restore();
  }

  function drawDeer(a,t,phase){
    const x=a.fx*W,y=a.fy*H,s=a.scale*Math.min(W,H)*0.03;
    const e=beatEnv(phase,a.off)*(state.reduce?0.15:1);
    const step=(0.5+0.5*Math.sin(t*2+a.off))*e;
    const headTurn=Math.sin(t*0.7+a.seed)*0.5;
    ctx.save();ctx.translate(x,y-step*s*0.15);ctx.scale(a.face,1);
    ctx.fillStyle=rgba(theme.sil,0.95);ctx.strokeStyle=rgba(theme.sil,0.95);
    ctx.lineWidth=s*0.14;ctx.lineCap='round';
    // legs
    ctx.beginPath();
    ctx.moveTo(-s*0.5,-s*0.5);ctx.lineTo(-s*0.55+step*s*0.2,0);
    ctx.moveTo(-s*0.25,-s*0.5);ctx.lineTo(-s*0.2,0);
    ctx.moveTo(s*0.3,-s*0.5);ctx.lineTo(s*0.3-step*s*0.2,0);
    ctx.moveTo(s*0.5,-s*0.5);ctx.lineTo(s*0.55,0);
    ctx.stroke();
    // body
    ctx.beginPath();ctx.ellipse(0,-s*0.7,s*0.62,s*0.34,0,0,7);ctx.fill();
    // neck+head
    ctx.save();ctx.translate(s*0.55,-s*0.95);ctx.rotate(-0.5+headTurn*0.4);
    ctx.fillRect(-s*0.08,-s*0.6,s*0.18,s*0.7);
    ctx.beginPath();ctx.ellipse(0,-s*0.6,s*0.22,s*0.14,0,0,7);ctx.fill();
    // antlers
    ctx.lineWidth=s*0.07;
    ctx.beginPath();
    ctx.moveTo(-s*0.05,-s*0.7);ctx.lineTo(-s*0.2,-s*1.05);ctx.moveTo(-s*0.14,-s*0.9);ctx.lineTo(-s*0.28,-s*0.95);
    ctx.moveTo(s*0.05,-s*0.7);ctx.lineTo(s*0.18,-s*1.05);ctx.moveTo(s*0.12,-s*0.9);ctx.lineTo(s*0.26,-s*0.95);
    ctx.stroke();
    ctx.restore();
    ctx.restore();
  }

  function drawBear(a,t,phase){
    const x=a.fx*W,y=a.fy*H,s=a.scale*Math.min(W,H)*0.036;
    const e=beatEnv(phase,a.off)*(state.reduce?0.12:1);
    const rock=Math.sin(t*1.6+a.off)*(0.2+e*0.35);
    ctx.save();ctx.translate(x,y);ctx.rotate(rock*0.14);ctx.scale(a.face,1);
    ctx.fillStyle=rgba(theme.sil,0.96);
    // legs
    ctx.beginPath();ctx.ellipse(-s*0.3,-s*0.15,s*0.22,s*0.3,0,0,7);ctx.fill();
    ctx.beginPath();ctx.ellipse(s*0.3,-s*0.15,s*0.22,s*0.3,0,0,7);ctx.fill();
    // body
    ctx.beginPath();ctx.ellipse(0,-s*0.75,s*0.62,s*0.7,0,0,7);ctx.fill();
    // head
    ctx.beginPath();ctx.arc(0,-s*1.5,s*0.42,0,7);ctx.fill();
    // ears
    ctx.beginPath();ctx.arc(-s*0.3,-s*1.85,s*0.16,0,7);ctx.arc(s*0.3,-s*1.85,s*0.16,0,7);ctx.fill();
    ctx.restore();
  }

  function drawWolf(a,t,phase){
    // gentle looping run
    const ang=(t*0.5*a.dir + a.off)*(state.reduce?0.15:1);
    const lx=(a.cx+Math.cos(ang)*a.loopR)*W;
    const ly=(a.cy+Math.sin(ang)*a.loopR*0.4)*H;
    a.fx=lx/W; a.fy=ly/H;
    const s=a.scale*Math.min(W,H)*0.024;
    const dir=Math.cos(ang)*a.dir<0?-1:1;
    const bob=Math.sin(t*6+a.off)*s*0.08*(state.reduce?0.2:1);
    ctx.save();ctx.translate(lx,ly+bob);ctx.scale(dir,1);
    ctx.fillStyle=rgba(theme.sil,0.95);ctx.strokeStyle=rgba(theme.sil,0.95);
    ctx.lineWidth=s*0.13;ctx.lineCap='round';
    const gait=Math.sin(t*8+a.off);
    ctx.beginPath();
    ctx.moveTo(-s*0.4,-s*0.4);ctx.lineTo(-s*0.4+gait*s*0.3,0);
    ctx.moveTo(s*0.35,-s*0.4);ctx.lineTo(s*0.35-gait*s*0.3,0);
    ctx.stroke();
    // body
    ctx.beginPath();ctx.ellipse(0,-s*0.6,s*0.6,s*0.26,0,0,7);ctx.fill();
    // tail
    ctx.beginPath();ctx.moveTo(-s*0.55,-s*0.6);ctx.quadraticCurveTo(-s*0.95,-s*0.5,-s*0.85,-s*0.9);ctx.lineWidth=s*0.2;ctx.stroke();
    // head
    ctx.save();ctx.translate(s*0.6,-s*0.7);
    ctx.beginPath();ctx.moveTo(0,0);ctx.lineTo(s*0.4,-s*0.05);ctx.lineTo(s*0.15,s*0.2);ctx.closePath();ctx.fill();
    // ears
    ctx.beginPath();ctx.moveTo(-s*0.02,-s*0.05);ctx.lineTo(-s*0.05,-s*0.3);ctx.lineTo(s*0.12,-s*0.1);ctx.closePath();ctx.fill();
    ctx.restore();
    ctx.restore();
  }

  function drawFrog(a,t,phase){
    const x=a.fx*W,y=a.fy*H,s=a.scale*Math.min(W,H)*0.013;
    const e=beatEnv(phase,a.off)*(state.reduce?0.1:1);
    const jump=Math.max(0,Math.sin(t*3+a.off))*e;
    const hop=jump*s*2.2;
    const sq=1+jump*0.3, st=1-jump*0.2;
    ctx.save();ctx.translate(x,y-hop);ctx.scale(a.face,1);
    ctx.fillStyle=rgba(theme.sil,0.95);
    // legs
    ctx.beginPath();ctx.ellipse(-s*0.7,s*0.1,s*0.3*sq,s*0.16,0.6,0,7);ctx.fill();
    ctx.beginPath();ctx.ellipse(s*0.7,s*0.1,s*0.3*sq,s*0.16,-0.6,0,7);ctx.fill();
    // body
    ctx.beginPath();ctx.ellipse(0,0,s*0.7*sq,s*0.55*st,0,0,7);ctx.fill();
    // eyes
    ctx.beginPath();ctx.arc(-s*0.28,-s*0.5,s*0.2,0,7);ctx.arc(s*0.28,-s*0.5,s*0.2,0,7);ctx.fill();
    ctx.fillStyle=rgba(theme.accentB,0.8);
    ctx.beginPath();ctx.arc(-s*0.28,-s*0.5,s*0.08,0,7);ctx.arc(s*0.28,-s*0.5,s*0.08,0,7);ctx.fill();
    ctx.restore();
  }

  /* ---- fireflies ---- */
  function updateFireflies(t,dt){
    const spiral=theme.spiral;
    for(const f of fireflies){
      if(!f.init){f.x=f.fx*W;f.y=f.fy*H;f.init=true;}
      let tx,ty;
      if(spiral>0.05){
        const c=spiralCenters[f.spGroup];
        const rot=t*0.6*(1+f.spGroup*0.1)+f.phase;
        const rad=f.sr*Math.min(W,H)* (0.4+ (f.phase%1));
        tx=c.fx*W + Math.cos(rot)*rad;
        ty=c.fy*H + Math.sin(rot)*rad*0.7;
      }else{ tx=null; }
      // wander target
      const wx=(0.5+0.45*(noise(f.seed*0.13+t*0.15)-0.5)*2)*W;
      const wy=(0.55+0.4*(noise(f.seed*0.21+t*0.12+50)-0.5)*2)*H;
      let gx = spiral>0.05 ? lerp(wx,tx,spiral) : wx;
      let gy = spiral>0.05 ? lerp(wy,ty,spiral) : wy;
      const sp=state.reduce?0.4:2.2;
      f.vx += (gx-f.x)*0.0009*sp; f.vy += (gy-f.y)*0.0009*sp;
      f.vx*=0.94; f.vy*=0.94;
      f.x+=f.vx*dt*60; f.y+=f.vy*dt*60;
    }
  }
  function drawFireflies(t){
    ctx.globalCompositeOperation='lighter';
    for(const f of fireflies){
      const blink=0.4+0.6*Math.abs(Math.sin(t*(1.5+ (f.seed%1)) +f.phase));
      const r=Math.min(W,H)*0.004*(0.6+blink*0.8);
      const g=ctx.createRadialGradient(f.x,f.y,0,f.x,f.y,r*4);
      g.addColorStop(0,rgba(theme.fire,0.9*blink));
      g.addColorStop(0.4,rgba(theme.accentB,0.4*blink));
      g.addColorStop(1,rgba(theme.fire,0));
      ctx.fillStyle=g;ctx.beginPath();ctx.arc(f.x,f.y,r*4,0,7);ctx.fill();
    }
    ctx.globalCompositeOperation='source-over';
  }

  function drawVignette(){
    const g=ctx.createRadialGradient(0.5*W,0.5*H,Math.min(W,H)*0.3,0.5*W,0.5*H,Math.max(W,H)*0.75);
    g.addColorStop(0,'rgba(0,0,0,0)');g.addColorStop(1,'rgba(0,0,0,0.55)');
    ctx.fillStyle=g;ctx.fillRect(0,0,W,H);
  }

  /* ---------- main loop ---------- */
  function render(t,dt){
    tickTheme(dt);
    // global pulse energy (throb) for lighting even in reduced motion (slow)
    const phase = t*state.tempo;
    state.energy = state.reduce
      ? 0.35+0.25*Math.sin(t*0.6)
      : lerp(state.energy, 0.3+0.7*beatEnv(phase,0), 0.25);

    drawSky();
    drawStars(t);
    drawMoon(t);
    drawTrees(t);
    drawGround();

    ctx.save();
    stageTransform();
    drawSpeakers(t);
    drawMushrooms(t);

    // creatures sorted by y (depth)
    const vis=creatures.filter(c=> (c.side==='human'&&state.show.human)||(c.side==='animal'&&state.show.animal));
    // handle human drift between groups
    if(!state.reduce){
      for(const c of creatures){
        if(c.kind==='human'&&c.behav==='drift'){
          const g=groups[c.driftTarget];
          const spd=0.06*dt;
          c.fx=lerp(c.fx,g.x,spd); c.fy=lerp(c.fy,g.y,spd);
          if(Math.abs(c.fx-g.x)<0.01){ c.driftG=c.driftTarget; c.driftTarget=(c.driftTarget+ (Math.random()<0.5?1:2))%groups.length; }
        }
      }
    }
    vis.sort((a,b)=>a.fy-b.fy);
    for(const c of vis){
      if(c.kind==='human')drawHuman(c,t,phase);
      else if(c.kind==='deer')drawDeer(c,t,phase);
      else if(c.kind==='bear')drawBear(c,t,phase);
      else if(c.kind==='wolf')drawWolf(c,t,phase);
      else if(c.kind==='frog')drawFrog(c,t,phase);
    }
    ctx.restore();

    updateFireflies(t,dt);
    drawFireflies(t);
    drawVignette();
  }

  function frame(now){
    if(!state.last)state.last=now;
    let dt=(now-state.last)/1000; state.last=now;
    dt=clamp(dt,0,0.05);
    if(!state.paused){ state.t += dt; }
    render(state.t,state.paused?0.0001:dt);
    requestAnimationFrame(frame);
  }
  requestAnimationFrame(frame);

  /* ---------- controls ---------- */
  function announce(msg){document.getElementById('announce').textContent=msg;}

  document.querySelectorAll('.movement-seg button').forEach(b=>{
    b.addEventListener('click',()=>setMovement(+b.dataset.move));
  });

  const isoBoth=document.getElementById('isoBoth'),
        isoH=document.getElementById('isoHumans'),
        isoA=document.getElementById('isoAnimals');
  function setIso(mode){
    state.show.human = (mode==='both'||mode==='human');
    state.show.animal= (mode==='both'||mode==='animal');
    isoBoth.setAttribute('aria-pressed',mode==='both');
    isoH.setAttribute('aria-pressed',mode==='human');
    isoA.setAttribute('aria-pressed',mode==='animal');
    announce(mode==='both'?'Showing all choreography':mode==='human'?'Isolated human choreography':'Isolated animal choreography');
  }
  isoBoth.addEventListener('click',()=>setIso('both'));
  isoH.addEventListener('click',()=>setIso('human'));
  isoA.addEventListener('click',()=>setIso('animal'));

  const pauseBtn=document.getElementById('pauseBtn');
  pauseBtn.addEventListener('click',()=>{
    state.paused=!state.paused;
    pauseBtn.setAttribute('aria-pressed',state.paused);
    pauseBtn.textContent=state.paused?'Resume everything':'Pause everything';
    document.getElementById('tagState').textContent=state.paused?'Paused':(state.reduce?'Reduced':'Playing');
    announce(state.paused?'Everything paused':'Resumed');
  });

  const reduceBtn=document.getElementById('reduceBtn');
  function setReduce(v){
    state.reduce=v;
    reduceBtn.setAttribute('aria-pressed',v);
    reduceBtn.textContent=v?'Full motion':'Reduced motion';
    document.getElementById('tagState').textContent=state.paused?'Paused':(v?'Reduced':'Playing');
    announce(v?'Reduced motion on — only slow lighting changes':'Full motion restored');
  }
  reduceBtn.addEventListener('click',()=>setReduce(!state.reduce));

  const tempo=document.getElementById('tempo'),tempoVal=document.getElementById('tempoVal');
  tempo.addEventListener('input',()=>{
    state.tempo=+tempo.value; tempoVal.textContent=(+tempo.value).toFixed(1)+'×';
  });

  // Respect OS reduced-motion preference on load
  if(window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches){
    setReduce(true);
  }

  // keyboard: space toggles pause when not on a control
  document.addEventListener('keydown',(ev)=>{
    if(ev.code==='Space' && !/BUTTON|INPUT|SUMMARY|DETAILS/.test(document.activeElement.tagName)){
      ev.preventDefault();pauseBtn.click();
    }
  });
})();
</script>
</body>
</html>