← back to Model Arena
verify five new Grateful Dead gallery builds
37b006700f868d7d493189b82d769728ac3961f6 · 2026-09-01 17:12:33 -0700 · Steve Abrams
Files touched
A data/artifacts/127317180daf/claude-code.htmlA data/artifacts/127317180daf/qwen25-7b.htmlA data/artifacts/127317180daf/qwen3-14b.htmlA data/artifacts/77fc58739877/claude-code.htmlA data/artifacts/77fc58739877/qwen25-7b.htmlM data/artifacts/77fc58739877/qwen25-7b.pngA data/artifacts/77fc58739877/qwen3-14b.htmlM data/artifacts/77fc58739877/qwen3-14b.pngA data/artifacts/97621618f31c/claude-code.htmlA data/artifacts/97621618f31c/qwen25-7b.htmlA data/artifacts/97621618f31c/qwen3-14b.htmlA data/artifacts/cd83de847d24/claude-code.htmlM data/artifacts/cd83de847d24/claude-code.pngA data/artifacts/cd83de847d24/qwen25-7b.htmlM data/artifacts/cd83de847d24/qwen25-7b.pngA data/artifacts/cd83de847d24/qwen3-14b.htmlA data/artifacts/ec9e68a64f43/claude-code.htmlA data/artifacts/ec9e68a64f43/qwen25-7b.htmlA data/artifacts/ec9e68a64f43/qwen3-14b.htmlM data/challenges.jsonA verification/grateful-dead-poster-five-live.pngA verification/grateful-dead-poster-five-proof.jsonA verification/grateful-dead-poster-five.e2e.js
Diff
commit 37b006700f868d7d493189b82d769728ac3961f6
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Tue Sep 1 17:12:33 2026 -0700
verify five new Grateful Dead gallery builds
---
data/artifacts/127317180daf/claude-code.html | 918 ++++++++++++++++++++
data/artifacts/127317180daf/qwen25-7b.html | 176 ++++
data/artifacts/127317180daf/qwen3-14b.html | 524 ++++++++++++
data/artifacts/77fc58739877/claude-code.html | 973 ++++++++++++++++++++++
data/artifacts/77fc58739877/qwen25-7b.html | 128 +++
data/artifacts/77fc58739877/qwen25-7b.png | Bin 6930 -> 2731 bytes
data/artifacts/77fc58739877/qwen3-14b.html | 423 ++++++++++
data/artifacts/77fc58739877/qwen3-14b.png | Bin 21564 -> 25509 bytes
data/artifacts/97621618f31c/claude-code.html | 865 +++++++++++++++++++
data/artifacts/97621618f31c/qwen25-7b.html | 146 ++++
data/artifacts/97621618f31c/qwen3-14b.html | 511 ++++++++++++
data/artifacts/cd83de847d24/claude-code.html | 887 ++++++++++++++++++++
data/artifacts/cd83de847d24/claude-code.png | Bin 57097 -> 76250 bytes
data/artifacts/cd83de847d24/qwen25-7b.html | 182 ++++
data/artifacts/cd83de847d24/qwen25-7b.png | Bin 32233 -> 79849 bytes
data/artifacts/cd83de847d24/qwen3-14b.html | 384 +++++++++
data/artifacts/ec9e68a64f43/claude-code.html | 794 ++++++++++++++++++
data/artifacts/ec9e68a64f43/qwen25-7b.html | 71 ++
data/artifacts/ec9e68a64f43/qwen3-14b.html | 400 +++++++++
data/challenges.json | 56 +-
verification/grateful-dead-poster-five-live.png | Bin 0 -> 90055 bytes
verification/grateful-dead-poster-five-proof.json | 39 +
verification/grateful-dead-poster-five.e2e.js | 79 ++
23 files changed, 7528 insertions(+), 28 deletions(-)
diff --git a/data/artifacts/127317180daf/claude-code.html b/data/artifacts/127317180daf/claude-code.html
new file mode 100644
index 0000000..0ab0589
--- /dev/null
+++ b/data/artifacts/127317180daf/claude-code.html
@@ -0,0 +1,918 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="UTF-8">
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+<title>Rose City Rooftop Solstice</title>
+<style>
+ :root{
+ --ink:#f6ecff;
+ --ink-dim:#c9bce0;
+ --panel:rgba(18,10,38,.72);
+ --panel-solid:#160a2e;
+ --edge:rgba(255,214,170,.28);
+ --gold:#f7c56b;
+ --rose:#ff7fae;
+ --violet:#b48bff;
+ --focus:#7ff0e6;
+ --shadow:0 18px 50px rgba(0,0,0,.55);
+ font-synthesis:none;
+ }
+ *{box-sizing:border-box}
+ html,body{height:100%;margin:0}
+ body{
+ background:#0a0618;
+ color:var(--ink);
+ font-family:"Iowan Old Style","Palatino Linotype",Palatino,Georgia,"Times New Roman",serif;
+ overflow:hidden;
+ -webkit-font-smoothing:antialiased;
+ }
+ #stage-wrap{position:fixed;inset:0}
+ canvas{display:block;width:100%;height:100%}
+
+ /* Title */
+ .masthead{
+ position:fixed;top:0;left:0;right:0;padding:18px 22px 40px;
+ pointer-events:none;
+ background:linear-gradient(180deg,rgba(6,3,16,.55),rgba(6,3,16,0));
+ z-index:3;
+ }
+ .masthead h1{
+ margin:0;font-weight:500;letter-spacing:.11em;
+ font-size:clamp(19px,3.1vw,34px);
+ text-transform:uppercase;
+ text-shadow:0 2px 22px rgba(0,0,0,.6);
+ }
+ .masthead h1 .rose{color:var(--rose)}
+ .masthead p{
+ margin:.35em 0 0;font-size:clamp(11px,1.5vw,14px);
+ color:var(--ink-dim);letter-spacing:.16em;text-transform:uppercase;
+ font-family:ui-sans-serif,system-ui,"Helvetica Neue",Arial,sans-serif;
+ }
+ .scene-flag{
+ display:inline-block;margin-top:.5em;padding:.28em .8em;
+ border:1px solid var(--edge);border-radius:999px;
+ font-family:ui-sans-serif,system-ui,sans-serif;font-size:12px;
+ letter-spacing:.22em;color:var(--gold);background:rgba(20,10,40,.4);
+ }
+
+ /* Controls */
+ .controls{
+ position:fixed;top:14px;right:14px;z-index:6;
+ width:min(320px,90vw);max-height:calc(100vh - 28px);overflow:auto;
+ background:var(--panel);backdrop-filter:blur(11px);
+ border:1px solid var(--edge);border-radius:16px;
+ box-shadow:var(--shadow);
+ font-family:ui-sans-serif,system-ui,"Helvetica Neue",Arial,sans-serif;
+ scrollbar-width:thin;
+ }
+ .controls header{
+ display:flex;align-items:center;justify-content:space-between;
+ padding:12px 14px;position:sticky;top:0;background:var(--panel-solid);
+ border-bottom:1px solid var(--edge);z-index:2;border-radius:16px 16px 0 0;
+ }
+ .controls header h2{margin:0;font-size:13px;letter-spacing:.16em;text-transform:uppercase;color:var(--gold);font-weight:600}
+ .toggle-panel{
+ background:none;border:1px solid var(--edge);color:var(--ink);
+ border-radius:8px;padding:4px 9px;cursor:pointer;font-size:12px;
+ }
+ .controls .body{padding:6px 14px 16px}
+ fieldset{border:none;border-top:1px solid rgba(255,255,255,.08);margin:0;padding:14px 0 4px}
+ fieldset:first-of-type{border-top:none}
+ legend{
+ padding:0;font-size:11px;letter-spacing:.2em;text-transform:uppercase;
+ color:var(--violet);font-weight:600;margin-bottom:8px;
+ }
+ .btnrow{display:flex;flex-wrap:wrap;gap:6px}
+ .seg{
+ flex:1 1 auto;min-width:64px;text-align:center;
+ background:rgba(255,255,255,.05);border:1px solid transparent;
+ color:var(--ink);border-radius:9px;padding:8px 6px;cursor:pointer;
+ font-size:12px;letter-spacing:.04em;transition:background .18s,border-color .18s;
+ }
+ .seg:hover{background:rgba(255,255,255,.12)}
+ .seg[aria-pressed="true"],.seg.active{
+ background:linear-gradient(180deg,rgba(255,127,174,.35),rgba(180,139,255,.28));
+ border-color:var(--edge);color:#fff;font-weight:600;
+ }
+ .slider{margin:10px 0}
+ .slider label{display:flex;justify-content:space-between;font-size:12px;color:var(--ink-dim);margin-bottom:4px}
+ .slider output{color:var(--gold);font-variant-numeric:tabular-nums}
+ input[type="range"]{width:100%;accent-color:var(--rose);height:22px;cursor:pointer}
+ .checks{display:grid;grid-template-columns:1fr 1fr;gap:6px 10px}
+ .chk{display:flex;align-items:center;gap:7px;font-size:12.5px;color:var(--ink);cursor:pointer;user-select:none}
+ .chk input{accent-color:var(--violet);width:15px;height:15px;cursor:pointer}
+ .transport{display:flex;gap:6px}
+ .transport .seg{font-weight:600}
+ .hint{font-size:11px;color:var(--ink-dim);margin:10px 0 0;line-height:1.5}
+ kbd{
+ font-family:ui-monospace,Menlo,Consolas,monospace;font-size:10.5px;
+ background:rgba(255,255,255,.1);border:1px solid var(--edge);
+ border-radius:4px;padding:1px 5px;color:var(--ink);
+ }
+ .note{
+ position:fixed;left:14px;bottom:12px;z-index:5;max-width:min(430px,72vw);
+ font-family:ui-sans-serif,system-ui,sans-serif;font-size:11.5px;line-height:1.5;
+ color:var(--ink-dim);background:rgba(10,5,24,.5);backdrop-filter:blur(6px);
+ border:1px solid rgba(255,255,255,.08);border-left:2px solid var(--gold);
+ border-radius:0 10px 10px 0;padding:9px 13px;
+ }
+ .note b{color:var(--gold);font-weight:600}
+
+ /* Focus states */
+ a:focus-visible,button:focus-visible,input:focus-visible,.chk:focus-within,[tabindex]:focus-visible{
+ outline:3px solid var(--focus);outline-offset:2px;border-radius:8px;
+ }
+ .skip{position:fixed;left:-999px;top:0;z-index:20;background:var(--panel-solid);color:#fff;padding:10px 16px;border-radius:0 0 10px 0}
+ .skip:focus{left:0}
+
+ .controls.collapsed .body{display:none}
+ @media (max-width:560px){
+ .controls{width:calc(100vw - 20px);left:10px;right:10px}
+ .note{max-width:calc(100vw - 28px)}
+ }
+ @media (prefers-reduced-motion:reduce){
+ *{scroll-behavior:auto}
+ }
+</style>
+</head>
+<body>
+<a class="skip" href="#controls">Skip to controls</a>
+
+<div id="stage-wrap">
+ <canvas id="scene" role="img"
+ aria-label="An animated urban-fantasy rooftop garden at solstice sunset: dancers spin around a rose-shaped light sculpture, couples cross narrow bridges, freestyle dancers trail curves near water towers, and a quiet procession climbs fire escapes while bears, cats, pigeons, dogs and moths move through the scene."></canvas>
+</div>
+
+<header class="masthead" aria-hidden="false">
+ <h1>Rose City <span class="rose">Rooftop</span> Solstice</h1>
+ <p>An authored dance-garden above the glowing city</p><br>
+ <span class="scene-flag" id="sceneFlag">Arrival</span>
+</header>
+
+<form class="controls" id="controls" aria-label="Composition controls">
+ <header>
+ <h2>Direct the Solstice</h2>
+ <button type="button" class="toggle-panel" id="panelToggle" aria-expanded="true" aria-controls="ctlBody">Hide</button>
+ </header>
+ <div class="body" id="ctlBody">
+
+ <fieldset>
+ <legend>Camera</legend>
+ <div class="btnrow" role="group" aria-label="Camera view">
+ <button type="button" class="seg" data-cam="skyline" aria-pressed="false">Skyline</button>
+ <button type="button" class="seg" data-cam="rooftop" aria-pressed="true">Rooftop</button>
+ <button type="button" class="seg" data-cam="birdseye" aria-pressed="false">Bird’s-eye</button>
+ </div>
+ </fieldset>
+
+ <fieldset>
+ <legend>Scene</legend>
+ <div class="btnrow" role="group" aria-label="Scene chapter">
+ <button type="button" class="seg" data-scene="0" aria-pressed="false">Arrival</button>
+ <button type="button" class="seg" data-scene="1" aria-pressed="false">Long Light</button>
+ <button type="button" class="seg" data-scene="2" aria-pressed="false">Midnight Bloom</button>
+ <button type="button" class="seg active" data-scene="auto" aria-pressed="true">Auto ↻</button>
+ </div>
+ </fieldset>
+
+ <fieldset>
+ <legend>Tempo & Crowd</legend>
+ <div class="slider">
+ <label for="tempo">Visual tempo <output id="tempoOut">1.0×</output></label>
+ <input type="range" id="tempo" min="0" max="2" step="0.05" value="1">
+ </div>
+ <div class="slider">
+ <label for="density">Crowd density <output id="densityOut">70%</output></label>
+ <input type="range" id="density" min="0" max="1" step="0.02" value="0.7">
+ </div>
+ </fieldset>
+
+ <fieldset>
+ <legend>Isolate Choreography</legend>
+ <div class="checks">
+ <label class="chk"><input type="checkbox" data-grp="circle" checked> Rose circle</label>
+ <label class="chk"><input type="checkbox" data-grp="couples" checked> Bridge couples</label>
+ <label class="chk"><input type="checkbox" data-grp="freestyle" checked> Freestyle</label>
+ <label class="chk"><input type="checkbox" data-grp="procession" checked> Procession</label>
+ <label class="chk"><input type="checkbox" data-grp="animals" checked> Animals</label>
+ </div>
+ </fieldset>
+
+ <fieldset>
+ <legend>Overlays</legend>
+ <div class="checks">
+ <label class="chk"><input type="checkbox" id="ovNames"> Animal names</label>
+ <label class="chk"><input type="checkbox" id="ovTrails" checked> Motion trails</label>
+ <label class="chk"><input type="checkbox" id="ovRoutes"> Route diagrams</label>
+ <label class="chk"><input type="checkbox" id="ovReduced"> Reduced motion</label>
+ </div>
+ </fieldset>
+
+ <fieldset>
+ <legend>Transport</legend>
+ <div class="transport" role="group" aria-label="Playback">
+ <button type="button" class="seg" id="btnPause" aria-pressed="false">⏸ Pause</button>
+ <button type="button" class="seg" id="btnRestart">↺ Restart</button>
+ </div>
+ <p class="hint">
+ <kbd>Space</kbd> pause · <kbd>1</kbd><kbd>2</kbd><kbd>3</kbd> cameras ·
+ <kbd>R</kbd> reduced motion · <kbd>N</kbd> names · <kbd>T</kbd> trails · <kbd>D</kbd> routes.
+ </p>
+ </fieldset>
+
+ </div>
+</form>
+
+<p class="note">
+ <b>An honest note.</b> This piece keeps its own solstice time. Its visual rhythm is
+ <b>authored in code</b> — every sway, spiral and bloom is choreographed by hand and does
+ <b>not</b> listen to any audio. Imagery is wholly original, evoking the generous,
+ improvisational spirit of open-air gatherings.
+</p>
+
+<script>
+(() => {
+"use strict";
+
+/* ---------- setup ---------- */
+const canvas = document.getElementById('scene');
+const ctx = canvas.getContext('2d');
+const STAGE_W = 1600, STAGE_H = 900;
+let W = 0, H = 0, DPR = 1, viewScale = 1;
+
+function resize(){
+ DPR = Math.min(window.devicePixelRatio||1, 2);
+ W = window.innerWidth; H = window.innerHeight;
+ canvas.width = Math.floor(W*DPR); canvas.height = Math.floor(H*DPR);
+ canvas.style.width = W+'px'; canvas.style.height = H+'px';
+ ctx.setTransform(DPR,0,0,DPR,0,0);
+ viewScale = Math.max(W/STAGE_W, H/STAGE_H) * 1.02;
+ if(state.reduced) render();
+}
+window.addEventListener('resize', resize);
+
+/* ---------- state ---------- */
+const state = {
+ tempo:1, density:0.7, paused:false, reduced:false,
+ camera:'rooftop',
+ sceneMode:'auto', scenePinned:0, scenePhase:0,
+ show:{circle:true,couples:true,freestyle:true,procession:true,animals:true},
+ names:false, trails:true, routes:false
+};
+
+/* ---------- seeded rng ---------- */
+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;};}
+const rand = mulberry32(20260621);
+
+/* ---------- palettes / scenes ---------- */
+const SCENES = [
+ { name:'Arrival', top:'#f7b25a', mid:'#c9527d', low:'#3a2350', glow:'#ffd98a', star:0.0, cx:0.30 },
+ { name:'Long Light', top:'#ff8f5e', mid:'#b13a80', low:'#2c1e5a', glow:'#ff9ec4', star:0.15, cx:0.70 },
+ { name:'Midnight Bloom', top:'#3a2f74', mid:'#6a2c82', low:'#120a2e', glow:'#c39bff', star:1.0, cx:0.95 }
+];
+const lerp=(a,b,t)=>a+(b-a)*t;
+function hx(c){return [parseInt(c.slice(1,3),16),parseInt(c.slice(3,5),16),parseInt(c.slice(5,7),16)];}
+function mix(c1,c2,t){const a=hx(c1),b=hx(c2);return `rgb(${Math.round(lerp(a[0],b[0],t))},${Math.round(lerp(a[1],b[1],t))},${Math.round(lerp(a[2],b[2],t))})`;}
+function scenePal(){
+ const p = state.scenePhase;
+ const i = Math.floor(p)%3, f = p-Math.floor(p), j=(i+1)%3;
+ const A=SCENES[i], B=SCENES[j];
+ return {
+ top:mix(A.top,B.top,f), mid:mix(A.mid,B.mid,f), low:mix(A.low,B.low,f),
+ glow:mix(A.glow,B.glow,f), star:lerp(A.star,B.star,f),
+ complexity:lerp(A.cx,B.cx,f), idx:i, frac:f, name:SCENES[i].name
+ };
+}
+
+/* ---------- static world geometry ---------- */
+// skyline buildings (parallax)
+const buildings = [];
+(function(){
+ // far layer
+ for(let i=0;i<26;i++){
+ const w=40+rand()*70; const x=rand()*STAGE_W;
+ buildings.push({x, w, h:120+rand()*260, depth:0.25, y:STAGE_H*0.62, hue:rand()});
+ }
+ // mid layer
+ for(let i=0;i<16;i++){
+ const w=70+rand()*120; const x=rand()*STAGE_W;
+ buildings.push({x, w, h:200+rand()*320, depth:0.55, y:STAGE_H*0.72, hue:rand()});
+ }
+})();
+buildings.sort((a,b)=>a.depth-b.depth);
+
+const clouds = [];
+for(let i=0;i<8;i++) clouds.push({x:rand()*STAGE_W, y:80+rand()*260, s:0.6+rand()*1.4, sp:6+rand()*14, d:0.35+rand()*0.4});
+
+// laundry lines between buildings
+const laundry=[];
+for(let i=0;i<5;i++){
+ const x=200+rand()*1100, y=180+rand()*180, len=90+rand()*140;
+ const items=3+Math.floor(rand()*4), pcs=[];
+ for(let k=0;k<items;k++) pcs.push({t:(k+1)/(items+1), c:['#ff9ec4','#ffe7a8','#9be7e0','#d9c6ff','#ffd0b0'][Math.floor(rand()*5)], w:14+rand()*10, h:20+rand()*16});
+ laundry.push({x,y,len,pcs,sag:14+rand()*10});
+}
+
+// rooftop platforms
+const roofs=[
+ {x:150,y:610,w:520,h:300}, // left roof
+ {x:640,y:560,w:640,h:340}, // central roof (dance floor)
+ {x:1270,y:640,w:300,h:280} // right roof
+];
+const ROSE={x:960,y:610}; // rose sculpture center (on central roof)
+const towers=[{x:520,y:470},{x:1180,y:430}]; // water towers
+const fireEscapeX=1470; // right building fire escape
+
+/* zone spread by camera (bird's-eye separates zones) */
+function camParams(){
+ switch(state.camera){
+ case 'skyline': return {zoom:0.66, squash:1.0, panX:0, panY:-40, spread:0.92, floor:0, figScale:0.86};
+ case 'birdseye': return {zoom:0.94, squash:0.74, panX:0, panY:120, spread:1.24, floor:1, figScale:1.0};
+ default: return {zoom:1.0, squash:1.0, panX:0, panY:38, spread:1.0, floor:0, figScale:1.06};
+ }
+}
+// map a stage point outward from rose center by spread (for bird's-eye separation)
+function spr(x,y,cp){ return {x: ROSE.x + (x-ROSE.x)*cp.spread, y: ROSE.y + (y-ROSE.y)*cp.spread}; }
+
+/* ---------- trails buffers ---------- */
+const freeTrails=[]; // per freestyle dancer
+const MAXTRAIL=26;
+
+/* ---------- clock ---------- */
+let clock=0, sceneClock=0, last=performance.now(), rafId=null;
+
+function loop(now){
+ const dt=Math.min(0.05,(now-last)/1000); last=now;
+ if(!state.paused && !state.reduced){
+ clock += dt*state.tempo;
+ if(state.sceneMode==='auto'){
+ sceneClock += dt*state.tempo;
+ state.scenePhase = (sceneClock/22)%3; // ~22s per chapter
+ }
+ }
+ render();
+ rafId=requestAnimationFrame(loop);
+}
+function startLoop(){ if(!rafId){ last=performance.now(); rafId=requestAnimationFrame(loop);} }
+function stopLoop(){ if(rafId){ cancelAnimationFrame(rafId); rafId=null;} }
+
+/* ---------- drawing helpers ---------- */
+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 drawDancer(x,y,s,ph,col,energy){
+ const e=state.reduced?0.18:energy;
+ const armA=Math.sin(ph)*1.1*e, armB=Math.sin(ph+2.1)*1.1*e;
+ const legA=Math.sin(ph+0.6)*0.5*e, legB=Math.sin(ph+3.1)*0.5*e;
+ const bob=Math.sin(ph*2)*2.4*e*s;
+ const cx=x, cy=y+bob;
+ ctx.save();
+ ctx.strokeStyle=col; ctx.lineWidth=2.2*s; ctx.lineCap='round'; ctx.fillStyle=col;
+ const hip=cy, sh=cy-13*s, hd=sh-8*s;
+ // legs
+ ctx.beginPath(); ctx.moveTo(cx,hip); ctx.lineTo(cx-6*s+legA*7*s, hip+15*s); ctx.stroke();
+ ctx.beginPath(); ctx.moveTo(cx,hip); ctx.lineTo(cx+6*s+legB*7*s, hip+15*s); ctx.stroke();
+ // torso
+ ctx.beginPath(); ctx.moveTo(cx,hip); ctx.lineTo(cx,sh); ctx.stroke();
+ // arms
+ ctx.beginPath(); ctx.moveTo(cx,sh+2*s); ctx.lineTo(cx-9*s, sh-6*s-armA*10*s); ctx.stroke();
+ ctx.beginPath(); ctx.moveTo(cx,sh+2*s); ctx.lineTo(cx+9*s, sh-6*s-armB*10*s); ctx.stroke();
+ // head
+ ctx.beginPath(); ctx.arc(cx,hd,4.2*s,0,7); ctx.fill();
+ ctx.restore();
+}
+
+function label(x,y,text,col){
+ ctx.save();
+ ctx.font='600 13px ui-sans-serif,system-ui,sans-serif';
+ ctx.textAlign='center';
+ const w=ctx.measureText(text).width+12;
+ ctx.fillStyle='rgba(8,4,20,.6)';
+ roundRect(x-w/2,y-16,w,17,8); ctx.fill();
+ ctx.fillStyle=col||'#fff';
+ ctx.fillText(text,x,y-3);
+ ctx.restore();
+}
+
+/* ---------- background ---------- */
+function drawSky(pal){
+ const g=ctx.createLinearGradient(0,0,0,STAGE_H);
+ g.addColorStop(0,pal.top); g.addColorStop(0.42,pal.mid); g.addColorStop(1,pal.low);
+ ctx.fillStyle=g; ctx.fillRect(-400,-400,STAGE_W+800,STAGE_H+800);
+ // sun / solstice orb sinking with scene
+ const sunY = lerp(320,540,pal.idx===2?1:pal.frac*0.5+ (pal.idx*0.5));
+ const sy = 260 + state.scenePhase*90;
+ const gr=ctx.createRadialGradient(1080,sy,10,1080,sy,220);
+ gr.addColorStop(0,pal.glow); gr.addColorStop(0.4,'rgba(255,210,150,'+(0.5-pal.star*0.3)+')'); gr.addColorStop(1,'rgba(255,210,150,0)');
+ ctx.fillStyle=gr; ctx.beginPath(); ctx.arc(1080,sy,220,0,7); ctx.fill();
+ ctx.fillStyle=mix(pal.glow,'#ffffff',0.4); ctx.beginPath(); ctx.arc(1080,sy,34-state.scenePhase*4,0,7); ctx.fill();
+ // stars
+ if(pal.star>0.02){
+ ctx.save(); ctx.fillStyle='#fff';
+ const sr=mulberry32(7);
+ for(let i=0;i<140;i++){
+ const x=sr()*STAGE_W, y=sr()*STAGE_H*0.55;
+ const tw=state.reduced?0.6:(0.4+0.6*Math.abs(Math.sin(clock*1.5+i)));
+ ctx.globalAlpha=pal.star*tw*0.9; ctx.fillRect(x,y,1.6,1.6);
+ }
+ ctx.restore();
+ }
+}
+
+function drawClouds(pal){
+ ctx.save();
+ for(const c of clouds){
+ const dx = state.reduced ? c.x : (c.x + clock*c.sp*c.d)%(STAGE_W+400)-200;
+ ctx.globalAlpha=0.16+0.12*(1-pal.star);
+ ctx.fillStyle=mix(pal.glow,'#ffffff',0.3);
+ for(let k=0;k<4;k++){
+ ctx.beginPath();
+ ctx.ellipse(dx+k*36*c.s, c.y+Math.sin(k)*8*c.s, 46*c.s-k*4, 20*c.s, 0,0,7);
+ ctx.fill();
+ }
+ }
+ ctx.restore();
+}
+
+function drawBuildings(pal,cp){
+ const drift = state.reduced?0:Math.sin(clock*0.06)*10;
+ for(const b of buildings){
+ const x=b.x + drift*b.depth;
+ const base = mix(pal.low, pal.mid, b.depth*0.5);
+ ctx.fillStyle=mix('#0c0722', base, 0.45+b.depth*0.4);
+ ctx.fillRect(x, b.y-b.h, b.w, b.h);
+ // reflective windows
+ const cols=Math.max(2,Math.floor(b.w/16)), rows=Math.floor(b.h/22);
+ for(let r=0;r<rows;r++){
+ for(let cc=0;cc<cols;cc++){
+ const wx=x+6+cc*16, wy=b.y-b.h+10+r*22;
+ if(wx>x+b.w-8) continue;
+ const idn=(r*7+cc*13+Math.floor(b.x));
+ const lit=(Math.sin(idn)*0.5+0.5);
+ let a=0.10+lit*0.35*(0.4+pal.star*0.8);
+ if(!state.reduced) a*= (0.6+0.4*Math.sin(clock*0.8+idn));
+ ctx.fillStyle= mix(pal.glow, pal.top, (cc+r)%2? .2:.7);
+ ctx.globalAlpha=Math.max(0,a);
+ ctx.fillRect(wx,wy,7,10);
+ }
+ }
+ ctx.globalAlpha=1;
+ }
+}
+
+function drawLaundry(pal){
+ ctx.save();
+ ctx.strokeStyle='rgba(255,255,255,.22)'; ctx.lineWidth=1.4;
+ for(const L of laundry){
+ ctx.beginPath();
+ ctx.moveTo(L.x,L.y);
+ ctx.quadraticCurveTo(L.x+L.len/2, L.y+L.sag, L.x+L.len, L.y);
+ ctx.stroke();
+ for(const p of L.pcs){
+ const px=L.x+L.len*p.t;
+ const py=L.y+L.sag*(1-Math.pow(2*p.t-1,2))*0.9;
+ const sway=state.reduced?0:Math.sin(clock*1.4+px*0.05)*3;
+ ctx.save(); ctx.translate(px,py); ctx.rotate(sway*0.03);
+ ctx.fillStyle=p.c; ctx.globalAlpha=0.85;
+ roundRect(-p.w/2,0,p.w,p.h,4); ctx.fill();
+ ctx.restore();
+ }
+ }
+ ctx.restore();
+}
+
+function drawRoofs(pal,cp){
+ for(const r of roofs){
+ const top=mix(pal.mid,pal.low,0.45);
+ ctx.fillStyle=mix('#150c30',top,0.5);
+ roundRect(r.x,r.y,r.w,r.h,10); ctx.fill();
+ // rooftop deck highlight
+ ctx.fillStyle=mix(pal.glow,pal.mid,0.7); ctx.globalAlpha=0.10+cp.floor*0.12;
+ roundRect(r.x+8,r.y+6,r.w-16,26,8); ctx.fill(); ctx.globalAlpha=1;
+ // parapet
+ ctx.strokeStyle='rgba(255,220,180,.18)'; ctx.lineWidth=2;
+ roundRect(r.x,r.y,r.w,r.h,10); ctx.stroke();
+ }
+ // bird's-eye floor grid on central roof
+ if(cp.floor){
+ ctx.save(); ctx.strokeStyle='rgba(255,210,170,.10)'; ctx.lineWidth=1;
+ const r=roofs[1];
+ for(let gx=r.x;gx<=r.x+r.w;gx+=40){ctx.beginPath();ctx.moveTo(gx,r.y);ctx.lineTo(gx,r.y+r.h);ctx.stroke();}
+ for(let gy=r.y;gy<=r.y+r.h;gy+=40){ctx.beginPath();ctx.moveTo(r.x,gy);ctx.lineTo(r.x+r.w,gy);ctx.stroke();}
+ ctx.restore();
+ }
+}
+
+/* lanterns + moths */
+const lanterns=[{x:720,y:560},{x:1180,y:560},{x:420,y:620},{x:1350,y:640},{x:960,y:470}];
+function drawLanterns(pal){
+ for(const L of lanterns){
+ const p=spr(L.x,L.y,camParams());
+ const pulse=state.reduced?1:(0.75+0.25*Math.sin(clock*2+L.x));
+ const gr=ctx.createRadialGradient(p.x,p.y,2,p.x,p.y,46);
+ gr.addColorStop(0,'rgba(255,224,150,'+(0.9*pulse)+')');
+ gr.addColorStop(1,'rgba(255,200,120,0)');
+ ctx.fillStyle=gr; ctx.beginPath(); ctx.arc(p.x,p.y,46,0,7); ctx.fill();
+ ctx.fillStyle='#ffe6a0'; ctx.beginPath(); ctx.arc(p.x,p.y,5,0,7); ctx.fill();
+ ctx.strokeStyle='rgba(255,255,255,.3)';ctx.lineWidth=1;
+ ctx.beginPath();ctx.moveTo(p.x,p.y-12);ctx.lineTo(p.x,p.y-30);ctx.stroke();
+ }
+}
+function drawMoths(pal,cp){
+ ctx.save();
+ for(let li=0;li<lanterns.length;li++){
+ const L=lanterns[li]; const c=spr(L.x,L.y,cp);
+ const n=3;
+ for(let m=0;m<n;m++){
+ const t=clock*1.6 + m*2.1 + li;
+ const rad=14+8*Math.sin(t*1.3+m);
+ const mx=c.x+Math.cos(t+m*2)*rad;
+ const my=c.y-8+Math.sin(t*1.4+m*2)*rad*0.7;
+ ctx.fillStyle='rgba(255,240,200,.9)';
+ const flut=state.reduced?3:(2.5+1.6*Math.abs(Math.sin(t*9)));
+ ctx.beginPath();ctx.ellipse(mx-2,my,flut,2.2,0.5,0,7);ctx.fill();
+ ctx.beginPath();ctx.ellipse(mx+2,my,flut,2.2,-0.5,0,7);ctx.fill();
+ if(li===0&&m===0&&state.names) label(mx,my-8,'moths','#ffe6a0');
+ }
+ }
+ ctx.restore();
+}
+
+/* ---------- the rose light sculpture ---------- */
+function drawRose(pal,cp){
+ const c=spr(ROSE.x,ROSE.y,cp);
+ const bloom = pal.idx===2 ? (0.6+0.4*pal.frac) : 0.35+pal.frac*0.35;
+ const rot = state.reduced?0:clock*0.35;
+ ctx.save(); ctx.translate(c.x,c.y-6);
+ // glow base
+ const gr=ctx.createRadialGradient(0,0,4,0,0,120);
+ gr.addColorStop(0,'rgba(255,150,190,'+(0.5*bloom)+')');
+ gr.addColorStop(0.5,'rgba(200,120,255,'+(0.28*bloom)+')');
+ gr.addColorStop(1,'rgba(120,80,200,0)');
+ ctx.fillStyle=gr; ctx.beginPath(); ctx.arc(0,0,120,0,7); ctx.fill();
+ // layered petals (rose curves)
+ const layers=5;
+ for(let L=0;L<layers;L++){
+ const k = 3 + L; // petal count varies per layer
+ const R = (18+L*13)*(0.85+0.25*bloom);
+ const pulse = state.reduced?1:(0.92+0.08*Math.sin(clock*2 - L));
+ ctx.save(); ctx.rotate(rot*(L%2?-1:1)*(0.5+L*0.12));
+ ctx.beginPath();
+ for(let a=0;a<=Math.PI*2+0.05;a+=0.05){
+ const r=R*Math.abs(Math.cos(k*a))*pulse + R*0.28;
+ const x=Math.cos(a)*r, y=Math.sin(a)*r;
+ a===0?ctx.moveTo(x,y):ctx.lineTo(x,y);
+ }
+ ctx.closePath();
+ ctx.fillStyle = 'rgba('+[255,120+L*20,180-L*10]+','+(0.14+0.06*bloom)+')';
+ ctx.strokeStyle='rgba(255,190,225,'+(0.35+0.12*bloom)+')'; ctx.lineWidth=1.4;
+ ctx.fill(); ctx.stroke();
+ ctx.restore();
+ }
+ // core
+ ctx.fillStyle='#fff3fb'; ctx.beginPath(); ctx.arc(0,0,6*(0.9+bloom*0.4),0,7); ctx.fill();
+ ctx.restore();
+}
+
+/* ---------- choreography groups ---------- */
+function drawCircle(pal,cp){
+ const c=spr(ROSE.x,ROSE.y,cp);
+ const count=Math.round((7+pal.complexity*7)* (0.4+state.density*0.9));
+ const spin=state.reduced?0.6:clock*0.5;
+ const R=92*cp.spread;
+ for(let i=0;i<count;i++){
+ const a=spin + i/count*Math.PI*2;
+ const rr=R*(1+0.05*Math.sin(clock*1.5+i));
+ const x=c.x+Math.cos(a)*rr, y=c.y-6+Math.sin(a)*rr*0.55*cp.squash;
+ const s=(0.9+0.2*Math.sin(a))*cp.figScale;
+ const col=mix(pal.glow,'#ffd1e6',(i%2)?0.2:0.7);
+ drawDancer(x,y,s,clock*3+i, col, 0.9);
+ }
+}
+
+function drawCouples(pal,cp){
+ // couples crossing narrow bridges in mirrored steps
+ const bridges=[
+ {a:spr(roofs[0].x+roofs[0].w,660,cp), b:spr(roofs[1].x,650,cp)},
+ {a:spr(roofs[1].x+roofs[1].w,660,cp), b:spr(roofs[2].x,690,cp)}
+ ];
+ for(let bi=0;bi<bridges.length;bi++){
+ const B=bridges[bi];
+ // bridge plank
+ ctx.strokeStyle='rgba(255,220,180,.35)'; ctx.lineWidth=6*cp.figScale;
+ ctx.beginPath(); ctx.moveTo(B.a.x,B.a.y); ctx.lineTo(B.b.x,B.b.y); ctx.stroke();
+ ctx.strokeStyle='rgba(255,220,180,.15)'; ctx.lineWidth=1;
+ for(let g=0;g<=6;g++){const t=g/6;ctx.beginPath();ctx.moveTo(lerp(B.a.x,B.b.x,t),lerp(B.a.y,B.b.y,t)-14);ctx.lineTo(lerp(B.a.x,B.b.x,t),lerp(B.a.y,B.b.y,t));ctx.stroke();}
+ const pairs=Math.max(1,Math.round((1+pal.complexity*1.5)*(0.5+state.density)));
+ for(let p=0;p<pairs;p++){
+ const base=(clock*0.12 + p/pairs + bi*0.3);
+ const t=(Math.sin(base*Math.PI*2)*0.5+0.5); // shuttle back & forth mirrored
+ const x=lerp(B.a.x,B.b.x,t), y=lerp(B.a.y,B.b.y,t)-8;
+ const ph=clock*2.4+p;
+ // mirrored partners
+ const dx=Math.cos(Math.atan2(B.b.y-B.a.y,B.b.x-B.a.x)+Math.PI/2)*10*cp.figScale;
+ const dy=Math.sin(Math.atan2(B.b.y-B.a.y,B.b.x-B.a.x)+Math.PI/2)*6*cp.figScale;
+ drawDancer(x+dx,y+dy,0.95*cp.figScale,ph,'#9be7e0',0.7);
+ drawDancer(x-dx,y-dy,0.95*cp.figScale,-ph,'#fff0d0',0.7);
+ }
+ }
+}
+
+function drawFreestyle(pal,cp){
+ const count=Math.round((3+pal.complexity*5)*(0.4+state.density));
+ for(let i=0;i<count;i++){
+ while(freeTrails.length<=i) freeTrails.push([]);
+ const base=towers[i%towers.length];
+ const bp=spr(base.x,base.y+70,cp);
+ const t=clock*(0.8+0.2*(i%3));
+ const x=bp.x+Math.cos(t*1.3+i)*70*cp.spread + Math.sin(t*0.7+i)*24;
+ const y=bp.y+Math.sin(t*1.9+i)*34*cp.squash + Math.cos(t*0.5)*10;
+ // trail
+ const tr=freeTrails[i];
+ if(!state.reduced){ tr.push({x,y}); if(tr.length>MAXTRAIL) tr.shift(); }
+ if(state.trails && tr.length>1){
+ ctx.save(); ctx.lineCap='round';
+ for(let k=1;k<tr.length;k++){
+ ctx.globalAlpha=(k/tr.length)*0.5;
+ ctx.strokeStyle=mix('#b48bff','#7ff0e6',(i%2)?0.2:0.8);
+ ctx.lineWidth=3*(k/tr.length)*cp.figScale;
+ ctx.beginPath();ctx.moveTo(tr[k-1].x,tr[k-1].y);ctx.lineTo(tr[k].x,tr[k].y);ctx.stroke();
+ }
+ ctx.restore();
+ }
+ drawDancer(x,y,0.95*cp.figScale,t*3+i,mix('#d9c6ff','#9be7e0',(i%2)?0.3:0.7),1);
+ // water tower
+ const wt=spr(base.x,base.y,cp);
+ ctx.save();ctx.fillStyle=mix(pal.low,'#2a1c48',0.4);ctx.strokeStyle='rgba(255,220,180,.25)';ctx.lineWidth=2;
+ roundRect(wt.x-24,wt.y-10,48,52,6);ctx.fill();ctx.stroke();
+ ctx.beginPath();ctx.moveTo(wt.x-26,wt.y-10);ctx.lineTo(wt.x,wt.y-34);ctx.lineTo(wt.x+26,wt.y-10);ctx.closePath();
+ ctx.fillStyle=mix(pal.mid,pal.low,0.4);ctx.fill();ctx.stroke();
+ ctx.strokeStyle='rgba(255,220,180,.2)';ctx.beginPath();ctx.moveTo(wt.x-18,wt.y+42);ctx.lineTo(wt.x-14,wt.y+64);ctx.moveTo(wt.x+18,wt.y+42);ctx.lineTo(wt.x+14,wt.y+64);ctx.stroke();
+ ctx.restore();
+ }
+}
+
+function drawProcession(pal,cp){
+ // quiet procession climbing fire escapes into the celebration
+ const bx=fireEscapeX;
+ // fire escape structure
+ const top=spr(bx,560,cp), bot=spr(bx,860,cp);
+ ctx.save();
+ ctx.strokeStyle='rgba(255,220,180,.3)';ctx.lineWidth=2;
+ for(let f=0;f<5;f++){
+ const y=lerp(bot.y,top.y,f/5);
+ ctx.beginPath();ctx.moveTo(bx-34,y);ctx.lineTo(bx+18,y);ctx.stroke(); // platform
+ ctx.beginPath();ctx.moveTo(bx-34,y);ctx.lineTo(bx-34,y-((bot.y-top.y)/5));ctx.stroke();
+ // zigzag stair
+ ctx.beginPath();ctx.moveTo(bx+18,y);ctx.lineTo(bx-34,y-((bot.y-top.y)/5));ctx.stroke();
+ }
+ ctx.restore();
+ const count=Math.round((4+pal.complexity*4)*(0.4+state.density*0.8));
+ for(let i=0;i<count;i++){
+ const prog=((clock*0.05 + i/count)%1);
+ const y=lerp(bot.y,top.y,prog);
+ const zig=(Math.floor(prog*5)%2)? 12 : -20;
+ const x=bx+zig+Math.sin(prog*30)*6;
+ const s=0.8*cp.figScale;
+ ctx.globalAlpha=0.55+prog*0.45;
+ drawDancer(x,y,s,clock*1.2+i,mix('#ffd98a',pal.glow,0.5),0.35);
+ ctx.globalAlpha=1;
+ }
+ if(state.routes){ label(top.x, top.y-30,'ascending procession','#ffd98a'); }
+}
+
+/* ---------- animals ---------- */
+function drawBear(x,y,s,ph){
+ const sway=state.reduced?0:Math.sin(ph)*0.14;
+ const squash=1+Math.abs(Math.sin(ph))*0.05;
+ ctx.save();ctx.translate(x,y);ctx.rotate(sway);ctx.scale(1, squash);
+ ctx.fillStyle='#5a3b2e';
+ roundRect(-16*s,-30*s,32*s,32*s,12*s);ctx.fill(); // body
+ ctx.beginPath();ctx.arc(0,-34*s,11*s,0,7);ctx.fill(); // head
+ ctx.fillStyle='#4a2f24';
+ ctx.beginPath();ctx.arc(-8*s,-42*s,4*s,0,7);ctx.arc(8*s,-42*s,4*s,0,7);ctx.fill(); //ears
+ ctx.fillStyle='#c9a06b';ctx.beginPath();ctx.arc(0,-31*s,4*s,0,7);ctx.fill(); //muzzle
+ ctx.restore();
+}
+function drawCat(x,y,s,leap){
+ ctx.save();ctx.translate(x,y);
+ ctx.fillStyle='#3a3350';
+ ctx.beginPath();ctx.ellipse(0,0,12*s,6*s,0,0,7);ctx.fill(); // body
+ ctx.beginPath();ctx.arc(9*s,-4*s,4*s,0,7);ctx.fill(); // head
+ ctx.beginPath();ctx.moveTo(11*s,-7*s);ctx.lineTo(13*s,-12*s);ctx.lineTo(8*s,-8*s);ctx.fill(); //ear
+ ctx.strokeStyle='#3a3350';ctx.lineWidth=2*s;ctx.lineCap='round';
+ ctx.beginPath();ctx.moveTo(-11*s,0);ctx.quadraticCurveTo(-20*s,-6*s-leap*4,-16*s,-12*s);ctx.stroke();//tail
+ ctx.restore();
+}
+function drawDog(x,y,s){
+ ctx.save();ctx.translate(x,y);
+ ctx.fillStyle='#b5793f';
+ ctx.beginPath();ctx.ellipse(0,0,13*s,7*s,0,0,7);ctx.fill();
+ ctx.beginPath();ctx.arc(11*s,-3*s,5*s,0,7);ctx.fill();
+ ctx.beginPath();ctx.moveTo(9*s,-6*s);ctx.lineTo(8*s,-13*s);ctx.lineTo(13*s,-7*s);ctx.fill();//ear
+ ctx.strokeStyle='#b5793f';ctx.lineWidth=2*s;ctx.lineCap='round';
+ const wag=state.reduced?0:Math.sin(clock*8)*0.4;
+ ctx.beginPath();ctx.moveTo(-12*s,-1*s);ctx.lineTo(-19*s,-6*s+wag*8);ctx.stroke();
+ ctx.restore();
+}
+
+function drawAnimals(pal,cp){
+ // bears sway at roof edges
+ const bears=[spr(roofs[0].x+70,700,cp), spr(roofs[2].x+220,760,cp)];
+ bears.forEach((b,i)=>{ drawBear(b.x,b.y,0.9*cp.figScale, clock*1.1+i); if(state.names) label(b.x,b.y-58,'bear','#c9a06b'); });
+
+ // cats leap between ledges
+ const ledges=[spr(roofs[1].x+40,570,cp), spr(roofs[1].x+300,555,cp), spr(roofs[2].x+40,660,cp)];
+ for(let i=0;i<2;i++){
+ const seg=(clock*0.25+i*0.5)% ledges.length;
+ const a=ledges[Math.floor(seg)], b=ledges[(Math.floor(seg)+1)%ledges.length];
+ const t=state.reduced?0.5:(seg-Math.floor(seg));
+ const x=lerp(a.x,b.x,t), arc=Math.sin(t*Math.PI)*40;
+ const y=lerp(a.y,b.y,t)-arc;
+ drawCat(x,y,1.0*cp.figScale, state.reduced?0:Math.sin(t*Math.PI));
+ if(state.names && i===0) label(x,y-16,'cat','#cfc7e6');
+ }
+
+ // dogs weave between circle dancers
+ const c=spr(ROSE.x,ROSE.y,cp);
+ for(let i=0;i<2;i++){
+ const t=clock*0.9+i*3;
+ const x=c.x+Math.cos(t*0.6)*150*cp.spread;
+ const y=c.y+70 + Math.sin(t*3)*16*cp.squash;
+ drawDog(x,y,1.0*cp.figScale);
+ if(state.names && i===0) label(x,y-16,'dog','#e6b98a');
+ }
+
+ // pigeons rise in coordinated spiral
+ const pc=Math.round(10+pal.complexity*8);
+ const origin=spr(880,700,cp);
+ for(let i=0;i<pc;i++){
+ const t=(clock*0.6 + i*0.12);
+ const rise=(t%3)/3; // 0..1 loop
+ const ang=i*0.5 + t*2;
+ const rad=(1-rise)*70 + 8;
+ const x=origin.x+Math.cos(ang)*rad*cp.spread;
+ const y=origin.y-rise*260*cp.squash + Math.sin(ang)*rad*0.4;
+ const flap=state.reduced?0.5:Math.abs(Math.sin(clock*10+i));
+ ctx.save();ctx.translate(x,y);
+ ctx.strokeStyle=mix('#d9d2e8',pal.glow,0.3);ctx.lineWidth=2;ctx.lineCap='round';
+ ctx.beginPath();
+ ctx.moveTo(-7,0);ctx.quadraticCurveTo(0,-6*flap-2,0,0);ctx.quadraticCurveTo(0,-6*flap-2,7,0);
+ ctx.stroke();ctx.restore();
+ }
+ if(state.names) label(origin.x, origin.y-250*cp.squash-8,'pigeons','#e8e2f2');
+}
+
+/* ---------- route diagrams overlay ---------- */
+function drawRoutes(pal,cp){
+ ctx.save();
+ ctx.setLineDash([7,7]); ctx.lineWidth=2;
+ ctx.strokeStyle='rgba(127,240,230,.85)';
+ // circle route
+ const c=spr(ROSE.x,ROSE.y,cp);
+ ctx.beginPath();ctx.ellipse(c.x,c.y-6,92*cp.spread,92*cp.spread*0.55*cp.squash,0,0,7);ctx.stroke();
+ label(c.x,c.y-92*cp.spread*0.55-6,'rose circle','#7ff0e6');
+ // bridges
+ ctx.strokeStyle='rgba(155,231,224,.85)';
+ const b1a=spr(roofs[0].x+roofs[0].w,660,cp),b1b=spr(roofs[1].x,650,cp);
+ const b2a=spr(roofs[1].x+roofs[1].w,660,cp),b2b=spr(roofs[2].x,690,cp);
+ ctx.beginPath();ctx.moveTo(b1a.x,b1a.y);ctx.lineTo(b1b.x,b1b.y);ctx.moveTo(b2a.x,b2a.y);ctx.lineTo(b2b.x,b2b.y);ctx.stroke();
+ label((b1a.x+b1b.x)/2,(b1a.y+b1b.y)/2-12,'mirrored crossing','#9be7e0');
+ // freestyle loops
+ ctx.strokeStyle='rgba(180,139,255,.85)';
+ towers.forEach(tw=>{const p=spr(tw.x,tw.y+70,cp);ctx.beginPath();ctx.ellipse(p.x,p.y,70*cp.spread,40*cp.squash,0.3,0,7);ctx.stroke();});
+ label(spr(towers[1].x,towers[1].y+70,cp).x, spr(towers[1].x,towers[1].y+70,cp).y-44,'freestyle curves','#b48bff');
+ ctx.restore();
+}
+
+/* ---------- main render ---------- */
+function render(){
+ const pal=scenePal();
+ const cp=camParams();
+ document.getElementById('sceneFlag').textContent = pal.name;
+
+ ctx.setTransform(DPR,0,0,DPR,0,0);
+ ctx.clearRect(0,0,W,H);
+
+ // camera transform
+ ctx.save();
+ ctx.translate(W/2, H/2);
+ const idle=state.reduced?0:Math.sin(clock*0.08)*8;
+ ctx.scale(viewScale*cp.zoom, viewScale*cp.zoom*cp.squash);
+ ctx.translate(-STAGE_W/2 + cp.panX + idle, -STAGE_H/2 + cp.panY);
+
+ drawSky(pal);
+ drawClouds(pal);
+ drawBuildings(pal,cp);
+ drawLaundry(pal);
+ drawRoofs(pal,cp);
+
+ drawLanterns(pal);
+ drawRose(pal,cp);
+
+ if(state.routes) drawRoutes(pal,cp);
+
+ // choreography (respect isolation toggles)
+ if(state.show.procession) drawProcession(pal,cp);
+ if(state.show.couples) drawCouples(pal,cp);
+ if(state.show.circle) drawCircle(pal,cp);
+ if(state.show.freestyle) drawFreestyle(pal,cp);
+ if(state.show.animals){ drawAnimals(pal,cp); drawMoths(pal,cp); }
+
+ ctx.restore();
+}
+
+/* ---------- controls wiring ---------- */
+function press(sel,val,attr='data-cam'){
+ document.querySelectorAll('['+attr+']').forEach(b=>{
+ const on=b.getAttribute(attr)===val;
+ b.setAttribute('aria-pressed', on?'true':'false');
+ b.classList.toggle('active',on);
+ });
+}
+
+document.querySelectorAll('[data-cam]').forEach(b=>{
+ b.addEventListener('click',()=>{ state.camera=b.dataset.cam; press('','','data-cam'); document.querySelectorAll('[data-cam]').forEach(x=>{const on=x.dataset.cam===state.camera;x.setAttribute('aria-pressed',on);x.classList.toggle('active',on);}); if(state.reduced)render(); });
+});
+function setCam(c){state.camera=c;document.querySelectorAll('[data-cam]').forEach(x=>{const on=x.dataset.cam===c;x.setAttribute('aria-pressed',on);x.classList.toggle('active',on);});if(state.reduced)render();}
+
+document.querySelectorAll('[data-scene]').forEach(b=>{
+ b.addEventListener('click',()=>{
+ const v=b.dataset.scene;
+ document.querySelectorAll('[data-scene]').forEach(x=>{const on=x===b;x.setAttribute('aria-pressed',on);x.classList.toggle('active',on);});
+ if(v==='auto'){ state.sceneMode='auto'; }
+ else { state.sceneMode='pinned'; state.scenePhase=parseInt(v,10); }
+ if(state.reduced)render();
+ });
+});
+
+const tempo=document.getElementById('tempo'), tempoOut=document.getElementById('tempoOut');
+tempo.addEventListener('input',()=>{state.tempo=parseFloat(tempo.value);tempoOut.textContent=state.tempo.toFixed(1)+'×';});
+const density=document.getElementById('density'), densityOut=document.getElementById('densityOut');
+density.addEventListener('input',()=>{state.density=parseFloat(density.value);densityOut.textContent=Math.round(state.density*100)+'%';if(state.reduced)render();});
+
+document.querySelectorAll('[data-grp]').forEach(c=>{
+ c.addEventListener('change',()=>{state.show[c.dataset.grp]=c.checked;if(state.reduced)render();});
+});
+
+const ovNames=document.getElementById('ovNames'),ovTrails=document.getElementById('ovTrails'),
+ ovRoutes=document.getElementById('ovRoutes'),ovReduced=document.getElementById('ovReduced');
+ovNames.addEventListener('change',()=>{state.names=ovNames.checked;if(state.reduced)render();});
+ovTrails.addEventListener('change',()=>{state.trails=ovTrails.checked;if(state.reduced)render();});
+ovRoutes.addEventListener('change',()=>{state.routes=ovRoutes.checked;if(state.reduced)render();});
+ovReduced.addEventListener('change',()=>setReduced(ovReduced.checked));
+
+const btnPause=document.getElementById('btnPause');
+btnPause.addEventListener('click',togglePause);
+function togglePause(){
+ state.paused=!state.paused;
+ btnPause.setAttribute('aria-pressed',state.paused);
+ btnPause.textContent=state.paused?'▶ Play':'⏸ Pause';
+}
+document.getElementById('btnRestart').addEventListener('click',()=>{
+ clock=0; sceneClock=0; state.scenePhase= state.sceneMode==='pinned'?state.scenePhase:0;
+ freeTrails.length=0;
+ if(state.reduced)render();
+});
+
+function setReduced(on){
+ state.reduced=on; ovReduced.checked=on;
+ if(on){ stopLoop(); render(); }
+ else { last=performance.now(); startLoop(); }
+}
+
+// panel collapse
+const panelToggle=document.getElementById('panelToggle'), ctlBody=document.getElementById('ctlBody'), panel=document.getElementById('controls');
+panelToggle.addEventListener('click',()=>{
+ const collapsed=panel.classList.toggle('collapsed');
+ panelToggle.textContent=collapsed?'Show':'Hide';
+ panelToggle.setAttribute('aria-expanded',(!collapsed).toString());
+});
+
+// keyboard
+window.addEventListener('keydown',e=>{
+ if(e.target.matches('input,textarea')) return;
+ switch(e.key){
+ case ' ': e.preventDefault(); togglePause(); break;
+ case '1': setCam('skyline'); break;
+ case '2': setCam('rooftop'); break;
+ case '3': setCam('birdseye'); break;
+ case 'r': case 'R': setReduced(!state.reduced); break;
+ case 'n': case 'N': ovNames.checked=!ovNames.checked; state.names=ovNames.checked; if(state.reduced)render(); break;
+ case 't': case 'T': ovTrails.checked=!ovTrails.checked; state.trails=ovTrails.checked; if(state.reduced)render(); break;
+ case 'd': case 'D': ovRoutes.checked=!ovRoutes.checked; state.routes=ovRoutes.checked; if(state.reduced)render(); break;
+ }
+});
+
+/* ---------- boot ---------- */
+resize();
+// honor OS reduced-motion preference on first load
+if(window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches){
+ setReduced(true);
+} else {
+ startLoop();
+}
+})();
+</script>
+</body>
+</html>
\ No newline at end of file
diff --git a/data/artifacts/127317180daf/qwen25-7b.html b/data/artifacts/127317180daf/qwen25-7b.html
new file mode 100644
index 0000000..c94fd6a
--- /dev/null
+++ b/data/artifacts/127317180daf/qwen25-7b.html
@@ -0,0 +1,176 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>Rose City Rooftop Solstice</title>
+ <style>
+ body { margin: 0; overflow: hidden; }
+ canvas { width: 100%; height: 100%; display: block; }
+ .controls { position: absolute; bottom: 10px; left: 10px; z-index: 2; }
+ button, input { margin: 5px; padding: 5px; font-size: 14px; }
+ </style>
+</head>
+<body>
+ <canvas id="scene"></canvas>
+ <div class="controls">
+ <label><input type="radio" name="camera" checked> Skyline</label>
+ <label><input type="radio" name="camera"> Rooftop</label>
+ <label><input type="radio" name="camera"> Bird's Eye</label>
+ <button id="toggleAnimals">Toggle Animals</button>
+ <button id="toggleRoutes">Toggle Routes</button>
+ <button id="toggleBuildings">Toggle Buildings</button>
+ <button id="pause">Pause</button>
+ <button id="restart">Restart</button>
+ <div id="settings"></div>
+ </div>
+
+ <script>
+ const canvas = document.getElementById('scene');
+ const ctx = canvas.getContext('2d');
+
+ class Animal {
+ constructor(x, y, type) {
+ this.x = x;
+ this.y = y;
+ this.type = type;
+ this.trail = [];
+ this.size = 15;
+ }
+ update() {
+ // Simple movement logic
+ }
+ draw() {
+ ctx.beginPath();
+ if (this.type === 'bear') {
+ ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
+ } else if (this.type === 'cat') {
+ ctx.rect(this.x - this.size / 3, this.y - this.size / 2, this.size, this.size / 2);
+ }
+ ctx.fillStyle = '#FF69B4';
+ ctx.fill();
+ }
+ }
+
+ class AnimalManager {
+ constructor() {
+ this.animals = [];
+ }
+ addAnimal(x, y, type) {
+ this.animals.push(new Animal(x, y, type));
+ }
+ update() {
+ for (let animal of this.animals) {
+ animal.update();
+ }
+ }
+ draw() {
+ for (let animal of this.animals) {
+ animal.draw();
+ }
+ }
+ }
+
+ const animals = new AnimalManager();
+
+ function createAnimals() {
+ // Randomly add some animals
+ for (let i = 0; i < 20; i++) {
+ let x = Math.random() * canvas.width;
+ let y = Math.random() * canvas.height;
+ let type = ['bear', 'cat'][Math.floor(Math.random() * 2)];
+ animals.addAnimal(x, y, type);
+ }
+ }
+
+ function drawBackground() {
+ ctx.fillStyle = '#FFD700';
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
+ }
+
+ function drawBuildings() {
+ // Simple parallax buildings
+ for (let i = -5; i < 2; i++) {
+ let x = i * canvas.width / 3;
+ let y = Math.random() * canvas.height;
+ ctx.fillStyle = `hsl(${(i + 1) * 60}, 75%, 45%)`;
+ ctx.fillRect(x, y - 150, canvas.width / 2, 150);
+ }
+ }
+
+ function drawRooftops() {
+ // Draw the rooftops and people
+ for (let i = 0; i < 10; i++) {
+ let x = Math.random() * canvas.width;
+ let y = canvas.height - 200 + (Math.sin(Date.now() / 500) * 40);
+ ctx.fillStyle = '#C71585';
+ ctx.fillRect(x, y, 30, 30);
+ }
+ }
+
+ function drawTrails() {
+ for (let animal of animals.animals) {
+ if (animal.trail.length > 0) {
+ ctx.strokeStyle = 'rgba(255, 165, 0, 0.7)';
+ ctx.beginPath();
+ ctx.moveTo(animal.trail[0].x, animal.trail[0].y);
+ for (let i = 1; i < animal.trail.length; i++) {
+ ctx.lineTo(animal.trail[i].x, animal.trail[i].y);
+ }
+ ctx.stroke();
+ }
+ }
+ }
+
+ function update() {
+ drawBackground();
+ drawRooftops();
+ animals.update();
+ drawTrails();
+ requestAnimationFrame(update);
+ }
+
+ function toggleAnimals() {
+ if (document.querySelector('input[name="animals"]:checked').value === 'true') {
+ document.querySelectorAll('input[type=radio][name=camera]').forEach(r => r.checked = false);
+ update();
+ } else {
+ update();
+ }
+ }
+
+ function toggleRoutes() {
+ document.querySelectorAll('input[type=radio][name=camera]').forEach(r => r.checked = false);
+ if (document.querySelector('input[name="camera"]:checked').value === 'Rooftop') {
+ update();
+ }
+ }
+
+ function toggleBuildings() {
+ drawBuildings();
+ requestAnimationFrame(update);
+ }
+
+ document.getElementById('toggleAnimals').addEventListener('click', toggleAnimals);
+ document.getElementById('toggleRoutes').addEventListener('click', toggleRoutes);
+ document.getElementById('toggleBuildings').addEventListener('click', toggleBuildings);
+
+ createAnimals();
+ update();
+
+ document.getElementById('pause').addEventListener('click', () => {
+ let pause = !document.getElementById('pause').innerText.includes('Play');
+ if (pause) {
+ document.getElementById('pause').innerText = 'Play';
+ } else {
+ document.getElementById('pause').innerText = 'Pause';
+ }
+ });
+
+ document.getElementById('restart').addEventListener('click', () => {
+ createAnimals();
+ update();
+ });
+ </script>
+</body>
+</html>
\ No newline at end of file
diff --git a/data/artifacts/127317180daf/qwen3-14b.html b/data/artifacts/127317180daf/qwen3-14b.html
new file mode 100644
index 0000000..268013d
--- /dev/null
+++ b/data/artifacts/127317180daf/qwen3-14b.html
@@ -0,0 +1,524 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <title>Rose City Rooftop Solstice</title>
+ <style>
+ html, body {
+ margin: 0;
+ padding: 0;
+ overflow: hidden;
+ background: #000;
+ font-family: 'Arial', sans-serif;
+ }
+ canvas {
+ display: block;
+ }
+ #controls {
+ position: absolute;
+ top: 10px;
+ left: 10px;
+ z-index: 10;
+ background: rgba(255,255,255,0.1);
+ padding: 10px;
+ border-radius: 10px;
+ color: #fff;
+ font-size: 14px;
+ }
+ #controls button, #controls select, #controls input {
+ margin: 5px 0;
+ padding: 6px 10px;
+ background: rgba(255,255,255,0.2);
+ border: 1px solid rgba(255,255,255,0.4);
+ color: #fff;
+ border-radius: 5px;
+ cursor: pointer;
+ transition: background 0.2s ease;
+ }
+ #controls button:hover, #controls select:hover, #controls input:hover {
+ background: rgba(255,255,255,0.3);
+ }
+ #controls label {
+ display: block;
+ margin: 5px 0 2px;
+ }
+ #info {
+ position: absolute;
+ bottom: 10px;
+ left: 10px;
+ z-index: 10;
+ color: #fff;
+ font-size: 12px;
+ text-align: left;
+ }
+ </style>
+</head>
+<body>
+ <canvas id="canvas"></canvas>
+ <div id="controls">
+ <label>Camera:</label>
+ <select id="camera">
+ <option value="skyline">Skyline</option>
+ <option value="rooftop">Rooftop</option>
+ <option value="birds-eye">Bird's-Eye</option>
+ </select>
+ <label>Scene:</label>
+ <select id="scene">
+ <option value="arrival">Arrival</option>
+ <option value="long-light">Long Light</option>
+ <option value="midnight-bloom">Midnight Bloom</option>
+ </select>
+ <label>Tempo:</label>
+ <input type="range" id="tempo" min="0.5" max="2" step="0.1" value="1">
+ <label>Density:</label>
+ <input type="range" id="density" min="0.3" max="1" step="0.1" value="0.7">
+ <label>Groups:</label>
+ <input type="checkbox" id="group1"> Central Circle
+ <input type="checkbox" id="group2"> Bridges
+ <input type="checkbox" id="group3"> Water Towers
+ <input type="checkbox" id="group4"> Fire Escapes
+ <label>Animals:</label>
+ <input type="checkbox" id="animalNames"> Show Names
+ <input type="checkbox" id="animalTrails"> Show Trails
+ <input type="checkbox" id="routeDiagrams"> Show Routes
+ <br>
+ <button id="pause">Pause</button>
+ <button id="restart">Restart</button>
+ <button id="reduceMotion">Reduce Motion</button>
+ </div>
+ <div id="info">
+ Visual rhythm is authored rather than listening to audio.
+ <br>
+ Keyboard controls:
+ <br>
+ Arrow keys: Move camera
+ <br>
+ W/S: Zoom
+ <br>
+ R: Reset
+ </div>
+ <script>
+ const canvas = document.getElementById('canvas');
+ const ctx = canvas.getContext('2d');
+ const width = canvas.width = window.innerWidth;
+ const height = canvas.height = window.innerHeight;
+
+ const controlElements = {
+ camera: document.getElementById('camera'),
+ scene: document.getElementById('scene'),
+ tempo: document.getElementById('tempo'),
+ density: document.getElementById('density'),
+ group1: document.getElementById('group1'),
+ group2: document.getElementById('group2'),
+ group3: document.getElementById('group3'),
+ group4: document.getElementById('group4'),
+ animalNames: document.getElementById('animalNames'),
+ animalTrails: document.getElementById('animalTrails'),
+ routeDiagrams: document.getElementById('routeDiagrams'),
+ pause: document.getElementById('pause'),
+ restart: document.getElementById('restart'),
+ reduceMotion: document.getElementById('reduceMotion'),
+ };
+
+ let state = {
+ scene: 'arrival',
+ camera: 'skyline',
+ tempo: 1,
+ density: 0.7,
+ groups: {1: true, 2: true, 3: true, 4: true},
+ showNames: false,
+ showTrails: false,
+ showRoutes: false,
+ isPaused: false,
+ reduceMotion: false,
+ time: 0,
+ frame: 0,
+ lastTime: 0,
+ };
+
+ const sceneColors = {
+ arrival: {bg: '#111', rose: '#ff6b6b', light: '#f0c14b', cloud: '#60a5fa'},
+ 'long-light': {bg: '#0a0a0a', rose: '#ff4081', light: '#ffd700', cloud: '#3b82f6'},
+ 'midnight-bloom': {bg: '#000', rose: '#e63946', light: '#e9c46a', cloud: '#2a2240'},
+ };
+
+ const entities = {
+ dancers: [],
+ animals: [],
+ buildings: [],
+ waterTowers: [],
+ fireEscapes: [],
+ };
+
+ const initEntities = () => {
+ const numDancers = Math.floor(100 * state.density);
+ const numAnimals = Math.floor(20 * state.density);
+ const numBuildings = Math.floor(50 * state.density);
+
+ for (let i = 0; i < numDancers; i++) {
+ entities.dancers.push({
+ type: 'dancer',
+ x: Math.random() * width,
+ y: Math.random() * height,
+ angle: Math.random() * Math.PI * 2,
+ speed: (Math.random() + 0.5) * 0.01 * state.tempo,
+ group: Math.floor(Math.random() * 4) + 1,
+ });
+ }
+
+ for (let i = 0; i < numAnimals; i++) {
+ const types = ['bear', 'cat', 'dog', 'moth', 'pigeon'];
+ const type = types[Math.floor(Math.random() * types.length)];
+ entities.animals.push({
+ type,
+ x: Math.random() * width,
+ y: Math.random() * height,
+ angle: Math.random() * Math.PI * 2,
+ speed: (Math.random() + 0.5) * 0.01 * state.tempo,
+ });
+ }
+
+ for (let i = 0; i < numBuildings; i++) {
+ entities.buildings.push({
+ x: Math.random() * width,
+ y: Math.random() * height,
+ scale: 0.2 + Math.random() * 0.3,
+ });
+ }
+
+ for (let i = 0; i < 10; i++) {
+ entities.waterTowers.push({
+ x: width * 0.2 + Math.random() * width * 0.6,
+ y: height * 0.2 + Math.random() * height * 0.6,
+ });
+ }
+
+ for (let i = 0; i < 5; i++) {
+ entities.fireEscapes.push({
+ x: width * 0.2 + Math.random() * width * 0.6,
+ y: height * 0.2 + Math.random() * height * 0.6,
+ });
+ }
+ };
+
+ const draw = (time) => {
+ if (state.isPaused) return;
+ state.time = time;
+ state.frame++;
+ ctx.clearRect(0, 0, width, height);
+
+ const color = sceneColors[state.scene];
+ ctx.fillStyle = color.bg;
+ ctx.fillRect(0, 0, width, height);
+
+ // Draw buildings with parallax
+ ctx.save();
+ ctx.translate(width / 2, height / 2);
+ ctx.scale(1, 1);
+ ctx.fillStyle = color.bg;
+ ctx.fillRect(-width / 2, -height / 2, width, height);
+ ctx.restore();
+
+ // Draw clouds
+ ctx.fillStyle = color.cloud;
+ for (let i = 0; i < 10; i++) {
+ const cx = Math.random() * width;
+ const cy = Math.random() * height * 0.3;
+ const size = 10 + Math.random() * 20;
+ ctx.beginPath();
+ ctx.arc(cx, cy, size, 0, Math.PI * 2);
+ ctx.fill();
+ }
+
+ // Draw water towers
+ ctx.fillStyle = color.light;
+ for (let tower of entities.waterTowers) {
+ const size = 15 + Math.random() * 10;
+ const x = tower.x;
+ const y = tower.y;
+ ctx.beginPath();
+ ctx.arc(x, y, size, 0, Math.PI * 2);
+ ctx.fill();
+ }
+
+ // Draw fire escapes
+ ctx.strokeStyle = color.light;
+ ctx.lineWidth = 2;
+ for (let escape of entities.fireEscapes) {
+ const size = 10 + Math.random() * 10;
+ const x = escape.x;
+ const y = escape.y;
+ ctx.beginPath();
+ ctx.moveTo(x - size, y);
+ ctx.lineTo(x + size, y);
+ ctx.stroke();
+ }
+
+ // Draw dancers
+ for (let d of entities.dancers) {
+ if (!state.groups[d.group]) continue;
+
+ let x = d.x;
+ let y = d.y;
+ let angle = d.angle;
+ if (d.group === 1) {
+ // Central Circle
+ const centerX = width / 2;
+ const centerY = height / 2;
+ const radius = 100 + Math.sin(state.time * d.speed) * 50;
+ x = centerX + Math.cos(angle) * radius;
+ y = centerY + Math.sin(angle) * radius;
+ angle += state.time * d.speed;
+ d.angle = angle;
+ } else if (d.group === 2) {
+ // Bridges
+ const start = {x: 0, y: height / 2};
+ const end = {x: width, y: height / 2};
+ const t = (Math.sin(state.time * d.speed) + 1) / 2;
+ x = start.x + t * (end.x - start.x);
+ y = start.y + t * (end.y - start.y);
+ angle = Math.atan2(end.y - start.y, end.x - start.x);
+ } else if (d.group === 3) {
+ // Water Towers
+ const size = 50 + Math.sin(state.time * d.speed) * 20;
+ x = d.x + Math.cos(angle) * size;
+ y = d.y + Math.sin(angle) * size;
+ angle += state.time * d.speed;
+ } else if (d.group === 4) {
+ // Fire Escapes
+ const speed = 0.005 * state.tempo;
+ d.y -= speed;
+ if (d.y < 0) {
+ d.y = height;
+ d.x = Math.random() * width;
+ }
+ }
+
+ d.x = x;
+ d.y = y;
+ d.angle = angle;
+
+ // Draw dancer
+ ctx.save();
+ ctx.translate(x, y);
+ ctx.rotate(angle);
+ ctx.fillStyle = color.rose;
+ ctx.beginPath();
+ ctx.arc(0, 0, 4, 0, Math.PI * 2);
+ ctx.fill();
+ ctx.restore();
+ }
+
+ // Draw animals
+ for (let a of entities.animals) {
+ let x = a.x;
+ let y = a.y;
+ let angle = a.angle;
+ if (a.type === 'bear') {
+ x += Math.cos(angle) * 5;
+ y += Math.sin(angle) * 5;
+ } else if (a.type === 'cat') {
+ x += Math.cos(angle) * 3;
+ y += Math.sin(angle) * 3;
+ } else if (a.type === 'dog') {
+ x += Math.cos(angle) * 4;
+ y += Math.sin(angle) * 4;
+ } else if (a.type === 'moth') {
+ x += Math.cos(angle) * 2;
+ y += Math.sin(angle) * 2;
+ angle += state.time * 0.01;
+ } else if (a.type === 'pigeon') {
+ x += Math.cos(angle) * 5;
+ y += Math.sin(angle) * 5;
+ angle += state.time * 0.005;
+ }
+
+ a.x = x;
+ a.y = y;
+ a.angle = angle;
+
+ if (state.showTrails) {
+ ctx.strokeStyle = color.light;
+ ctx.lineWidth = 1;
+ ctx.beginPath();
+ ctx.moveTo(a.x, a.y);
+ ctx.lineTo(a.x + Math.cos(angle) * 5, a.y + Math.sin(angle) * 5);
+ ctx.stroke();
+ }
+
+ ctx.save();
+ ctx.translate(x, y);
+ ctx.rotate(angle);
+ ctx.fillStyle = color.light;
+ ctx.beginPath();
+ if (a.type === 'bear') {
+ ctx.fillRect(-4, -4, 8, 8);
+ } else if (a.type === 'cat') {
+ ctx.beginPath();
+ ctx.arc(0, 0, 4, 0, Math.PI * 2);
+ ctx.fill();
+ } else if (a.type === 'dog') {
+ ctx.beginPath();
+ ctx.arc(0, 0, 4, 0, Math.PI * 2);
+ ctx.fill();
+ ctx.fillRect(-2, -2, 4, 4);
+ } else if (a.type === 'moth') {
+ ctx.beginPath();
+ ctx.arc(0, 0, 3, 0, Math.PI * 2);
+ ctx.fill();
+ ctx.beginPath();
+ ctx.moveTo(-3, 0);
+ ctx.lineTo(0, 4);
+ ctx.lineTo(3, 0);
+ ctx.fill();
+ } else if (a.type === 'pigeon') {
+ ctx.beginPath();
+ ctx.arc(0, 0, 5, 0, Math.PI * 2);
+ ctx.fill();
+ ctx.beginPath();
+ ctx.moveTo(0, 0);
+ ctx.lineTo(0, -5);
+ ctx.stroke();
+ }
+ ctx.restore();
+
+ if (state.showNames) {
+ ctx.fillStyle = color.light;
+ ctx.font = '10px Arial';
+ ctx.fillText(a.type, x + 6, y - 6);
+ }
+ }
+
+ // Draw central rose sculpture
+ ctx.fillStyle = color.rose;
+ ctx.beginPath();
+ ctx.moveTo(width / 2, height / 2);
+ for (let i = 0; i < 12; i++) {
+ const angle = (Math.PI * 2 / 12) * i;
+ const r = 40 + 20 * Math.sin(state.time * 0.01 * i);
+ const x = width / 2 + Math.cos(angle) * r;
+ const y = height / 2 + Math.sin(angle) * r;
+ ctx.lineTo(x, y);
+ }
+ ctx.closePath();
+ ctx.fill();
+
+ // Draw light trails
+ if (state.showTrails) {
+ ctx.strokeStyle = color.light;
+ ctx.lineWidth = 1;
+ for (let d of entities.dancers) {
+ ctx.beginPath();
+ ctx.moveTo(d.x, d.y);
+ ctx.lineTo(d.x + Math.cos(d.angle) * 5, d.y + Math.sin(d.angle) * 5);
+ ctx.stroke();
+ }
+ }
+
+ // Draw route diagrams
+ if (state.showRoutes) {
+ ctx.strokeStyle = color.light;
+ ctx.lineWidth = 1;
+ ctx.globalAlpha = 0.3;
+ for (let i = 0; i < 10; i++) {
+ ctx.beginPath();
+ ctx.moveTo(0, height / 2);
+ ctx.lineTo(width, height / 2);
+ ctx.stroke();
+ }
+ ctx.globalAlpha = 1;
+ }
+
+ requestAnimationFrame(draw);
+ };
+
+ const updateState = () => {
+ state.scene = controlElements.scene.value;
+ state.camera = controlElements.camera.value;
+ state.tempo = parseFloat(controlElements.tempo.value);
+ state.density = parseFloat(controlElements.density.value);
+ state.groups = {
+ 1: controlElements.group1.checked,
+ 2: controlElements.group2.checked,
+ 3: controlElements.group3.checked,
+ 4: controlElements.group4.checked,
+ };
+ state.showNames = controlElements.animalNames.checked;
+ state.showTrails = controlElements.animalTrails.checked;
+ state.showRoutes = controlElements.routeDiagrams.checked;
+ state.reduceMotion = controlElements.reduceMotion.checked;
+ };
+
+ const bindControls = () => {
+ controlElements.camera.addEventListener('change', updateState);
+ controlElements.scene.addEventListener('change', updateState);
+ controlElements.tempo.addEventListener('input', updateState);
+ controlElements.density.addEventListener('input', updateState);
+ controlElements.group1.addEventListener('change', updateState);
+ controlElements.group2.addEventListener('change', updateState);
+ controlElements.group3.addEventListener('change', updateState);
+ controlElements.group4.addEventListener('change', updateState);
+ controlElements.animalNames.addEventListener('change', updateState);
+ controlElements.animalTrails.addEventListener('change', updateState);
+ controlElements.routeDiagrams.addEventListener('change', updateState);
+ controlElements.pause.addEventListener('click', () => {
+ state.isPaused = !state.isPaused;
+ controlElements.pause.textContent = state.isPaused ? 'Resume' : 'Pause';
+ });
+ controlElements.restart.addEventListener('click', () => {
+ state.time = 0;
+ state.frame = 0;
+ initEntities();
+ });
+ controlElements.reduceMotion.addEventListener('click', () => {
+ state.reduceMotion = !state.reduceMotion;
+ controlElements.reduceMotion.textContent = state.reduceMotion ? 'Normal Motion' : 'Reduce Motion';
+ });
+
+ document.addEventListener('keydown', (e) => {
+ if (e.key === 'ArrowUp') {
+ e.preventDefault();
+ state.camera = 'skyline';
+ controlElements.camera.value = 'skyline';
+ } else if (e.key === 'ArrowDown') {
+ e.preventDefault();
+ state.camera = 'rooftop';
+ controlElements.camera.value = 'rooftop';
+ } else if (e.key === 'ArrowLeft') {
+ e.preventDefault();
+ state.camera = 'birds-eye';
+ controlElements.camera.value = 'birds-eye';
+ } else if (e.key === 'ArrowRight') {
+ e.preventDefault();
+ state.scene = 'arrival';
+ controlElements.scene.value = 'arrival';
+ } else if (e.key === 'w') {
+ e.preventDefault();
+ state.density = Math.min(state.density + 0.1, 1);
+ controlElements.density.value = state.density;
+ } else if (e.key === 's') {
+ e.preventDefault();
+ state.density = Math.max(state.density - 0.1, 0.3);
+ controlElements.density.value = state.density;
+ } else if (e.key === 'r') {
+ e.preventDefault();
+ state.time = 0;
+ state.frame = 0;
+ initEntities();
+ }
+ });
+ };
+
+ window.addEventListener('resize', () => {
+ canvas.width = window.innerWidth;
+ canvas.height = window.innerHeight;
+ width = canvas.width;
+ height = canvas.height;
+ });
+
+ initEntities();
+ bindControls();
+ draw(0);
+ </script>
+</body>
+</html>
\ No newline at end of file
diff --git a/data/artifacts/77fc58739877/claude-code.html b/data/artifacts/77fc58739877/claude-code.html
new file mode 100644
index 0000000..216de33
--- /dev/null
+++ b/data/artifacts/77fc58739877/claude-code.html
@@ -0,0 +1,973 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="UTF-8">
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+<title>Indigo Train Through Dream Country — An Animated Narrative Installation</title>
+<style>
+ :root{
+ --bg:#070512;
+ --ink:#eae4ff;
+ --ink-dim:#a99fce;
+ --glass:rgba(20,14,42,0.72);
+ --glass-line:rgba(150,120,220,0.28);
+ --accent:#c9a6ff;
+ --focus:#ffd27a;
+ }
+ *{box-sizing:border-box}
+ html,body{margin:0;height:100%;background:var(--bg);color:var(--ink);
+ font-family:"Georgia","Iowan Old Style",serif;overflow:hidden}
+ #stage{position:fixed;inset:0}
+ canvas{display:block;width:100%;height:100%}
+
+ /* Title */
+ #titlebar{position:fixed;top:14px;left:18px;right:18px;pointer-events:none;
+ display:flex;justify-content:space-between;gap:12px;align-items:flex-start;z-index:5}
+ #titlebar h1{font-size:clamp(16px,2.4vw,26px);margin:0;letter-spacing:.06em;
+ text-shadow:0 2px 14px rgba(0,0,0,.7);font-weight:600}
+ #titlebar h1 span{display:block;font-size:.52em;letter-spacing:.28em;
+ color:var(--ink-dim);font-style:italic;text-transform:uppercase;margin-top:3px}
+ #disclaimer{pointer-events:auto;max-width:340px;font-family:system-ui,sans-serif;
+ font-size:11px;line-height:1.45;color:var(--ink-dim);
+ background:var(--glass);border:1px solid var(--glass-line);border-radius:10px;
+ padding:8px 11px;backdrop-filter:blur(8px)}
+ #disclaimer strong{color:var(--accent)}
+
+ /* Caption */
+ #caption{position:fixed;left:18px;bottom:18px;z-index:5;max-width:min(46ch,60vw);
+ background:var(--glass);border:1px solid var(--glass-line);border-radius:14px;
+ padding:14px 18px;backdrop-filter:blur(10px);box-shadow:0 10px 40px rgba(0,0,0,.5);
+ transition:opacity .5s}
+ #caption h2{margin:0 0 4px;font-size:19px;letter-spacing:.02em}
+ #caption .meta{font-family:system-ui,sans-serif;font-size:10.5px;text-transform:uppercase;
+ letter-spacing:.16em;color:var(--accent);margin-bottom:7px}
+ #caption p{margin:0;font-size:13.5px;line-height:1.55;color:var(--ink-dim);font-style:italic}
+
+ /* Control panel */
+ #panel{position:fixed;top:0;right:0;height:100%;width:326px;z-index:6;
+ background:linear-gradient(180deg,rgba(14,9,32,.94),rgba(10,7,24,.97));
+ border-left:1px solid var(--glass-line);backdrop-filter:blur(14px);
+ padding:16px 16px 26px;overflow-y:auto;font-family:system-ui,sans-serif;
+ transition:transform .35s ease}
+ #panel.hidden{transform:translateX(100%)}
+ #panel h3{font-family:"Georgia",serif;font-size:14px;letter-spacing:.14em;
+ text-transform:uppercase;color:var(--accent);margin:20px 0 10px;
+ border-bottom:1px solid var(--glass-line);padding-bottom:5px}
+ #panel h3:first-of-type{margin-top:6px}
+ .field{margin:11px 0}
+ .field label{display:block;font-size:11.5px;letter-spacing:.05em;color:var(--ink-dim);
+ margin-bottom:5px;text-transform:uppercase}
+ .field .val{color:var(--ink);font-variant-numeric:tabular-nums;float:right;font-weight:600}
+ input[type=range]{width:100%;accent-color:var(--accent);cursor:pointer;height:22px}
+ select{width:100%;padding:7px 8px;background:#160f30;color:var(--ink);
+ border:1px solid var(--glass-line);border-radius:8px;font-size:13px}
+ .cams{display:grid;grid-template-columns:1fr 1fr 1fr;gap:6px}
+ .cams button{padding:9px 4px;font-size:11px}
+ button{background:#20163f;color:var(--ink);border:1px solid var(--glass-line);
+ border-radius:8px;padding:8px 10px;font-size:12.5px;cursor:pointer;
+ transition:background .2s,border-color .2s}
+ button:hover{background:#2c1f57}
+ button[aria-pressed=true]{background:var(--accent);color:#160f30;border-color:var(--accent);font-weight:700}
+ .row{display:flex;gap:7px;flex-wrap:wrap}
+ .row button{flex:1}
+ .toggle{display:flex;align-items:center;justify-content:space-between;
+ gap:8px;padding:7px 0;font-size:12.5px;color:var(--ink-dim)}
+ .switch{position:relative;width:42px;height:23px;flex:0 0 auto}
+ .switch input{position:absolute;opacity:0;width:100%;height:100%;margin:0;cursor:pointer}
+ .switch .track{position:absolute;inset:0;background:#2a2050;border-radius:20px;
+ border:1px solid var(--glass-line);transition:background .2s}
+ .switch .knob{position:absolute;top:2px;left:2px;width:17px;height:17px;border-radius:50%;
+ background:var(--ink-dim);transition:transform .2s,background .2s}
+ .switch input:checked + .track{background:var(--accent)}
+ .switch input:checked + .track + .knob{transform:translateX(19px);background:#160f30}
+ #panelToggle{position:fixed;top:14px;right:14px;z-index:7;font-size:12px;
+ font-family:system-ui,sans-serif;padding:8px 12px}
+ .hint{font-size:10.5px;color:#7d7099;line-height:1.5;margin-top:8px}
+ kbd{background:#20163f;border:1px solid var(--glass-line);border-radius:4px;
+ padding:1px 5px;font-size:10.5px;font-family:ui-monospace,monospace}
+
+ /* Focus visibility (accessibility) */
+ a:focus-visible, button:focus-visible, select:focus-visible,
+ input:focus-visible{outline:3px solid var(--focus);outline-offset:2px;border-radius:6px}
+ .switch input:focus-visible + .track{outline:3px solid var(--focus);outline-offset:2px}
+
+ .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:640px){
+ #panel{width:290px}
+ #disclaimer{display:none}
+ #caption{max-width:70vw}
+ }
+</style>
+</head>
+<body>
+<div id="stage">
+ <canvas id="scene" role="img"
+ aria-label="Animated night train crossing five dream landscapes; travellers dance in a cutaway carriage while animals keep rhythm on the roof and alongside."></canvas>
+</div>
+
+<header id="titlebar">
+ <h1>Indigo Train Through Dream Country
+ <span>A gallery installation in motion</span></h1>
+ <div id="disclaimer">
+ <strong>Please note:</strong> every rhythm here is <strong>choreographed animation</strong> —
+ hand-timed dancer and animal motion. It is <em>not</em> generated from, synced to, or an
+ analysis of any music. All graphics are original psychedelic travel-poster artwork.
+ </div>
+</header>
+
+<figure id="caption" aria-live="polite">
+ <div class="meta" id="capMeta">Landscape 1 of 5</div>
+ <h2 id="capTitle">Desert Mesas</h2>
+ <p id="capText"></p>
+</figure>
+
+<button id="panelToggle" aria-controls="panel" aria-expanded="true">☰ Controls</button>
+
+<aside id="panel" aria-label="Installation controls">
+ <h3>Camera</h3>
+ <div class="cams" role="group" aria-label="Camera view">
+ <button data-cam="exterior" aria-pressed="true">Exterior<br>tracking</button>
+ <button data-cam="cutaway" aria-pressed="false">Carriage<br>cutaway</button>
+ <button data-cam="conductor" aria-pressed="false">Conductor</button>
+ </div>
+
+ <h3>Journey</h3>
+ <div class="field">
+ <label for="scrub">Scrub landscape <span class="val" id="scrubVal">1 · Desert</span></label>
+ <input type="range" id="scrub" min="0" max="1000" value="50" step="1"
+ aria-describedby="scrubHint">
+ <div class="hint" id="scrubHint">The train never stops — scrubbing re-tunes the world it moves through.</div>
+ </div>
+ <div class="toggle">
+ <label for="autoJourney" style="margin:0">Auto-drift through landscapes</label>
+ <span class="switch"><input type="checkbox" id="autoJourney" checked>
+ <span class="track"></span><span class="knob"></span></span>
+ </div>
+
+ <h3>Motion</h3>
+ <div class="field">
+ <label for="tempo">Visual tempo <span class="val" id="tempoVal">1.0×</span></label>
+ <input type="range" id="tempo" min="20" max="220" value="100">
+ </div>
+ <div class="field">
+ <label for="speed">Train speed <span class="val" id="speedVal">1.0×</span></label>
+ <input type="range" id="speed" min="0" max="220" value="100">
+ </div>
+ <div class="field">
+ <label for="trail">Trail intensity <span class="val" id="trailVal">Low</span></label>
+ <input type="range" id="trail" min="0" max="100" value="18">
+ </div>
+
+ <h3>Cast</h3>
+ <div class="field">
+ <label for="density">Crowd density <span class="val" id="densityVal">Medium</span></label>
+ <input type="range" id="density" min="2" max="10" value="6">
+ </div>
+ <div class="field">
+ <label for="animals">Animal visibility <span class="val" id="animalsVal">All</span></label>
+ <input type="range" id="animals" min="0" max="100" value="100">
+ </div>
+
+ <h3>Reveal & Access</h3>
+ <div class="toggle">
+ <label for="paths" style="margin:0">Reveal movement paths</label>
+ <span class="switch"><input type="checkbox" id="paths">
+ <span class="track"></span><span class="knob"></span></span>
+ </div>
+ <div class="toggle">
+ <label for="reduced" style="margin:0">Reduced motion</label>
+ <span class="switch"><input type="checkbox" id="reduced">
+ <span class="track"></span><span class="knob"></span></span>
+ </div>
+
+ <h3>Transport</h3>
+ <div class="row">
+ <button id="pauseBtn" aria-pressed="false">⏸ Pause</button>
+ <button id="resetBtn">↺ Reset</button>
+ </div>
+
+ <p class="hint">Keys: <kbd>Space</kbd> pause · <kbd>R</kbd> reset ·
+ <kbd>1</kbd>–<kbd>5</kbd> landscape · <kbd>C</kbd> camera ·
+ <kbd>P</kbd> paths · <kbd>M</kbd> reduced motion · <kbd>←</kbd>/<kbd>→</kbd> scrub.</p>
+</aside>
+
+<div class="sr-only" role="status" aria-live="polite" id="live"></div>
+
+<script>
+(() => {
+"use strict";
+const cv = document.getElementById('scene');
+const ctx = cv.getContext('2d');
+let W=0,H=0,DPR=1;
+
+function resize(){
+ DPR = Math.min(2, window.devicePixelRatio||1);
+ W = window.innerWidth; H = window.innerHeight;
+ cv.width = Math.floor(W*DPR); cv.height = Math.floor(H*DPR);
+ ctx.setTransform(DPR,0,0,DPR,0,0);
+}
+window.addEventListener('resize', resize); resize();
+
+/* ---------- Landscapes: palette, lighting, movement vocabulary, caption ---------- */
+const LAND = [
+ { name:"Desert Mesas", meta:"Landscape 1 of 5 · dusk copper",
+ sky:['#160d3e','#3a2668','#5b3d7a'], sun:'#ffcf7a', sunGlow:'rgba(255,180,90,.5)',
+ ground:['#6a3b2b','#4a2620','#2c1830'], accent:'#f0a24e', star:'#ffe6b0',
+ move:{speed:0.72, sway:1.0, style:'grounded'},
+ cap:"The train leaves at dusk over copper mesas. Warm light pools low; the dancers keep their weight in their heels, swaying like heat rising off the stone. A slow, grounded vocabulary." },
+ { name:"Rose Garden", meta:"Landscape 2 of 5 · twilight bloom",
+ sky:['#2a0f42','#5a1f5e','#7a2f6a'], sun:'#ffb0d0', sunGlow:'rgba(255,120,170,.45)',
+ ground:['#2e5a3a','#1f3f2c','#152a30'], accent:'#ff6c98', star:'#ffd0e6',
+ move:{speed:0.9, sway:1.35, style:'romantic'},
+ cap:"A garden of impossible roses drifts past. Couples find shared centres and turn around them; the movement softens and curves, romantic and unhurried, petals answering the sway." },
+ { name:"Flooded Moonlit Town", meta:"Landscape 3 of 5 · silver water",
+ sky:['#07122a','#123157','#1c4a72'], sun:'#dff0ff', sunGlow:'rgba(150,200,255,.4)',
+ ground:['#0b2742','#08243b','#06131f'], accent:'#8fc0e6', star:'#eaf6ff',
+ move:{speed:0.55, sway:0.8, style:'drifting'},
+ cap:"Rooftops stand in still silver water; the whole world doubles in reflection. Motion slows and floats — dancers drift as if buoyed, and the train glides across its own mirror image." },
+ { name:"Crystalline Forest", meta:"Landscape 4 of 5 · prism light",
+ sky:['#06132c','#0f3a49','#12586a'], sun:'#c9fff0', sunGlow:'rgba(90,255,215,.42)',
+ ground:['#0c3a44','#0a2c38','#07202b'], accent:'#5ff0d0', star:'#c9fff4',
+ move:{speed:1.25, sway:0.7, style:'crystalline'},
+ cap:"Trees of faceted glass throw prism light through the windows. The vocabulary turns sharp and quick — solo spinners brace crisply through their turns, refracting the beat into angles." },
+ { name:"Sunrise Coast", meta:"Landscape 5 of 5 · dawn gold",
+ sky:['#301a4a','#8a4a6a','#e0966a'], sun:'#ffe6a8', sunGlow:'rgba(255,200,120,.55)',
+ ground:['#2a3a6a','#3a4a7a','#1a2440'], accent:'#ffb066', star:'#fff0cc',
+ move:{speed:1.0, sway:1.15, style:'rising'},
+ cap:"The journey opens onto a dawn coast. Light lifts, gold and rose; the dancers rise onto their toes and the whole carriage seems to breathe upward as the sea brightens toward morning." }
+];
+
+/* ---------- State ---------- */
+const S = {
+ t:0, worldX:0, paused:false, cam:'exterior',
+ journey:0.05, // 0..1 across the five landscapes
+ auto:true,
+ tempo:1, speed:1, trail:0.18, density:6, animals:1,
+ paths:false, reduced:false, curLand:0
+};
+
+/* ---------- Helpers ---------- */
+const lerp=(a,b,t)=>a+(b-a)*t;
+const clamp=(v,a,b)=>v<a?a:v>b?b:v;
+const smooth=t=>t*t*(3-2*t);
+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)];}
+function mix(c1,c2,t){const a=hex(c1),b=hex(c2);return`rgb(${Math.round(lerp(a[0],b[0],t))},${Math.round(lerp(a[1],b[1],t))},${Math.round(lerp(a[2],b[2],t))})`;}
+function mixA(c1,c2,t,al){const a=hex(c1),b=hex(c2);return`rgba(${Math.round(lerp(a[0],b[0],t))},${Math.round(lerp(a[1],b[1],t))},${Math.round(lerp(a[2],b[2],t))},${al})`;}
+
+// weight of each landscape given journey position
+function landWeights(j){
+ const w=[0,0,0,0,0];
+ const p=j*5; // 0..5
+ for(let i=0;i<5;i++){
+ const c=i+0.5;
+ const d=Math.abs(p-c);
+ w[i]=Math.max(0,1-d); // triangular, overlapping neighbours
+ }
+ // normalize
+ let s=w.reduce((a,b)=>a+b,0)||1;
+ return w.map(x=>x/s);
+}
+// blended palette getters
+function blend(field, idxA){ // field: 'sun','accent','star'; blend two dominant
+ const w=landWeights(S.journey);
+ let r=0,g=0,b=0;
+ for(let i=0;i<5;i++){const c=hex(LAND[i][field]);r+=c[0]*w[i];g+=c[1]*w[i];b+=c[2]*w[i];}
+ return `rgb(${Math.round(r)},${Math.round(g)},${Math.round(b)})`;
+}
+function blendArr(field,k){
+ const w=landWeights(S.journey);let r=0,g=0,b=0;
+ for(let i=0;i<5;i++){const c=hex(LAND[i][field][k]);r+=c[0]*w[i];g+=c[1]*w[i];b+=c[2]*w[i];}
+ return `rgb(${Math.round(r)},${Math.round(g)},${Math.round(b)})`;
+}
+function blendMove(){
+ const w=landWeights(S.journey);let sp=0,sw=0;
+ for(let i=0;i<5;i++){sp+=LAND[i].move.speed*w[i];sw+=LAND[i].move.sway*w[i];}
+ return {speed:sp,sway:sw};
+}
+function dominant(){const w=landWeights(S.journey);let m=0,idx=0;for(let i=0;i<5;i++)if(w[i]>m){m=w[i];idx=i;}return idx;}
+
+/* ---------- Cast ---------- */
+let dancers=[], animals=[], trailHist=new Map();
+function buildDancers(){
+ dancers=[];
+ const clusters = S.density; // 2..10
+ const types=['circle','couple','solo','seated','seated','circle'];
+ let id=0;
+ for(let c=0;c<clusters;c++){
+ const u = (c+0.5)/clusters; // horizontal slot in carriage
+ const type = types[c%types.length];
+ const seatRow = (c%2===0)?0.34:0.66;
+ dancers.push({id:id++, type, u, v: type==='seated'? (c%2?0.72:0.3):0.5,
+ phase: c*0.9, dir:1, size: type==='couple'?1:1});
+ }
+}
+function buildAnimals(){
+ // roofline + alongside; distinct rhythms
+ animals=[
+ {kind:'bear', u:0.18, roof:true, rate:0.7, amp:5, phase:0.0},
+ {kind:'raven', u:0.5, roof:true, rate:2.4, amp:9, phase:1.1},
+ {kind:'raven', u:0.74, roof:true, rate:2.1, amp:8, phase:2.3},
+ {kind:'fox', u:0.88, roof:true, rate:1.5, amp:6, phase:0.6},
+ {kind:'jackrabbit', u:0.28, roof:false, rate:1.0, amp:26, phase:0.0}, // hop bursts
+ {kind:'jackrabbit', u:0.62, roof:false, rate:1.0, amp:24, phase:0.5},
+ {kind:'turtle', u:0.82, roof:false, rate:0.28, amp:2, phase:0.0}
+ ];
+}
+buildDancers(); buildAnimals();
+
+/* ---------- Geometry: carriage rect per camera ---------- */
+function carriageRect(){
+ if(S.cam==='cutaway'){
+ return {x:W*0.06, y:H*0.24, w:W*0.68, h:H*0.44, scale:1.9};
+ }
+ if(S.cam==='conductor'){
+ return {x:W*0.30, y:H*0.60, w:W*0.40, h:H*0.16, scale:0.7};
+ }
+ // exterior
+ return {x:W*0.16, y:H*0.44, w:W*0.62, h:H*0.20, scale:1.0};
+}
+
+/* ---------- Drawing: sky + poster sun ---------- */
+function drawSky(){
+ const g=ctx.createLinearGradient(0,0,0,H);
+ g.addColorStop(0, blendArr('sky',0));
+ g.addColorStop(0.5, blendArr('sky',1));
+ g.addColorStop(1, blendArr('sky',2));
+ ctx.fillStyle=g; ctx.fillRect(0,0,W,H);
+
+ // stars
+ const star=blend('star');
+ ctx.save();
+ for(let i=0;i<70;i++){
+ const sx=((i*137.5)%W);
+ const sy=(( (i*i*53.3)%1)*H*0.55);
+ const tw = S.reduced?0.6:(0.4+0.6*Math.abs(Math.sin(S.t*0.7+i)));
+ ctx.globalAlpha=0.5*tw;
+ ctx.fillStyle=star;
+ ctx.fillRect(sx, sy, 1.6,1.6);
+ }
+ ctx.restore();
+
+ // psychedelic travel-poster sun with radiating rays behind the horizon
+ const horizon=H*0.62;
+ const sunX=W*0.5, sunY=horizon;
+ const sunCol=blend('sun');
+ ctx.save();
+ ctx.translate(sunX,sunY);
+ const rot = S.reduced?0:S.t*0.05;
+ ctx.rotate(rot);
+ ctx.globalAlpha=0.16;
+ for(let r=0;r<24;r++){
+ ctx.rotate(Math.PI/12);
+ ctx.fillStyle= r%2? sunCol : blend('accent');
+ ctx.beginPath();ctx.moveTo(0,0);
+ ctx.lineTo(-W*0.9, -18); ctx.lineTo(-W*0.9, 18); ctx.closePath();ctx.fill();
+ }
+ ctx.restore();
+ // sun disc glow
+ const rg=ctx.createRadialGradient(sunX,sunY,0,sunX,sunY,W*0.22);
+ rg.addColorStop(0, sunCol);
+ rg.addColorStop(0.28, sunCol);
+ rg.addColorStop(1,'rgba(0,0,0,0)');
+ ctx.save();ctx.globalAlpha=0.55;ctx.fillStyle=rg;
+ ctx.beginPath();ctx.arc(sunX,sunY,W*0.22,0,7);ctx.fill();ctx.restore();
+ ctx.fillStyle=sunCol;ctx.globalAlpha=0.9;
+ ctx.beginPath();ctx.arc(sunX,sunY,W*0.055,0,7);ctx.fill();ctx.globalAlpha=1;
+ return horizon;
+}
+
+/* ---------- Landscape decor (crossfaded per weight) ---------- */
+function motifX(i,spacing,parallax,pad){
+ const span=W+pad*2;
+ let x=((i*spacing - S.worldX*parallax)%span);
+ if(x<0)x+=span;
+ return x-pad;
+}
+function drawLand(horizon){
+ const w=landWeights(S.journey);
+ // base rolling hills (poster bands) using blended ground colours
+ for(let band=0;band<3;band++){
+ const yy=horizon + band*H*0.08 + H*0.02;
+ ctx.fillStyle=blendArr('ground',band);
+ ctx.beginPath();ctx.moveTo(0,yy);
+ for(let x=0;x<=W;x+=24){
+ const amp=(3-band)*10;
+ const y=yy + Math.sin(x*0.006 + band + S.worldX*0.002*(3-band))*amp;
+ ctx.lineTo(x,y);
+ }
+ ctx.lineTo(W,H);ctx.lineTo(0,H);ctx.closePath();ctx.fill();
+ }
+ const acc=blend('accent');
+ // 0 Desert mesas
+ if(w[0]>0.02){ctx.save();ctx.globalAlpha=w[0];ctx.fillStyle=LAND[0].ground[0];
+ for(let i=0;i<7;i++){const x=motifX(i,240,0.5,140);const bh=70+((i*53)%60);const bw=90;
+ ctx.fillStyle=mix(LAND[0].ground[0],LAND[0].accent,0.25+0.1*(i%3));
+ ctx.fillRect(x, horizon-bh, bw, bh);
+ ctx.fillRect(x+bw*0.2, horizon-bh-24, bw*0.55, 24);}
+ ctx.restore();}
+ // 1 Rose garden
+ if(w[1]>0.02){ctx.save();ctx.globalAlpha=w[1];
+ for(let i=0;i<14;i++){const x=motifX(i,120,0.75,80);const y=horizon-8-((i*37)%26);
+ const s=10+((i*17)%8);
+ ctx.fillStyle='#1f6a33';ctx.fillRect(x-1,y,2,26);
+ for(let p=0;p<6;p++){ctx.fillStyle=p%2?LAND[1].accent:'#ff9dc0';
+ ctx.beginPath();ctx.ellipse(x+Math.cos(p)*s*0.4,y+Math.sin(p)*s*0.4,s*0.5,s*0.32,p,0,7);ctx.fill();}
+ ctx.fillStyle='#ffd7ea';ctx.beginPath();ctx.arc(x,y,s*0.32,0,7);ctx.fill();}
+ ctx.restore();}
+ // 2 Flooded town (rooftops + reflection handled by water layer)
+ if(w[2]>0.02){ctx.save();ctx.globalAlpha=w[2];
+ for(let i=0;i<9;i++){const x=motifX(i,170,0.6,120);const bh=44+((i*41)%50);const bw=64;
+ ctx.fillStyle=mix('#0b2742','#8fc0e6',0.15+0.08*(i%3));
+ ctx.fillRect(x,horizon-bh,bw,bh);
+ ctx.fillStyle=acc;ctx.beginPath();ctx.moveTo(x-6,horizon-bh);ctx.lineTo(x+bw/2,horizon-bh-26);
+ ctx.lineTo(x+bw+6,horizon-bh);ctx.closePath();ctx.fill();
+ // lit windows
+ ctx.fillStyle='rgba(255,220,150,'+(0.5+0.5*Math.sin(S.t+i))+')';
+ ctx.fillRect(x+12,horizon-bh+14,8,10);ctx.fillRect(x+bw-22,horizon-bh+14,8,10);}
+ ctx.restore();}
+ // 3 Crystalline forest
+ if(w[3]>0.02){ctx.save();ctx.globalAlpha=w[3];
+ for(let i=0;i<11;i++){const x=motifX(i,150,0.7,120);const ch=90+((i*61)%80);
+ const grad=ctx.createLinearGradient(x,horizon-ch,x+30,horizon);
+ grad.addColorStop(0, LAND[3].accent);grad.addColorStop(1,'#123a49');
+ ctx.fillStyle=grad;
+ ctx.beginPath();ctx.moveTo(x,horizon);ctx.lineTo(x+8,horizon-ch);
+ ctx.lineTo(x+18,horizon-ch*0.7);ctx.lineTo(x+28,horizon);ctx.closePath();
+ ctx.globalAlpha=w[3]*(0.6+0.4*Math.abs(Math.sin(S.t*0.9+i)));ctx.fill();}
+ ctx.restore();}
+ // 4 Sunrise coast (waves)
+ if(w[4]>0.02){ctx.save();ctx.globalAlpha=w[4]*0.9;
+ for(let r=0;r<4;r++){const yy=horizon+6+r*10;
+ ctx.strokeStyle=mixA(LAND[4].accent,'#ffffff',0.2+0.15*r,0.5);ctx.lineWidth=2;
+ ctx.beginPath();
+ for(let x=0;x<=W;x+=14){const y=yy+Math.sin(x*0.03 - S.t*1.4 + r)*4;
+ x===0?ctx.moveTo(x,y):ctx.lineTo(x,y);}
+ ctx.stroke();}
+ ctx.restore();}
+}
+
+/* ---------- Water / reflection layer (strong on flooded town) ---------- */
+function drawWater(horizon){
+ const w=landWeights(S.journey);
+ const wet = w[2]*1.0 + w[4]*0.35; // flooded strongest, coast some
+ if(wet<0.03) return;
+ ctx.save();
+ ctx.globalAlpha=clamp(wet,0,1)*0.5;
+ const g=ctx.createLinearGradient(0,horizon,0,H);
+ g.addColorStop(0, blendArr('ground',2));
+ g.addColorStop(1, blendArr('sky',2));
+ ctx.fillStyle=g;ctx.fillRect(0,horizon,W,H-horizon);
+ // shimmer lines
+ ctx.globalAlpha=clamp(wet,0,1)*0.28;
+ ctx.strokeStyle=blend('star');ctx.lineWidth=1;
+ for(let i=0;i<26;i++){const y=horizon+ (i/26)*(H-horizon);
+ const off=Math.sin(S.t*(S.reduced?0.3:1.2)+i)*8*(S.reduced?0.4:1);
+ ctx.beginPath();ctx.moveTo(0,y+off);ctx.lineTo(W,y-off);ctx.stroke();}
+ ctx.restore();
+}
+
+/* ---------- The Train ---------- */
+function drawTrain(horizon){
+ const acc=blend('accent'); const glow=blend('sun');
+ if(S.cam==='conductor'){ drawConductor(horizon,acc,glow); return; }
+
+ const rc=carriageRect();
+ // reflection of train (on water landscapes)
+ const w=landWeights(S.journey); const wet=clamp(w[2]+w[4]*0.4,0,1);
+ const bodyY=rc.y, bodyH=rc.h, bodyX=rc.x, bodyW=rc.w;
+
+ function bodyShape(yBase, h, flip){
+ ctx.beginPath();
+ const r=14;
+ ctx.moveTo(bodyX+r,yBase);
+ ctx.lineTo(bodyX+bodyW-r,yBase);
+ ctx.quadraticCurveTo(bodyX+bodyW,yBase,bodyX+bodyW,yBase+(flip?-r:r));
+ ctx.lineTo(bodyX+bodyW,yBase+(flip?-h+r:h-r));
+ ctx.quadraticCurveTo(bodyX+bodyW,yBase+(flip?-h:h),bodyX+bodyW-r,yBase+(flip?-h:h));
+ ctx.lineTo(bodyX+r,yBase+(flip?-h:h));
+ ctx.quadraticCurveTo(bodyX,yBase+(flip?-h:h),bodyX,yBase+(flip?-h+r:h-r));
+ ctx.lineTo(bodyX,yBase+(flip?-r:r));
+ ctx.quadraticCurveTo(bodyX,yBase,bodyX+r,yBase);
+ ctx.closePath();
+ }
+
+ // reflected ghost
+ if(wet>0.05){
+ ctx.save();ctx.globalAlpha=wet*0.32;
+ ctx.translate(0, 2*(bodyY+bodyH));ctx.scale(1,-1);
+ bodyShape(bodyY,bodyH,false);
+ ctx.fillStyle=mix('#0b1030',acc,0.3);ctx.fill();
+ ctx.restore();
+ }
+
+ // locomotive nose (left)
+ const noseW=Math.min(120,bodyW*0.22);
+ ctx.save();
+ const ng=ctx.createLinearGradient(bodyX-noseW,0,bodyX,0);
+ ng.addColorStop(0,mix('#0c1236',acc,0.15));ng.addColorStop(1,mix('#161d4a',acc,0.3));
+ ctx.fillStyle=ng;
+ ctx.beginPath();
+ ctx.moveTo(bodyX+8,bodyY);
+ ctx.lineTo(bodyX-noseW+18,bodyY+bodyH*0.16);
+ ctx.quadraticCurveTo(bodyX-noseW,bodyY+bodyH*0.55,bodyX-noseW+18,bodyY+bodyH*0.9);
+ ctx.lineTo(bodyX+8,bodyY+bodyH);
+ ctx.closePath();ctx.fill();
+ // headlamp
+ ctx.fillStyle=glow;ctx.globalAlpha=0.9;
+ ctx.beginPath();ctx.arc(bodyX-noseW+24,bodyY+bodyH*0.52,5,0,7);ctx.fill();
+ if(!S.reduced){ctx.globalAlpha=0.18;ctx.beginPath();
+ ctx.moveTo(bodyX-noseW+24,bodyY+bodyH*0.52);
+ ctx.lineTo(bodyX-noseW-160,bodyY+bodyH*0.1);
+ ctx.lineTo(bodyX-noseW-160,bodyY+bodyH*0.95);ctx.closePath();ctx.fill();}
+ ctx.restore();
+
+ // steam puffs from nose
+ drawSteam(bodyX-noseW+30, bodyY, glow);
+
+ // carriage body
+ ctx.save();
+ const bg=ctx.createLinearGradient(0,bodyY,0,bodyY+bodyH);
+ bg.addColorStop(0,mix('#1a2050',acc,0.10));
+ bg.addColorStop(1,mix('#0a0d28',acc,0.05));
+ bodyShape(bodyY,bodyH,false);
+ ctx.fillStyle=bg;ctx.fill();
+ // luminous outline
+ ctx.shadowColor=glow;ctx.shadowBlur=S.reduced?6:22;
+ ctx.lineWidth=2.2;ctx.strokeStyle=mix(glow,'#ffffff',0.2);ctx.stroke();
+ ctx.shadowBlur=0;
+ // accent stripe
+ ctx.strokeStyle=acc;ctx.lineWidth=3;ctx.globalAlpha=0.7;
+ ctx.beginPath();ctx.moveTo(bodyX,bodyY+bodyH*0.72);ctx.lineTo(bodyX+bodyW,bodyY+bodyH*0.72);ctx.stroke();
+ ctx.restore();
+
+ // ---- interior floor & the cutaway (windows) ----
+ // dark interior
+ ctx.save();
+ bodyShape(bodyY,bodyH,false);ctx.clip();
+ ctx.fillStyle='rgba(8,6,22,0.55)';ctx.fillRect(bodyX,bodyY,bodyW,bodyH);
+ // curtains at window tops responding to motion
+ drawCurtains(bodyX,bodyY,bodyW,bodyH,acc);
+ ctx.restore();
+
+ // window frame divisions
+ const winCount=Math.max(4,Math.floor(bodyW/90));
+ ctx.save();ctx.strokeStyle=mixA(glow,'#ffffff',0.3,0.5);ctx.lineWidth=1.5;
+ for(let i=1;i<winCount;i++){const x=bodyX+bodyW*i/winCount;
+ ctx.beginPath();ctx.moveTo(x,bodyY+6);ctx.lineTo(x,bodyY+bodyH-6);ctx.stroke();}
+ ctx.restore();
+
+ // DANCERS in the cutaway
+ drawDancers(rc);
+
+ // wheels + bogies
+ ctx.save();ctx.fillStyle='#05060f';
+ const wy=bodyY+bodyH; const wheelN=Math.max(3,Math.floor(bodyW/120));
+ for(let i=0;i<wheelN;i++){const wx=bodyX+bodyW*(i+0.5)/wheelN;
+ ctx.beginPath();ctx.arc(wx,wy+10,10,0,7);ctx.fill();
+ ctx.strokeStyle=acc;ctx.lineWidth=2;
+ const spin=S.worldX*0.05;
+ ctx.beginPath();ctx.moveTo(wx+Math.cos(spin)*7,wy+10+Math.sin(spin)*7);
+ ctx.lineTo(wx-Math.cos(spin)*7,wy+10-Math.sin(spin)*7);ctx.stroke();}
+ ctx.restore();
+
+ // ANIMALS (roof + alongside)
+ drawAnimals(rc, wy);
+
+ // luggage on roof responding to motion
+ drawLuggage(bodyX,bodyY,bodyW);
+}
+
+function drawConductor(horizon,acc,glow){
+ // forward view: converging rails into the landscape
+ const cx=W*0.5;
+ ctx.save();
+ ctx.strokeStyle=mixA(glow,'#ffffff',0.3,0.7);ctx.lineWidth=3;
+ const gaugeSway=Math.sin(S.t*1.2)*(S.reduced?2:8);
+ for(const s of [-1,1]){
+ ctx.beginPath();
+ ctx.moveTo(cx+s*30, horizon);
+ ctx.lineTo(cx+s*(W*0.42)+gaugeSway, H);
+ ctx.stroke();
+ }
+ // ties
+ ctx.strokeStyle=mixA(acc,'#000',0.2,0.5);ctx.lineWidth=4;
+ for(let i=0;i<16;i++){
+ const tt=(i/16 + (S.worldX*0.004)%(1/16));
+ const p=tt%1;const y=lerp(horizon,H,p*p);const wdt=lerp(30, W*0.42, p*p);
+ ctx.globalAlpha=0.6*p;
+ ctx.beginPath();ctx.moveTo(cx-wdt+gaugeSway*p,y);ctx.lineTo(cx+wdt+gaugeSway*p,y);ctx.stroke();
+ }
+ ctx.globalAlpha=1;
+ ctx.restore();
+ // steam ahead
+ drawSteam(cx, horizon-10, glow, true);
+ // locomotive nose framing bottom
+ ctx.save();
+ const g=ctx.createLinearGradient(0,H*0.82,0,H);
+ g.addColorStop(0,mix('#12183e',acc,0.2));g.addColorStop(1,'#04050e');
+ ctx.fillStyle=g;
+ ctx.beginPath();ctx.moveTo(0,H);ctx.lineTo(0,H*0.9);
+ ctx.quadraticCurveTo(cx,H*0.78,W,H*0.9);ctx.lineTo(W,H);ctx.closePath();ctx.fill();
+ // headlamp glow
+ ctx.fillStyle=glow;ctx.shadowColor=glow;ctx.shadowBlur=30;
+ ctx.beginPath();ctx.arc(cx,H*0.86,10,0,7);ctx.fill();ctx.shadowBlur=0;
+ // instrument dials
+ ctx.strokeStyle=mixA(glow,'#fff',0.3,0.6);ctx.lineWidth=2;
+ for(const dx of [-90,90]){ctx.beginPath();ctx.arc(cx+dx,H*0.93,16,0,7);ctx.stroke();
+ const a=-2.4+Math.sin(S.t*1.5+dx)*(S.reduced?0.1:0.5);
+ ctx.beginPath();ctx.moveTo(cx+dx,H*0.93);ctx.lineTo(cx+dx+Math.cos(a)*13,H*0.93+Math.sin(a)*13);ctx.stroke();}
+ ctx.restore();
+ // animals running alongside visible at edges
+ drawConductorAnimals(horizon);
+}
+
+/* ---------- Steam ---------- */
+let steamPuffs=[];
+function drawSteam(x,y,glow,upward){
+ if(S.reduced){ // gentle static-ish
+ }
+ // spawn
+ if(!S.paused && Math.random()<0.4*S.speed){
+ steamPuffs.push({x,y,r:6,life:1,vx:(upward?0:-1.2)*S.speed - (upward?0:0.5), vy:-0.6-Math.random()*0.7});
+ }
+ ctx.save();
+ for(const p of steamPuffs){
+ ctx.globalAlpha=p.life*0.35;
+ ctx.fillStyle=mix(glow,'#ffffff',0.5);
+ ctx.beginPath();ctx.arc(p.x,p.y,p.r,0,7);ctx.fill();
+ }
+ ctx.restore();
+ if(!S.paused){
+ const dt=(S.reduced?0.4:1);
+ for(const p of steamPuffs){p.x+=p.vx*dt;p.y+=p.vy*dt;p.r+=0.5*dt;p.life-=0.012*dt;}
+ steamPuffs=steamPuffs.filter(p=>p.life>0).slice(-60);
+ }
+}
+
+/* ---------- Curtains & Luggage ---------- */
+function drawCurtains(x,y,w,h,acc){
+ const n=Math.max(4,Math.floor(w/90));
+ ctx.save();
+ for(let i=0;i<n;i++){
+ const wx=x+w*i/n, ww=w/n;
+ const sway=Math.sin(S.t*1.4 + i)* (S.reduced?2:6) * S.speed;
+ ctx.fillStyle=mixA(acc,'#000',0.35,0.5);
+ ctx.beginPath();ctx.moveTo(wx+4,y+4);
+ ctx.quadraticCurveTo(wx+ww*0.5+sway,y+h*0.28,wx+ww-4,y+4);
+ ctx.lineTo(wx+ww-4,y+4);ctx.closePath();ctx.fill();
+ }
+ ctx.restore();
+}
+function drawLuggage(x,y,w){
+ const cases=[[0.3,acc0(),18,12],[0.55,'#8a5a2a',22,14],[0.72,'#5a6a8a',16,11]];
+ function acc0(){return blend('accent');}
+ const bump = Math.sin(S.worldX*0.02)* (S.reduced?0.5:2.5)*S.speed;
+ ctx.save();
+ for(const c of cases){
+ const cx=x+w*c[0]; const cy=y-c[3]-2+bump*(0.6+c[0]);
+ ctx.fillStyle=c[1];ctx.fillRect(cx,cy,c[2],c[3]);
+ ctx.strokeStyle='rgba(0,0,0,.4)';ctx.strokeRect(cx,cy,c[2],c[3]);
+ ctx.fillStyle='rgba(255,255,255,.3)';ctx.fillRect(cx+2,cy+c[3]*0.4,c[2]-4,1.5);
+ }
+ ctx.restore();
+}
+
+/* ---------- Dancers ---------- */
+function pushTrail(id,x,y){
+ let arr=trailHist.get(id);if(!arr){arr=[];trailHist.set(id,arr);}
+ arr.push([x,y]);
+ const maxLen=Math.round(2 + S.trail*60);
+ while(arr.length>maxLen)arr.shift();
+ return arr;
+}
+function drawTrail(arr,col){
+ if(S.trail<0.02||arr.length<2)return;
+ ctx.save();
+ for(let i=1;i<arr.length;i++){
+ ctx.globalAlpha=(i/arr.length)*0.5*S.trail*3;
+ ctx.strokeStyle=col;ctx.lineWidth=2;
+ ctx.beginPath();ctx.moveTo(arr[i-1][0],arr[i-1][1]);ctx.lineTo(arr[i][0],arr[i][1]);ctx.stroke();
+ }
+ ctx.restore();
+}
+function figure(x,y,r,col,lean){
+ ctx.save();ctx.translate(x,y);ctx.rotate(lean||0);
+ ctx.fillStyle=col;
+ ctx.beginPath();ctx.arc(0,-r*1.4,r*0.55,0,7);ctx.fill(); // head
+ ctx.beginPath();ctx.moveTo(0,-r*0.9);ctx.lineTo(-r*0.5,r*0.6);
+ ctx.lineTo(r*0.5,r*0.6);ctx.closePath();ctx.fill(); // body
+ ctx.restore();
+}
+function drawDancers(rc){
+ const mv=blendMove();
+ const spd=mv.speed*S.tempo*(S.reduced?0.35:1);
+ const swayF=mv.sway*(S.reduced?0.4:1);
+ const acc=blend('accent'); const sun=blend('sun');
+ const base=Math.min(rc.w,rc.h)*0.05*rc.scale;
+ const t=S.t;
+ for(const d of dancers){
+ const cx=rc.x + rc.w*d.u;
+ const cy=rc.y + rc.h*d.v;
+ const R=base*(rc.h/ (H*0.2));
+ const col = mix('#f4ecff', d.id%2?acc:sun, 0.4);
+ if(d.type==='circle'){
+ // small circle that reverses direction every ~5s
+ const period=5;
+ const dir = Math.floor((t*spd)/period)%2===0?1:-1;
+ const ang = t*spd*1.4*dir + d.phase;
+ const rr=R*1.3;
+ const x=cx+Math.cos(ang)*rr, y=cy+Math.sin(ang)*rr*0.6;
+ if(S.paths){ctx.save();ctx.globalAlpha=0.4;ctx.strokeStyle=acc;ctx.setLineDash([3,4]);
+ ctx.beginPath();ctx.ellipse(cx,cy,rr,rr*0.6,0,0,7);ctx.stroke();ctx.restore();}
+ const arr=pushTrail(d.id,x,y);drawTrail(arr,col);
+ figure(x,y,R,col, Math.sin(t*spd*2+d.phase)*0.15*swayF);
+ } else if(d.type==='couple'){
+ // two figures rotating a shared centre
+ const ang=t*spd*1.1 + d.phase; const rr=R*1.6;
+ const x1=cx+Math.cos(ang)*rr,y1=cy+Math.sin(ang)*rr*0.6;
+ const x2=cx-Math.cos(ang)*rr,y2=cy-Math.sin(ang)*rr*0.6;
+ if(S.paths){ctx.save();ctx.globalAlpha=0.4;ctx.strokeStyle=sun;ctx.setLineDash([3,4]);
+ ctx.beginPath();ctx.ellipse(cx,cy,rr,rr*0.6,0,0,7);ctx.stroke();
+ ctx.fillStyle=sun;ctx.beginPath();ctx.arc(cx,cy,2,0,7);ctx.fill();ctx.restore();}
+ const a1=pushTrail(d.id*100+1,x1,y1);drawTrail(a1,acc);
+ const a2=pushTrail(d.id*100+2,x2,y2);drawTrail(a2,sun);
+ // joined hands line
+ ctx.save();ctx.globalAlpha=0.35;ctx.strokeStyle=col;ctx.lineWidth=1.5;
+ ctx.beginPath();ctx.moveTo(x1,y1);ctx.lineTo(x2,y2);ctx.stroke();ctx.restore();
+ figure(x1,y1,R,mix(col,acc,0.4),(x1-cx)*0.01);
+ figure(x2,y2,R,mix(col,sun,0.4),(x2-cx)*0.01);
+ } else if(d.type==='solo'){
+ // spinner bracing through turns (lean opposes spin)
+ const ang=t*spd*3 + d.phase;
+ const lean=Math.sin(ang)*0.35*swayF;
+ const x=cx+Math.sin(ang*0.5)*R*0.4, y=cy;
+ if(S.paths){ctx.save();ctx.globalAlpha=0.35;ctx.strokeStyle=acc;ctx.setLineDash([2,4]);
+ ctx.beginPath();ctx.arc(cx,cy,R*0.9,0,7);ctx.stroke();ctx.restore();}
+ const arr=pushTrail(d.id,x,y-R);drawTrail(arr,col);
+ ctx.save();ctx.translate(x,y);ctx.scale(Math.cos(ang)*0.4+0.7,1);ctx.translate(-x,-y);
+ figure(x,y,R,col,lean);ctx.restore();
+ } else { // seated clapping in offset waves
+ const wave = Math.sin(t*spd*2 - d.u*4)*0.5+0.5; // offset by position => wave
+ const spread=R*wave*swayF;
+ // body seated
+ figure(cx,cy+R*0.2,R*0.9,mix(col,'#b9a8e0',0.3),0);
+ // clapping hands
+ ctx.save();ctx.fillStyle=sun;
+ ctx.beginPath();ctx.arc(cx-spread,cy-R*0.6,R*0.22,0,7);ctx.fill();
+ ctx.beginPath();ctx.arc(cx+spread,cy-R*0.6,R*0.22,0,7);ctx.fill();
+ if(wave>0.9){ctx.globalAlpha=0.5;ctx.strokeStyle=sun;
+ ctx.beginPath();ctx.arc(cx,cy-R*0.6,R*0.4,0,7);ctx.stroke();}
+ ctx.restore();
+ if(S.paths){ctx.save();ctx.globalAlpha=0.3;ctx.strokeStyle=sun;ctx.setLineDash([2,3]);
+ ctx.beginPath();ctx.moveTo(cx-R,cy-R*0.6);ctx.lineTo(cx+R,cy-R*0.6);ctx.stroke();ctx.restore();}
+ }
+ }
+}
+
+/* ---------- Animals ---------- */
+function animalGlyph(kind,x,y,s,col,t,a){
+ ctx.save();ctx.translate(x,y);ctx.fillStyle=col;
+ if(kind==='bear'){
+ ctx.beginPath();ctx.ellipse(0,0,s*1.1,s*0.7,0,0,7);ctx.fill();
+ ctx.beginPath();ctx.arc(s*0.9,-s*0.4,s*0.5,0,7);ctx.fill();
+ ctx.beginPath();ctx.arc(s*0.7,-s*0.9,s*0.18,0,7);ctx.fill();
+ ctx.beginPath();ctx.arc(s*1.1,-s*0.9,s*0.18,0,7);ctx.fill();
+ } else if(kind==='raven'){
+ const flap=Math.sin(t*a.rate+a.phase);
+ ctx.beginPath();ctx.ellipse(0,0,s*0.7,s*0.35,0,0,7);ctx.fill();
+ ctx.beginPath();ctx.moveTo(0,0);ctx.lineTo(-s*1.2,-flap*s);ctx.lineTo(-s*0.4,s*0.2);ctx.closePath();ctx.fill();
+ ctx.beginPath();ctx.moveTo(0,0);ctx.lineTo(s*1.2,-flap*s);ctx.lineTo(s*0.4,s*0.2);ctx.closePath();ctx.fill();
+ ctx.beginPath();ctx.arc(s*0.6,-s*0.3,s*0.22,0,7);ctx.fill();
+ } else if(kind==='fox'){
+ ctx.beginPath();ctx.ellipse(0,0,s,s*0.42,0,0,7);ctx.fill();
+ ctx.beginPath();ctx.arc(s*0.8,-s*0.3,s*0.35,0,7);ctx.fill();
+ ctx.beginPath();ctx.moveTo(s*0.7,-s*0.6);ctx.lineTo(s*0.95,-s*1.05);ctx.lineTo(s*1.05,-s*0.55);ctx.closePath();ctx.fill();
+ ctx.beginPath();ctx.moveTo(-s,0);ctx.lineTo(-s*1.7,-s*0.3);ctx.lineTo(-s*0.9,s*0.25);ctx.closePath();ctx.fill();
+ } else if(kind==='jackrabbit'){
+ ctx.beginPath();ctx.ellipse(0,0,s*0.7,s*0.5,0,0,7);ctx.fill();
+ ctx.beginPath();ctx.arc(s*0.6,-s*0.4,s*0.3,0,7);ctx.fill();
+ ctx.beginPath();ctx.ellipse(s*0.6,-s*1.1,s*0.12,s*0.5,0.2,0,7);ctx.fill();
+ ctx.beginPath();ctx.ellipse(s*0.85,-s*1.1,s*0.12,s*0.5,0.4,0,7);ctx.fill();
+ } else if(kind==='turtle'){
+ ctx.beginPath();ctx.ellipse(0,0,s*1.1,s*0.6,0,Math.PI,0);ctx.fill();
+ ctx.fillRect(-s*1.1,0,s*2.2,s*0.2);
+ ctx.beginPath();ctx.arc(s*1.2,-s*0.05,s*0.28,0,7);ctx.fill();
+ ctx.strokeStyle='rgba(0,0,0,.3)';ctx.lineWidth=1;
+ ctx.beginPath();ctx.moveTo(-s*0.4,-s*0.5);ctx.lineTo(-s*0.4,0);
+ ctx.moveTo(s*0.4,-s*0.5);ctx.lineTo(s*0.4,0);ctx.stroke();
+ }
+ ctx.restore();
+}
+function drawAnimals(rc, wheelY){
+ if(S.animals<0.02)return;
+ const acc=blend('accent'); const sun=blend('sun');
+ const t=S.t*S.tempo*(S.reduced?0.4:1);
+ const roofY=rc.y-6; const groundY=wheelY+18;
+ const cols={bear:'#5a3a24',raven:'#1a1428',fox:'#d06a2a',jackrabbit:'#c9b48a',turtle:'#3a6a4a'};
+ let idx=0;
+ for(const a of animals){
+ idx++;
+ if((idx/animals.length) > S.animals+0.001) continue; // visibility fades cast out gradually
+ const s=Math.min(rc.w,rc.h)*0.045*(a.roof?1:1.1);
+ let x,y;
+ if(a.roof){
+ x=rc.x+rc.w*a.u;
+ y=roofY - Math.abs(Math.sin(t*a.rate+a.phase))*a.amp - s*0.5;
+ } else {
+ // alongside on the ground; jackrabbits hop in bursts
+ x=rc.x+rc.w*a.u + (a.kind==='turtle'? Math.sin(t*a.rate)*4 : 0);
+ let hop=0;
+ if(a.kind==='jackrabbit'){
+ const cyc=(t*a.rate+a.phase)%2.2;
+ hop = cyc<0.6? Math.sin(cyc/0.6*Math.PI)*a.amp : 0;
+ } else hop=Math.abs(Math.sin(t*a.rate+a.phase))*a.amp;
+ y=groundY - hop;
+ }
+ ctx.save();ctx.globalAlpha=clamp((S.animals+0.05)*1.2,0,1);
+ // faint rhythm path
+ if(S.paths){ctx.globalAlpha*=0.5;ctx.strokeStyle=acc;ctx.setLineDash([2,4]);
+ ctx.beginPath();ctx.moveTo(x-a.amp*0.2,y+a.amp);ctx.lineTo(x-a.amp*0.2,y);ctx.stroke();ctx.setLineDash([]);}
+ animalGlyph(a.kind,x,y,s,cols[a.kind]||acc,t,a);
+ ctx.restore();
+ }
+}
+function drawConductorAnimals(horizon){
+ if(S.animals<0.02)return;
+ const t=S.t*S.tempo*(S.reduced?0.4:1);
+ const cols={fox:'#d06a2a',jackrabbit:'#c9b48a',raven:'#1a1428'};
+ const runners=[{kind:'fox',side:-1,u:0.7,rate:1.5,amp:14},
+ {kind:'jackrabbit',side:1,u:0.62,rate:1.0,amp:26},
+ {kind:'raven',side:-1,u:0.4,rate:2.4,amp:12}];
+ for(const r of runners){
+ const p=r.u;const y=lerp(horizon,H*0.95,p*p);
+ const x=W*0.5 + r.side*lerp(40,W*0.4,p*p);
+ const s=lerp(6,26,p);
+ let hop=r.kind==='jackrabbit'? (((t*r.rate)%2<0.6)?Math.sin(((t*r.rate)%2)/0.6*Math.PI)*r.amp:0)
+ :Math.abs(Math.sin(t*r.rate))*r.amp;
+ ctx.save();ctx.globalAlpha=clamp(S.animals,0,1)*p;
+ animalGlyph(r.kind,x,y-hop,s,cols[r.kind],t,{rate:r.rate,phase:0});
+ ctx.restore();
+ }
+}
+
+/* ---------- Main loop ---------- */
+let last=performance.now();
+function frame(now){
+ const dt=Math.min(0.05,(now-last)/1000); last=now;
+ if(!S.paused){
+ const eff=S.reduced?0.4:1;
+ S.t += dt*eff;
+ S.worldX += dt*60*S.speed*eff*(1+blendMove().speed*0.3);
+ if(S.auto){
+ S.journey += dt*0.02*(S.reduced?0.4:1);
+ if(S.journey>1) S.journey-=1;
+ syncScrub();
+ }
+ }
+ // background: fully repaint (trails handled per-object)
+ const horizon=drawSky();
+ drawLand(horizon);
+ drawWater(horizon);
+ drawTrain(horizon);
+ // vignette
+ const vg=ctx.createRadialGradient(W/2,H/2,H*0.3,W/2,H/2,H*0.85);
+ vg.addColorStop(0,'rgba(0,0,0,0)');vg.addColorStop(1,'rgba(0,0,0,0.55)');
+ ctx.fillStyle=vg;ctx.fillRect(0,0,W,H);
+
+ updateCaption();
+ requestAnimationFrame(frame);
+}
+requestAnimationFrame(frame);
+
+/* ---------- Caption / landscape sync ---------- */
+const capMeta=document.getElementById('capMeta'),capTitle=document.getElementById('capTitle'),
+ capText=document.getElementById('capText'),live=document.getElementById('live');
+function updateCaption(){
+ const idx=dominant();
+ if(idx!==S.curLand){
+ S.curLand=idx;
+ const L=LAND[idx];
+ capMeta.textContent=L.meta;capTitle.textContent=L.name;capText.textContent=L.cap;
+ live.textContent="Now crossing "+L.name+". "+L.cap;
+ }
+}
+updateCaption();
+
+/* ---------- Controls wiring ---------- */
+const $=id=>document.getElementById(id);
+const scrub=$('scrub'), scrubVal=$('scrubVal');
+function syncScrub(){ scrub.value=Math.round(S.journey*1000);
+ const i=dominant(); scrubVal.textContent=(i+1)+" · "+LAND[i].name.split(' ')[0];}
+scrub.addEventListener('input',()=>{S.journey=clamp(scrub.value/1000,0,1);syncScrub();});
+$('autoJourney').addEventListener('change',e=>S.auto=e.target.checked);
+
+const camBtns=document.querySelectorAll('.cams button');
+function setCam(cam){
+ S.cam=cam; trailHist.clear();
+ camBtns.forEach(b=>b.setAttribute('aria-pressed', b.dataset.cam===cam));
+ live.textContent="Camera: "+cam;
+}
+camBtns.forEach(b=>b.addEventListener('click',()=>setCam(b.dataset.cam)));
+
+$('tempo').addEventListener('input',e=>{S.tempo=e.target.value/100;$('tempoVal').textContent=S.tempo.toFixed(1)+'×';});
+$('speed').addEventListener('input',e=>{S.speed=e.target.value/100;$('speedVal').textContent=S.speed.toFixed(1)+'×';});
+$('trail').addEventListener('input',e=>{S.trail=e.target.value/100;
+ $('trailVal').textContent=S.trail<0.05?'Off':S.trail<0.35?'Low':S.trail<0.7?'Medium':'High';});
+$('density').addEventListener('input',e=>{S.density=+e.target.value;buildDancers();trailHist.clear();
+ $('densityVal').textContent=S.density<4?'Sparse':S.density<7?'Medium':'Crowded';});
+$('animals').addEventListener('input',e=>{S.animals=e.target.value/100;
+ $('animalsVal').textContent=S.animals<0.05?'Hidden':S.animals<0.5?'Few':S.animals<0.99?'Most':'All';});
+$('paths').addEventListener('change',e=>{S.paths=e.target.checked;live.textContent="Movement paths "+(S.paths?'revealed':'hidden');});
+$('reduced').addEventListener('change',e=>{S.reduced=e.target.checked;live.textContent="Reduced motion "+(S.reduced?'on':'off');});
+
+const pauseBtn=$('pauseBtn');
+function togglePause(){S.paused=!S.paused;pauseBtn.setAttribute('aria-pressed',S.paused);
+ pauseBtn.textContent=S.paused?'▶ Play':'⏸ Pause';live.textContent=S.paused?'Paused':'Playing';}
+pauseBtn.addEventListener('click',togglePause);
+
+$('resetBtn').addEventListener('click',reset);
+function reset(){
+ S.t=0;S.worldX=0;S.journey=0.05;S.tempo=1;S.speed=1;S.trail=0.18;S.density=6;
+ S.animals=1;S.paths=false;S.reduced=false;S.auto=true;S.paused=false;S.cam='exterior';
+ steamPuffs=[];trailHist.clear();buildDancers();buildAnimals();
+ $('tempo').value=100;$('speed').value=100;$('trail').value=18;$('density').value=6;
+ $('animals').value=100;$('paths').checked=false;$('reduced').checked=false;$('autoJourney').checked=true;
+ $('tempoVal').textContent='1.0×';$('speedVal').textContent='1.0×';$('trailVal').textContent='Low';
+ $('densityVal').textContent='Medium';$('animalsVal').textContent='All';
+ pauseBtn.setAttribute('aria-pressed',false);pauseBtn.textContent='⏸ Pause';
+ setCam('exterior');syncScrub();live.textContent='Reset to the start of the journey.';
+}
+
+// panel toggle
+const panel=$('panel'), pt=$('panelToggle');
+pt.addEventListener('click',()=>{const h=panel.classList.toggle('hidden');
+ pt.setAttribute('aria-expanded',!h);});
+
+// Respect OS reduced-motion preference on load
+if(window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches){
+ S.reduced=true; $('reduced').checked=true;
+}
+
+// Keyboard shortcuts
+window.addEventListener('keydown',e=>{
+ if(['INPUT','SELECT','TEXTAREA'].includes(document.activeElement.tagName) && e.key!=='Escape') {
+ // still allow global if not typing in range? ranges use arrows; let them.
+ }
+ const k=e.key.toLowerCase();
+ if(k===' '){e.preventDefault();togglePause();}
+ else if(k==='r'){reset();}
+ else if(k==='c'){const order=['exterior','cutaway','conductor'];setCam(order[(order.indexOf(S.cam)+1)%3]);}
+ else if(k==='p'){S.paths=!S.paths;$('paths').checked=S.paths;}
+ else if(k==='m'){S.reduced=!S.reduced;$('reduced').checked=S.reduced;}
+ else if(k>='1'&&k<='5'){S.auto=false;$('autoJourney').checked=false;
+ S.journey=(+k-1)/5+0.1;syncScrub();}
+ else if(k==='arrowleft'){S.auto=false;$('autoJourney').checked=false;S.journey=clamp(S.journey-0.02,0,1);syncScrub();}
+ else if(k==='arrowright'){S.auto=false;$('autoJourney').checked=false;S.journey=clamp(S.journey+0.02,0,1);syncScrub();}
+});
+
+syncScrub();
+})();
+</script>
+</body>
+</html>
\ No newline at end of file
diff --git a/data/artifacts/77fc58739877/qwen25-7b.html b/data/artifacts/77fc58739877/qwen25-7b.html
new file mode 100644
index 0000000..7496c8a
--- /dev/null
+++ b/data/artifacts/77fc58739877/qwen25-7b.html
@@ -0,0 +1,128 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>Indigo Train Through Dream Country</title>
+ <style>
+ body, html {
+ margin: 0;
+ padding: 0;
+ overflow: hidden;
+ height: 100%;
+ background-color: #283747;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ }
+ canvas {
+ width: 100vw;
+ height: 100vh;
+ z-index: -1;
+ }
+ </style>
+</head>
+<body>
+ <canvas id="dreamTrainCanvas"></canvas>
+ <script>
+ const canvas = document.getElementById('dreamTrainCanvas');
+ const ctx = canvas.getContext('2d');
+ canvas.width = window.innerWidth;
+ canvas.height = window.innerHeight;
+
+ function setupScene(palette) {
+ ctx.fillStyle = palette.background;
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
+ }
+
+ function drawDesertMesa() {
+ setupScene({background: '#964B00', text: '#fff'});
+ // Draw desert mesas
+ ctx.beginPath();
+ ctx.arc(canvas.width / 2, canvas.height / 2, 350, 0, Math.PI * 2);
+ ctx.fillStyle = '#F0D8B3';
+ ctx.fill();
+ }
+
+ function drawRoseGarden() {
+ setupScene({background: '#ff69b4', text: '#fff'});
+ // Draw rose garden
+ for (let i = -canvas.width; i < canvas.width; i += 15) {
+ ctx.beginPath();
+ ctx.arc(i, canvas.height / 2, 30 + Math.random() * 50, 0, Math.PI * 2);
+ ctx.fillStyle = '#ff69b4';
+ ctx.fill();
+ }
+ }
+
+ function drawFloodedTown() {
+ setupScene({background: '#1c1c1c', text: '#fff'});
+ // Draw flooded town
+ for (let i = -canvas.width; i < canvas.width; i += 20) {
+ ctx.beginPath();
+ ctx.arc(i, Math.random() * canvas.height + 50, 30 + Math.random() * 10, 0, Math.PI * 2);
+ ctx.fillStyle = '#ff69b4';
+ ctx.fill();
+ }
+ }
+
+ function drawCrystallineForest() {
+ setupScene({background: '#87CEEB', text: '#fff'});
+ // Draw crystalline forest
+ for (let i = -canvas.width; i < canvas.width; i += 10) {
+ ctx.beginPath();
+ ctx.arc(i, Math.random() * canvas.height + 50, 30 + Math.random() * 10, 0, Math.PI * 2);
+ ctx.fillStyle = '#87CEEB';
+ ctx.fill();
+ }
+ }
+
+ function drawSunriseCoast() {
+ setupScene({background: '#FADBD8', text: '#000'});
+ // Draw sunrise coast
+ for (let i = -canvas.width; i < canvas.width; i += 25) {
+ ctx.beginPath();
+ ctx.arc(i, Math.random() * canvas.height + 50, 30 + Math.random() * 10, 0, Math.PI * 2);
+ ctx.fillStyle = '#FADBD8';
+ ctx.fill();
+ }
+ }
+
+ function animateTrain(path) {
+ let x = 0;
+ const speed = 0.5;
+
+ function drawFrame(timestamp) {
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
+ path(x);
+ x += speed * Math.sin(timestamp / 1000);
+ if (x > canvas.width) x -= canvas.width;
+ requestAnimationFrame(drawFrame);
+ }
+
+ requestAnimationFrame(drawFrame);
+ }
+
+ function drawTrainAnimation() {
+ animateTrain(drawDesertMesa);
+ }
+
+ function startAnimation(path) {
+ const animation = requestAnimationFrame(() => {
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
+ path();
+ animation;
+ });
+ }
+
+ window.addEventListener('load', () => {
+ drawTrainAnimation();
+ });
+
+ // Demo: Change to another scene
+ function changeScene() {
+ startAnimation(drawRoseGarden);
+ }
+ </script>
+</body>
+</html>
diff --git a/data/artifacts/77fc58739877/qwen25-7b.png b/data/artifacts/77fc58739877/qwen25-7b.png
index e52e66a..60dfcf3 100644
Binary files a/data/artifacts/77fc58739877/qwen25-7b.png and b/data/artifacts/77fc58739877/qwen25-7b.png differ
diff --git a/data/artifacts/77fc58739877/qwen3-14b.html b/data/artifacts/77fc58739877/qwen3-14b.html
new file mode 100644
index 0000000..9faee00
--- /dev/null
+++ b/data/artifacts/77fc58739877/qwen3-14b.html
@@ -0,0 +1,423 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <title>Indigo Train Through Dream Country</title>
+ <style>
+ html, body {
+ margin: 0;
+ padding: 0;
+ height: 100%;
+ overflow: hidden;
+ background: #000;
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
+ color: #fff;
+ }
+ canvas {
+ display: block;
+ }
+ #controls {
+ position: absolute;
+ top: 10px;
+ left: 10px;
+ background: rgba(0, 0, 0, 0.5);
+ padding: 10px;
+ border-radius: 8px;
+ z-index: 10;
+ }
+ #controls label {
+ display: block;
+ margin: 5px 0;
+ }
+ #controls input, #controls select, #controls button {
+ margin: 5px 0;
+ padding: 6px;
+ font-size: 14px;
+ width: 150px;
+ border: 1px solid #888;
+ background: #222;
+ color: #fff;
+ border-radius: 4px;
+ }
+ #controls button {
+ cursor: pointer;
+ }
+ #caption {
+ position: absolute;
+ bottom: 10px;
+ left: 10px;
+ background: rgba(0, 0, 0, 0.5);
+ padding: 10px;
+ border-radius: 8px;
+ z-index: 10;
+ max-width: 600px;
+ white-space: pre-wrap;
+ }
+ </style>
+</head>
+<body>
+ <div id="controls">
+ <label>View:</label>
+ <select id="view" aria-label="View">
+ <option value="exterior">Exterior Tracking</option>
+ <option value="carriage">Carriage Cutaway</option>
+ <option value="conductor">Conductor Camera</option>
+ </select>
+ <label>Current Landscape:</label>
+ <select id="landscape" aria-label="Landscape">
+ <option value="desert">Desert Mesas</option>
+ <option value="garden">Rose Garden</option>
+ <option value="flooded">Flooded Moonlit Town</option>
+ <option value="crystal">Crystalline Forest</option>
+ <option value="sunrise">Sunrise Coast</option>
+ </select>
+ <label>Tempo:</label>
+ <input type="range" id="tempo" min="0.5" max="2" step="0.1" value="1" aria-label="Tempo" />
+ <label>Train Speed:</label>
+ <input type="range" id="speed" min="1" max="5" step="0.1" value="2" aria-label="Train Speed" />
+ <label>Crowd Density:</label>
+ <input type="range" id="density" min="0" max="1" step="0.1" value="0.5" aria-label="Crowd Density" />
+ <label>Animal Visibility:</label>
+ <input type="range" id="animal" min="0" max="1" step="0.1" value="1" aria-label="Animal Visibility" />
+ <label>Trail Intensity:</label>
+ <input type="range" id="trail" min="0" max="1" step="0.1" value="0.3" aria-label="Trail Intensity" />
+ <button id="reset">Reset</button>
+ <button id="pause">Pause</button>
+ <button id="paths">Show Movement Paths</button>
+ </div>
+ <div id="caption"></div>
+ <canvas id="canvas"></canvas>
+
+ <script>
+ const canvas = document.getElementById('canvas');
+ const ctx = canvas.getContext('2d');
+ const width = window.innerWidth;
+ const height = window.innerHeight;
+ canvas.width = width;
+ canvas.height = height;
+ let isPaused = false;
+ let showPaths = false;
+
+ const landscapes = [
+ {
+ name: 'Desert Mesas',
+ color: '#331155',
+ light: '#8800cc',
+ caption: 'Amidst golden dunes and jagged rock spires, travelers move in small, reverberating circles, their steps echoing against the sky.'
+ },
+ {
+ name: 'Rose Garden',
+ color: '#553377',
+ light: '#cc00ff',
+ caption: 'Petals drift in the air as dancers rotate in couples, their movements slow and deliberate, like petals unfolding in the breeze.'
+ },
+ {
+ name: 'Flooded Moonlit Town',
+ color: '#224466',
+ light: '#0088cc',
+ caption: 'Reflecting waters mirror the travelers, whose choreography is fluid and mirrored, like ripples in a pond.'
+ },
+ {
+ name: 'Crystalline Forest',
+ color: '#116688',
+ light: '#00ddff',
+ caption: 'Shimmering crystals refract the light as dancers spin in intricate spirals, their motion precise and resonant.'
+ },
+ {
+ name: 'Sunrise Coast',
+ color: '#443322',
+ light: '#ffaa00',
+ caption: 'Golden light rises from the sea as dancers move in synchronized waves, their steps echoing the rhythm of the tide.'
+ }
+ ];
+
+ const animals = [
+ { type: 'bear', color: '#884422', speed: 0.5, offset: 0 },
+ { type: 'jackrabbit', color: '#cc8844', speed: 1.2, offset: 1 },
+ { type: 'raven', color: '#444444', speed: 0.8, offset: 2 },
+ { type: 'fox', color: '#aa5500', speed: 1.5, offset: 3 },
+ { type: 'turtle', color: '#335533', speed: 0.3, offset: 4 }
+ ];
+
+ const travelers = [];
+ const animalPositions = [];
+
+ function initTravelers(count) {
+ travelers.length = 0;
+ for (let i = 0; i < count; i++) {
+ const type = Math.floor(Math.random() * 4);
+ travelers.push({
+ type,
+ x: Math.random() * canvas.width,
+ y: Math.random() * canvas.height,
+ angle: Math.random() * Math.PI * 2,
+ speed: 0.5 + Math.random(),
+ path: []
+ });
+ }
+ }
+
+ function initAnimals(count) {
+ animalPositions.length = 0;
+ for (let i = 0; i < count; i++) {
+ const animal = animals[Math.floor(Math.random() * animals.length)];
+ animalPositions.push({
+ ...animal,
+ x: Math.random() * canvas.width,
+ y: Math.random() * canvas.height,
+ angle: Math.random() * Math.PI * 2,
+ path: []
+ });
+ }
+ }
+
+ function drawCircle(x, y, radius, color, alpha) {
+ ctx.beginPath();
+ ctx.arc(x, y, radius, 0, Math.PI * 2);
+ ctx.fillStyle = `rgba(${color}, ${alpha})`;
+ ctx.fill();
+ }
+
+ function drawLine(x1, y1, x2, y2, color, alpha) {
+ ctx.beginPath();
+ ctx.moveTo(x1, y1);
+ ctx.lineTo(x2, y2);
+ ctx.strokeStyle = `rgba(${color}, ${alpha})`;
+ ctx.stroke();
+ }
+
+ function drawTriangle(x, y, size, color, alpha) {
+ ctx.beginPath();
+ ctx.moveTo(x, y);
+ ctx.lineTo(x + size, y + size);
+ ctx.lineTo(x - size, y + size);
+ ctx.closePath();
+ ctx.fillStyle = `rgba(${color}, ${alpha})`;
+ ctx.fill();
+ }
+
+ function drawRectangle(x, y, width, height, color, alpha) {
+ ctx.fillStyle = `rgba(${color}, ${alpha})`;
+ ctx.fillRect(x, y, width, height);
+ }
+
+ function drawText(x, y, text, color, size, alpha) {
+ ctx.fillStyle = `rgba(${color}, ${alpha})`;
+ ctx.font = `${size}px sans-serif`;
+ ctx.fillText(text, x, y);
+ }
+
+ function updateTravelers(delta) {
+ const density = parseFloat(document.getElementById('density').value);
+ const count = Math.floor(20 * density);
+ if (travelers.length !== count) {
+ initTravelers(count);
+ }
+ for (let t of travelers) {
+ t.path.push({ x: t.x, y: t.y });
+ if (t.path.length > 50) t.path.shift();
+ t.angle += t.speed * delta * 0.1;
+ t.x += Math.cos(t.angle) * t.speed * delta;
+ t.y += Math.sin(t.angle) * t.speed * delta;
+ if (t.x < 0) t.x = canvas.width;
+ if (t.x > canvas.width) t.x = 0;
+ if (t.y < 0) t.y = canvas.height;
+ if (t.y > canvas.height) t.y = 0;
+ }
+ }
+
+ function updateAnimals(delta) {
+ const visibility = parseFloat(document.getElementById('animal').value);
+ const count = Math.floor(10 * visibility);
+ if (animalPositions.length !== count) {
+ initAnimals(count);
+ }
+ for (let a of animalPositions) {
+ a.path.push({ x: a.x, y: a.y });
+ if (a.path.length > 50) a.path.shift();
+ a.angle += a.speed * delta * 0.1;
+ a.x += Math.cos(a.angle) * a.speed * delta;
+ a.y += Math.sin(a.angle) * a.speed * delta;
+ if (a.x < 0) a.x = canvas.width;
+ if (a.x > canvas.width) a.x = 0;
+ if (a.y < 0) a.y = canvas.height;
+ if (a.y > canvas.height) a.y = 0;
+ }
+ }
+
+ function drawTravelers() {
+ for (let t of travelers) {
+ if (t.type === 0) {
+ drawCircle(t.x, t.y, 10, 0xff00ff, 0.6);
+ } else if (t.type === 1) {
+ drawTriangle(t.x, t.y, 10, 0x00ffff, 0.6);
+ } else if (t.type === 2) {
+ drawRectangle(t.x, t.y, 15, 10, 0xffff00, 0.6);
+ } else if (t.type === 3) {
+ drawCircle(t.x, t.y, 8, 0x00cc00, 0.6);
+ }
+ }
+ }
+
+ function drawAnimals() {
+ for (let a of animalPositions) {
+ if (a.type === 'bear') {
+ drawCircle(a.x, a.y, 15, a.color, 0.5);
+ } else if (a.type === 'jackrabbit') {
+ drawTriangle(a.x, a.y, 10, a.color, 0.5);
+ } else if (a.type === 'raven') {
+ drawLine(a.x, a.y, a.x + 20, a.y + 20, a.color, 0.5);
+ drawLine(a.x, a.y, a.x - 20, a.y + 20, a.color, 0.5);
+ } else if (a.type === 'fox') {
+ drawLine(a.x, a.y, a.x + 20, a.y, a.color, 0.5);
+ drawLine(a.x, a.y, a.x - 20, a.y, a.color, 0.5);
+ } else if (a.type === 'turtle') {
+ drawLine(a.x, a.y, a.x + 15, a.y + 10, a.color, 0.5);
+ drawLine(a.x, a.y, a.x - 15, a.y + 10, a.color, 0.5);
+ }
+ }
+ }
+
+ function drawTrails() {
+ const trail = parseFloat(document.getElementById('trail').value);
+ for (let t of travelers) {
+ for (let i = 0; i < t.path.length - 1; i++) {
+ const p = t.path[i];
+ const n = t.path[i + 1];
+ const alpha = trail * (1 - i / t.path.length);
+ drawLine(p.x, p.y, n.x, n.y, 0xffffff, alpha);
+ }
+ }
+ for (let a of animalPositions) {
+ for (let i = 0; i < a.path.length - 1; i++) {
+ const p = a.path[i];
+ const n = a.path[i + 1];
+ const alpha = trail * (1 - i / a.path.length);
+ drawLine(p.x, p.y, n.x, n.y, a.color, alpha);
+ }
+ }
+ }
+
+ function drawLandscape(landscape) {
+ const color = landscape.color;
+ const light = landscape.light;
+ const palette = [color, light, 0x333333, 0x666666, 0x999999, 0x000000];
+ ctx.fillStyle = `rgba(0, 0, 0, 1)`;
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
+ for (let i = 0; i < 100; i++) {
+ const x = Math.random() * canvas.width;
+ const y = Math.random() * canvas.height;
+ const size = Math.random() * 30;
+ const alpha = 0.1;
+ ctx.fillStyle = `rgba(${palette[Math.floor(Math.random() * 6)]}, ${alpha})`;
+ ctx.beginPath();
+ ctx.arc(x, y, size, 0, Math.PI * 2);
+ ctx.fill();
+ }
+ }
+
+ function drawTrain(view) {
+ const w = 100;
+ const h = 50;
+ const x = canvas.width / 2;
+ const y = canvas.height / 2;
+ ctx.fillStyle = '#0000aa';
+ ctx.fillRect(x - w / 2, y - h / 2, w, h);
+ ctx.fillStyle = '#0088ff';
+ ctx.fillRect(x - w / 4, y - h / 4, w / 2, h / 2);
+ ctx.fillStyle = '#0000cc';
+ ctx.fillRect(x - w / 4, y - h / 4, w / 2, h / 2);
+ }
+
+ function drawCutaway() {
+ const x = canvas.width / 2;
+ const y = canvas.height / 2;
+ const w = 100;
+ const h = 50;
+ ctx.fillStyle = '#333333';
+ ctx.fillRect(x - w / 2, y - h / 2, w, h);
+ ctx.fillStyle = '#555555';
+ ctx.fillRect(x - w / 2 + 10, y - h / 2 + 10, w - 20, h - 20);
+ ctx.fillStyle = '#ffffff';
+ ctx.font = '12px sans-serif';
+ ctx.fillText('Cutaway', x - 20, y - 10);
+ }
+
+ function drawConductor() {
+ const x = canvas.width / 2;
+ const y = canvas.height / 2;
+ ctx.fillStyle = '#000000';
+ ctx.fillRect(x - 50, y - 25, 100, 50);
+ ctx.fillStyle = '#ffffff';
+ ctx.font = '12px sans-serif';
+ ctx.fillText('Conductor\'s View', x - 30, y + 10);
+ }
+
+ function drawCaption(landscape) {
+ document.getElementById('caption').textContent = landscape.caption;
+ }
+
+ function animate() {
+ if (isPaused) return;
+ const now = performance.now();
+ const delta = now - (lastTime || now);
+ lastTime = now;
+ const view = document.getElementById('view').value;
+ const landscapeControl = document.getElementById('landscape');
+ const landscape = landscapes[landscapeControl.selectedIndex] || landscapes[0];
+ const tempo = parseFloat(document.getElementById('tempo').value);
+ const speed = parseFloat(document.getElementById('speed').value);
+
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
+ drawLandscape(landscape);
+ drawTrain(view);
+ if (view === 'carriage') drawCutaway();
+ if (view === 'conductor') drawConductor();
+ updateTravelers(delta * tempo * speed);
+ updateAnimals(delta * tempo * speed);
+ drawTravelers();
+ drawAnimals();
+ if (showPaths) drawTrails();
+ }
+
+ let lastTime = 0;
+ let animationId = 0;
+ function loop() {
+ if (!isPaused) {
+ animationId = requestAnimationFrame(loop);
+ animate();
+ }
+ }
+
+ function reset() {
+ isPaused = false;
+ showPaths = false;
+ lastTime = performance.now();
+ initTravelers(20);
+ initAnimals(10);
+ loop();
+ }
+
+ window.addEventListener('resize', () => {
+ canvas.width = window.innerWidth;
+ canvas.height = window.innerHeight;
+ });
+
+ document.getElementById('reset').addEventListener('click', reset);
+ document.getElementById('pause').addEventListener('click', () => {
+ isPaused = !isPaused;
+ });
+ document.getElementById('paths').addEventListener('click', () => {
+ showPaths = !showPaths;
+ });
+
+ document.getElementById('landscape').addEventListener('change', (e) => {
+ const landscape = landscapes[e.target.selectedIndex] || landscapes[0];
+ drawCaption(landscape);
+ });
+
+ reset();
+ </script>
+</body>
+</html>
diff --git a/data/artifacts/77fc58739877/qwen3-14b.png b/data/artifacts/77fc58739877/qwen3-14b.png
index 02c6333..5c3cb5c 100644
Binary files a/data/artifacts/77fc58739877/qwen3-14b.png and b/data/artifacts/77fc58739877/qwen3-14b.png differ
diff --git a/data/artifacts/97621618f31c/claude-code.html b/data/artifacts/97621618f31c/claude-code.html
new file mode 100644
index 0000000..53e6fdf
--- /dev/null
+++ b/data/artifacts/97621618f31c/claude-code.html
@@ -0,0 +1,865 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="UTF-8">
+<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
+<title>Terrapin Lantern Observatory — An Animated Surrealist Exhibition</title>
+<style>
+ :root{
+ --ink:#e9e4f5;
+ --dim:#9a93b8;
+ --panel:rgba(14,12,28,0.78);
+ --panelbrd:rgba(150,140,210,0.22);
+ --accent:#e7b96b;
+ --accent2:#7fb7d8;
+ --focus:#ffd98a;
+ --radius:13px;
+ }
+ *{box-sizing:border-box}
+ html,body{margin:0;height:100%;background:#05040d;color:var(--ink);
+ font-family:"Iowan Old Style","Palatino Linotype",Palatino,Georgia,serif;
+ -webkit-font-smoothing:antialiased;overflow:hidden;}
+ #stage{position:fixed;inset:0}
+ canvas{display:block;width:100%;height:100%}
+
+ /* ---------- Header / title ---------- */
+ .titlebar{
+ position:fixed;left:0;right:0;top:0;padding:14px 18px 30px;
+ background:linear-gradient(180deg,rgba(4,3,12,0.72),rgba(4,3,12,0));
+ pointer-events:none;z-index:5;display:flex;flex-direction:column;gap:2px;
+ }
+ .titlebar h1{
+ margin:0;font-size:clamp(15px,2.4vw,25px);font-weight:600;letter-spacing:.14em;
+ text-transform:uppercase;color:var(--ink);text-shadow:0 2px 18px rgba(0,0,0,.8);
+ }
+ .titlebar .sub{font-size:clamp(10px,1.4vw,13px);color:var(--dim);letter-spacing:.06em}
+ .scenechip{
+ display:inline-block;margin-top:6px;font-size:11px;letter-spacing:.18em;text-transform:uppercase;
+ color:var(--accent);opacity:.9;
+ }
+
+ /* ---------- Control dock ---------- */
+ .dock{
+ position:fixed;left:14px;bottom:14px;z-index:10;width:min(340px,calc(100vw - 28px));
+ background:var(--panel);border:1px solid var(--panelbrd);border-radius:var(--radius);
+ backdrop-filter:blur(9px);-webkit-backdrop-filter:blur(9px);
+ padding:12px 13px 13px;box-shadow:0 18px 46px rgba(0,0,0,.55);
+ max-height:calc(100vh - 28px);overflow:auto;transition:transform .35s ease,opacity .35s;
+ }
+ .dock.collapsed{transform:translateY(calc(100% - 46px))}
+ .dock h2{margin:0 0 2px;font-size:12px;letter-spacing:.16em;text-transform:uppercase;color:var(--dim)}
+ .dockhead{display:flex;align-items:center;justify-content:space-between;gap:8px;cursor:pointer}
+ .dockhead .hint{font-size:10px;color:var(--dim);letter-spacing:.05em}
+ .group{margin-top:11px}
+ .glabel{font-size:10px;letter-spacing:.15em;text-transform:uppercase;color:var(--dim);margin-bottom:5px}
+ .row{display:flex;flex-wrap:wrap;gap:6px}
+ button.ctl,.seg button{
+ font-family:inherit;color:var(--ink);background:rgba(40,36,66,0.55);
+ border:1px solid rgba(150,140,210,0.25);border-radius:9px;padding:7px 10px;
+ font-size:12px;letter-spacing:.02em;cursor:pointer;transition:.16s;flex:1 1 auto;
+ min-height:34px;
+ }
+ button.ctl:hover,.seg button:hover{background:rgba(70,62,110,0.7);border-color:rgba(200,190,255,.4)}
+ .seg button.active,button.ctl.active{
+ background:linear-gradient(180deg,rgba(231,185,107,.32),rgba(231,185,107,.14));
+ border-color:var(--accent);color:#fff2d8;
+ }
+ .seg{display:flex;gap:5px}
+ button:focus-visible,input:focus-visible,.dockhead:focus-visible{
+ outline:2px solid var(--focus);outline-offset:2px;border-radius:9px;
+ }
+ .slider{display:flex;align-items:center;gap:9px}
+ .slider input[type=range]{flex:1;accent-color:var(--accent);height:22px}
+ .slider .val{font-size:11px;color:var(--accent2);min-width:64px;text-align:right;letter-spacing:.04em}
+ .toggle-row{display:grid;grid-template-columns:1fr 1fr;gap:6px}
+
+ /* ---------- Info / labels ---------- */
+ .honest{
+ position:fixed;right:14px;bottom:14px;z-index:9;max-width:min(310px,calc(100vw - 28px));
+ background:var(--panel);border:1px solid var(--panelbrd);border-radius:var(--radius);
+ backdrop-filter:blur(9px);-webkit-backdrop-filter:blur(9px);padding:11px 13px;
+ font-size:11.5px;line-height:1.5;color:var(--dim);box-shadow:0 14px 40px rgba(0,0,0,.5);
+ transition:opacity .3s;
+ }
+ .honest b{color:var(--ink);font-weight:600}
+ .honest .close{float:right;cursor:pointer;color:var(--dim);border:none;background:none;font-size:15px;margin:-2px -3px 0 6px}
+ @media (max-width:720px){ .honest{display:none} }
+
+ .toast{
+ position:fixed;left:50%;top:16px;transform:translateX(-50%);z-index:20;
+ background:rgba(10,8,22,.9);border:1px solid var(--panelbrd);border-radius:20px;
+ padding:7px 16px;font-size:12px;letter-spacing:.05em;color:var(--ink);
+ opacity:0;transition:opacity .3s;pointer-events:none;white-space:nowrap;
+ }
+ .toast.show{opacity:1}
+ .kbd{font-size:10px;color:var(--dim);letter-spacing:.05em;margin-top:9px;line-height:1.7}
+ .kbd span{display:inline-block;border:1px solid var(--panelbrd);border-radius:5px;padding:1px 5px;margin-right:2px;color:var(--ink)}
+ a.creditlink{color:var(--accent2);text-decoration:none}
+</style>
+</head>
+<body>
+<div id="stage">
+ <canvas id="c" role="img" aria-label="An open-air observatory riding on the shell of an immense walking cosmic tortoise crossing a midnight desert, with dancers around brass telescopes and animated animal constellations overhead."></canvas>
+</div>
+
+<div class="titlebar">
+ <h1>Terrapin Lantern Observatory</h1>
+ <div class="sub">An animated surrealist exhibition — projection installation, choreographed light</div>
+ <span class="scenechip" id="sceneChip" aria-live="polite">Blue Hour</span>
+</div>
+
+<div class="toast" id="toast" role="status" aria-live="polite"></div>
+
+<div class="honest" id="honest">
+ <button class="close" id="honestClose" aria-label="Dismiss note">×</button>
+ <b>A note on rhythm.</b> Every pulse, sway and orbit here is <b>designed</b>, not derived from live music — no microphone, no audio input, no beat detection. The tortoise's gait is a hand-authored cycle; the dancers keep time to a silent, composed clock.
+</div>
+
+<div class="dock" id="dock" aria-label="Exhibition controls">
+ <div class="dockhead" id="dockHead" role="button" tabindex="0" aria-expanded="true" aria-controls="dockBody">
+ <h2>Controls</h2>
+ <span class="hint" id="dockHint">tap to fold · press H for help</span>
+ </div>
+ <div id="dockBody">
+
+ <div class="group">
+ <div class="glabel">Scene</div>
+ <div class="seg" role="group" aria-label="Scene">
+ <button data-scene="0" class="active">Blue Hour</button>
+ <button data-scene="1">Zenith</button>
+ <button data-scene="2">Rose Dawn</button>
+ </div>
+ </div>
+
+ <div class="group">
+ <div class="glabel">Camera</div>
+ <div class="seg" role="group" aria-label="Camera">
+ <button data-cam="ground" class="active">Ground</button>
+ <button data-cam="shell">Shell</button>
+ <button data-cam="sky">Sky</button>
+ </div>
+ </div>
+
+ <div class="group">
+ <div class="glabel">Visual tempo</div>
+ <div class="slider">
+ <input id="tempo" type="range" min="0" max="200" value="100" aria-label="Visual tempo percent">
+ <span class="val" id="tempoVal">100% · designed</span>
+ </div>
+ </div>
+
+ <div class="group">
+ <div class="glabel">Reveal</div>
+ <div class="toggle-row">
+ <button class="ctl" data-tog="paths">Choreography paths</button>
+ <button class="ctl" data-tog="labels">Constellation labels</button>
+ <button class="ctl" data-tog="people">Isolate people</button>
+ <button class="ctl" data-tog="animals">Isolate animals</button>
+ </div>
+ </div>
+
+ <div class="group">
+ <div class="row">
+ <button class="ctl" id="btnPause">❚❚ Pause</button>
+ <button class="ctl" id="btnReset">↺ Reset</button>
+ <button class="ctl" id="btnStill" aria-pressed="false">Still mode</button>
+ </div>
+ </div>
+
+ <div class="kbd">
+ <span>1</span><span>2</span><span>3</span> scene ·
+ <span>G</span><span>S</span><span>K</span> camera ·
+ <span>Space</span> pause · <span>P</span> paths ·
+ <span>L</span> labels · <span>R</span> reset · <span>M</span> still
+ </div>
+ </div>
+</div>
+
+<script>
+"use strict";
+(function(){
+const cv = document.getElementById('c');
+const ctx = cv.getContext('2d', {alpha:false});
+let W=0,H=0,DPR=1;
+
+function resize(){
+ DPR = Math.min(2, window.devicePixelRatio||1);
+ W = window.innerWidth; H = window.innerHeight;
+ cv.width = Math.floor(W*DPR); cv.height = Math.floor(H*DPR);
+ ctx.setTransform(DPR,0,0,DPR,0,0);
+}
+window.addEventListener('resize', resize); resize();
+
+/* ---------------- State ---------------- */
+const reduceMQ = window.matchMedia('(prefers-reduced-motion: reduce)');
+const state = {
+ scene:0, cam:'ground', tempo:1.0, paused:false, still:false,
+ paths:false, labels:false, isoPeople:false, isoAnimals:false,
+ t:0, // designed clock (seconds, tempo-scaled)
+ trans:1, // scene transition 0..1
+ camTrans:1,
+};
+
+/* Palettes per scene — Blue Hour, Zenith, Rose Dawn */
+const SCENES = [
+ { name:"Blue Hour",
+ sky:['#0a1030','#12224a','#243a63'], horizon:'#3a5a86', dune:'#0c1226', dune2:'#141d3a',
+ lamp:'#ffd27a', star:'#cfe0ff', tortoise:'#0e1430', shellA:'#1d2a55', shellB:'#33528a',
+ accent:'#7fb7d8', density:0.72, tint:'rgba(60,90,150,0.10)' },
+ { name:"Zenith",
+ sky:['#03040f','#070b22','#0d1436'], horizon:'#1a2450', dune:'#070a18', dune2:'#0d1228',
+ lamp:'#ffe6a3', star:'#ffffff', tortoise:'#080c22', shellA:'#141f47', shellB:'#2b459a',
+ accent:'#b39bff', density:1.0, tint:'rgba(30,20,70,0.14)' },
+ { name:"Rose Dawn",
+ sky:['#2a1836','#5a2c46','#a8586a'], horizon:'#e08a72', dune:'#241428', dune2:'#3a2036',
+ lamp:'#fff0c8', star:'#ffe4d0', tortoise:'#1b1026', shellA:'#3a2148', shellB:'#8a4f74',
+ accent:'#f0a27f', density:0.52, tint:'rgba(180,90,110,0.10)' },
+];
+function pal(){ return SCENES[state.scene]; }
+
+/* ---------------- Utilities ---------------- */
+const TAU=Math.PI*2;
+function lerp(a,b,t){return a+(b-a)*t;}
+function mix(c1,c2,t){ // hex mix
+ const a=hx(c1),b=hx(c2);
+ return `rgb(${Math.round(lerp(a[0],b[0],t))},${Math.round(lerp(a[1],b[1],t))},${Math.round(lerp(a[2],b[2],t))})`;
+}
+function hx(h){h=h.replace('#','');return [parseInt(h.substr(0,2),16),parseInt(h.substr(2,2),16),parseInt(h.substr(4,2),16)];}
+function ease(t){return t<.5?2*t*t:1-Math.pow(-2*t+2,2)/2;}
+// deterministic pseudo-random
+function rnd(seed){let x=Math.sin(seed*127.1+31.7)*43758.5453;return x-Math.floor(x);}
+
+/* ---------------- Gait model ----------------
+ The tortoise walk is a hand-authored cycle. It produces a gentle world SWAY
+ (rock + heave) that moves lamps, cloth, shadows and figures — but the CAMERA
+ framing is held perfectly steady so the interface never shakes. */
+function gait(t){
+ const p = t*0.42; // slow footfall cadence
+ const rock = Math.sin(p)*0.9 + Math.sin(p*2+0.6)*0.28; // side-to-side roll (deg-ish)
+ const heave = Math.sin(p*2+1.1)*0.5 + Math.sin(p+0.3)*0.9; // vertical rise/fall
+ const surge = Math.sin(p+0.9)*0.4; // slight forward lurch
+ const step = (Math.sin(p*2)+1)*0.5; // 0..1 footfall phase
+ return {rock, heave, surge, step, p};
+}
+
+/* ---------------- STARFIELD / SKY ---------------- */
+const stars=[];
+function buildStars(){
+ stars.length=0;
+ for(let i=0;i<220;i++){
+ stars.push({x:rnd(i*1.3), y:rnd(i*2.7)*0.72, s:0.4+rnd(i*3.1)*1.7, tw:rnd(i*4.9)*TAU, sp:0.6+rnd(i*5.5)});
+ }
+}
+buildStars();
+
+function drawSky(g){
+ const P=pal();
+ const grad=ctx.createLinearGradient(0,0,0,H);
+ grad.addColorStop(0,P.sky[0]); grad.addColorStop(0.45,P.sky[1]); grad.addColorStop(0.8,P.sky[2]);
+ grad.addColorStop(1,P.horizon);
+ ctx.fillStyle=grad; ctx.fillRect(0,0,W,H);
+
+ // subtle nebula floral-geometry wash
+ const cx=W*0.5, cy=H*0.32;
+ for(let k=0;k<3;k++){
+ const rad=Math.min(W,H)*(0.35+k*0.18);
+ const gg=ctx.createRadialGradient(cx+Math.sin(g.p+k)*30,cy,0,cx,cy,rad);
+ gg.addColorStop(0,P.tint); gg.addColorStop(1,'rgba(0,0,0,0)');
+ ctx.fillStyle=gg; ctx.fillRect(0,0,W,H);
+ }
+ // stars
+ const dimFactor = state.isoPeople?0.18:1;
+ for(const s of stars){
+ const x=s.x*W, y=s.y*H;
+ const tw = state.still?0.8:(0.55+0.45*Math.sin(g.p*s.sp+s.tw));
+ ctx.globalAlpha=tw*0.9*dimFactor;
+ ctx.fillStyle=P.star;
+ ctx.beginPath(); ctx.arc(x,y,s.s*0.9,0,TAU); ctx.fill();
+ }
+ ctx.globalAlpha=1;
+}
+
+/* ---------------- CONSTELLATION ANIMALS ----------------
+ Original abstract animal constellations: hares leap, owls orbit the dome,
+ coyotes bow & turn, tiny bears waltz along the rim. Points are stylised. */
+const HARE=[[0,0],[.6,-.5],[1.1,-.4],[1.5,-.9],[1.9,-.5],[1.4,.1],[.9,.35],[.4,.4]];
+const OWL=[[0,0],[.5,-.7],[1,-.7],[1.5,0],[1.2,.6],[.6,.85],[0,.6],[-.3,-.05],[.75,-.25]];
+const COYOTE=[[0,0],[.5,-.35],[1.1,-.2],[1.7,-.55],[2.1,-.1],[1.6,.35],[1.0,.45],[.45,.55],[0,.4]];
+const BEAR=[[0,0],[.45,-.5],[.95,-.55],[1.35,-.15],[1.1,.4],[.55,.55],[.05,.4]];
+
+function makeConstellations(){
+ const arr=[];
+ // hares — leaping across upper sky
+ for(let i=0;i<3;i++){
+ arr.push({type:'hare',pts:HARE,name:'The Leaping Hare',x:0.16+i*0.30,y:0.16+ (i%2)*0.05,
+ scale:0.055,phase:i*2.1,motion:'leap',hue:pal().accent});
+ }
+ // owls orbit the dome
+ for(let i=0;i<2;i++){
+ arr.push({type:'owl',pts:OWL,name:'The Orbiting Owl',x:0.5,y:0.3,scale:0.05,phase:i*Math.PI,motion:'orbit',
+ orbR:0.16+i*0.06,hue:pal().accent});
+ }
+ // coyotes bow and turn near horizon
+ for(let i=0;i<2;i++){
+ arr.push({type:'coyote',pts:COYOTE,name:'The Bowing Coyote',x:0.30+i*0.42,y:0.42,scale:0.05,
+ phase:i*1.7,motion:'bow',hue:pal().accent});
+ }
+ // tiny bears waltz along shell rim (positioned relative later)
+ for(let i=0;i<4;i++){
+ arr.push({type:'bear',pts:BEAR,name:'The Waltzing Bear',rim:i/4,scale:0.028,phase:i*1.3,motion:'waltz',hue:pal().accent});
+ }
+ return arr;
+}
+let CONST=makeConstellations();
+
+function drawConstellation(c,g){
+ const P=pal();
+ let ox,oy,rot=0,sc=Math.min(W,H)*c.scale;
+ const beat=g.p;
+ if(c.motion==='orbit'){
+ const a=beat*0.25+c.phase;
+ ox=W*(0.5+Math.cos(a)*c.orbR); oy=H*(0.30+Math.sin(a)*c.orbR*0.55);
+ rot=Math.sin(a)*0.3;
+ } else if(c.motion==='leap'){
+ const cyc=(beat*0.5+c.phase)%TAU;
+ const hop=Math.abs(Math.sin(cyc))* -0.05;
+ ox=W*(c.x + ((beat*0.02+c.phase)%1)*0.0); oy=H*(c.y+hop);
+ // drift slowly rightward and wrap
+ ox=W*((c.x + (beat*0.012)) %1.1 -0.05);
+ rot=Math.sin(cyc)*0.12;
+ sc*= 1+Math.max(0,Math.sin(cyc))*0.06;
+ } else if(c.motion==='bow'){
+ ox=W*c.x; oy=H*c.y + Math.sin(beat*0.5+c.phase)*6;
+ rot=Math.sin(beat*0.6+c.phase)*0.22 - 0.05;
+ } else if(c.motion==='waltz'){
+ // placed along the shell rim in world; handled in shell draw — skip here
+ return;
+ }
+ drawStarFigure(c,ox,oy,sc,rot,g);
+}
+
+function drawStarFigure(c,ox,oy,sc,rot,g){
+ const P=pal();
+ const tw=state.still?1:(0.6+0.4*Math.sin(g.p*1.4+c.phase));
+ ctx.save();
+ ctx.translate(ox,oy); ctx.rotate(rot);
+ // connecting lines
+ ctx.strokeStyle = `rgba(${hx(P.accent).join(',')},${0.32*tw})`;
+ ctx.lineWidth=1; ctx.beginPath();
+ for(let i=0;i<c.pts.length;i++){
+ const px=c.pts[i][0]*sc, py=c.pts[i][1]*sc;
+ if(i===0)ctx.moveTo(px,py); else ctx.lineTo(px,py);
+ }
+ ctx.stroke();
+ // stars
+ for(let i=0;i<c.pts.length;i++){
+ const px=c.pts[i][0]*sc, py=c.pts[i][1]*sc;
+ const r=(i===0?2.6:1.7)*(0.8+0.3*tw);
+ ctx.fillStyle=P.star;
+ ctx.shadowColor=P.accent; ctx.shadowBlur=8*tw;
+ ctx.beginPath(); ctx.arc(px,py,r,0,TAU); ctx.fill();
+ }
+ ctx.shadowBlur=0;
+ if(state.labels){
+ ctx.rotate(-rot);
+ ctx.fillStyle=`rgba(${hx(P.accent).join(',')},0.9)`;
+ ctx.font='11px Georgia'; ctx.textAlign='center';
+ ctx.fillText(c.name, 0, sc*0.9+16);
+ }
+ ctx.restore();
+}
+
+/* ---------------- DUNES / DESERT ---------------- */
+function drawDunes(g){
+ const P=pal();
+ const baseY=H*0.72;
+ ctx.fillStyle=P.dune2;
+ ctx.beginPath(); ctx.moveTo(0,H);
+ for(let x=0;x<=W;x+=24){
+ const y=baseY+Math.sin(x*0.006 + 1.2)*18 + Math.sin(x*0.0016)*40 + 30;
+ ctx.lineTo(x,y);
+ }
+ ctx.lineTo(W,H); ctx.closePath(); ctx.fill();
+
+ ctx.fillStyle=P.dune;
+ ctx.beginPath(); ctx.moveTo(0,H);
+ const drift = g.p*6; // sand slowly passing as tortoise walks
+ for(let x=0;x<=W;x+=20){
+ const y=baseY+70+Math.sin((x+drift)*0.008)*24 + Math.sin((x-drift)*0.003)*46;
+ ctx.lineTo(x,y);
+ }
+ ctx.lineTo(W,H); ctx.closePath(); ctx.fill();
+}
+
+/* ---------------- HUMAN FIGURES ----------------
+ Four choreographies: nested spinning circles, pairs exchanging along curves,
+ children tracing star shapes with lanterns, robed mirrored gestures. */
+function stickFigure(x,y,scale,pose,color,lantern,g,phase){
+ ctx.save(); ctx.translate(x,y); ctx.scale(scale,scale);
+ const sway=Math.sin(g.p*1.5+phase)*0.08;
+ ctx.rotate(sway*0.4);
+ ctx.strokeStyle=color; ctx.lineWidth=2.4; ctx.lineCap='round';
+ const armSwing=Math.sin(g.p*2+phase)*(pose==='mirror'?0.9:0.6);
+ const legSwing=Math.sin(g.p*2+phase+Math.PI)*0.5;
+ // body
+ ctx.beginPath();
+ ctx.moveTo(0,-14); ctx.lineTo(0,4); // torso
+ ctx.stroke();
+ // head
+ ctx.beginPath(); ctx.arc(0,-19,4.2,0,TAU); ctx.fillStyle=color; ctx.fill();
+ // robe (for mirror figures) — cloth reacts to gait heave
+ if(pose==='robe'||pose==='mirror'){
+ const flare=6+g.heave*1.2;
+ ctx.beginPath(); ctx.moveTo(0,-12);
+ ctx.quadraticCurveTo(-flare-legSwing*6,10,-flare*1.4,20+g.heave);
+ ctx.lineTo(flare*1.4,20-g.heave);
+ ctx.quadraticCurveTo(flare+legSwing*6,10,0,-12);
+ ctx.closePath();
+ ctx.globalAlpha=0.28; ctx.fillStyle=color; ctx.fill(); ctx.globalAlpha=1;
+ } else {
+ // legs
+ ctx.beginPath();
+ ctx.moveTo(0,4); ctx.lineTo(-4-legSwing*6,20);
+ ctx.moveTo(0,4); ctx.lineTo(4+legSwing*6,20);
+ ctx.stroke();
+ }
+ // arms
+ ctx.beginPath();
+ ctx.moveTo(0,-9); ctx.lineTo(-9-armSwing*7,-4+armSwing*4);
+ ctx.moveTo(0,-9); ctx.lineTo(9+armSwing*7,-4-armSwing*4);
+ ctx.stroke();
+ // lantern in hand
+ if(lantern){
+ const lx=9+armSwing*7, ly=-4-armSwing*4;
+ ctx.fillStyle=pal().lamp; ctx.shadowColor=pal().lamp; ctx.shadowBlur=14;
+ ctx.beginPath(); ctx.arc(lx,ly,3.1,0,TAU); ctx.fill(); ctx.shadowBlur=0;
+ }
+ ctx.restore();
+}
+
+function drawDancers(deck, g){
+ const P=pal();
+ const dens = pal().density * (reduceMQ.matches?0.7:1);
+ const cx=deck.cx, topY=deck.topY, dscale=deck.scale;
+
+ // ---- helper to draw a choreography path (revealed) ----
+ function path(fn){
+ if(!state.paths)return;
+ ctx.save();
+ ctx.strokeStyle=`rgba(${hx(P.accent).join(',')},0.5)`;
+ ctx.setLineDash([4,5]); ctx.lineWidth=1.2;
+ ctx.beginPath(); fn(); ctx.stroke(); ctx.setLineDash([]); ctx.restore();
+ }
+
+ // GROUP 1 — nested spinning circles (three rings)
+ const g1x=cx-140*dscale, g1y=topY-4;
+ for(let ring=0;ring<3;ring++){
+ const R=(18+ring*15)*dscale;
+ const n=4+ring*2;
+ path(()=>{ctx.ellipse(g1x,g1y,R,R*0.42,0,0,TAU);});
+ for(let i=0;i<n;i++){
+ const a=g.p*(ring%2?-0.5:0.6)+ (i/n)*TAU;
+ const px=g1x+Math.cos(a)*R, py=g1y+Math.sin(a)*R*0.42;
+ stickFigure(px,py-6,0.7*dscale,'stand',mix(P.star,P.accent,0.4),false,g,i+ring*3);
+ }
+ }
+
+ // GROUP 2 — pairs exchange positions along curved paths
+ const g2x=cx+120*dscale, g2y=topY-2;
+ for(let pr=0;pr<3;pr++){
+ const off=pr*26*dscale;
+ const swap=(Math.sin(g.p*0.5+pr)+1)*0.5; // 0..1 exchange
+ path(()=>{
+ ctx.moveTo(g2x-40*dscale, g2y-off);
+ ctx.quadraticCurveTo(g2x, g2y-off-30*dscale, g2x+40*dscale, g2y-off);
+ });
+ const ax=lerp(g2x-40*dscale, g2x+40*dscale, swap);
+ const ay=g2y-off - Math.sin(swap*Math.PI)*26*dscale;
+ const bx=lerp(g2x+40*dscale, g2x-40*dscale, swap);
+ const by=g2y-off - Math.sin((1-swap)*Math.PI)*26*dscale;
+ stickFigure(ax,ay,0.72*dscale,'stand',mix(P.star,P.accent2,0.5),false,g,pr*2);
+ stickFigure(bx,by,0.72*dscale,'stand',mix(P.star,P.accent2,0.5),false,g,pr*2+1);
+ }
+
+ // GROUP 3 — children tracing star shapes with lanterns
+ const g3x=cx, g3y=topY+30*dscale;
+ const starPath=[];
+ for(let i=0;i<=5;i++){const a=-Math.PI/2 + i*(TAU*2/5); starPath.push([Math.cos(a),Math.sin(a)]);}
+ const R3=30*dscale;
+ path(()=>{ for(let i=0;i<starPath.length;i++){const p=starPath[i]; const x=g3x+p[0]*R3,y=g3y+p[1]*R3*0.5; i?ctx.lineTo(x,y):ctx.moveTo(x,y);} });
+ for(let ch=0;ch<3;ch++){
+ const prog=((g.p*0.28+ch/3)%1)*5; // travels the 5-point star
+ const seg=Math.floor(prog), fr=prog-seg;
+ const a=starPath[seg], b=starPath[(seg+1)%5];
+ const px=g3x+lerp(a[0],b[0],fr)*R3;
+ const py=g3y+lerp(a[1],b[1],fr)*R3*0.5;
+ stickFigure(px,py,0.5*dscale,'stand',P.star,true,g,ch*3+1);
+ }
+
+ // GROUP 4 — robed figures, slow mirrored gestures
+ const g4x=cx, g4y=topY-40*dscale;
+ for(let m=0;m<2;m++){
+ const dir=m===0?-1:1;
+ const px=g4x+dir*46*dscale;
+ stickFigure(px,g4y,0.9*dscale,'mirror',mix(P.accent,P.star,0.2),false,
+ {p:g.p, heave:g.heave*dir}, m*Math.PI); // mirrored phase
+ }
+}
+
+/* ---------------- BRASS TELESCOPES ---------------- */
+function drawTelescope(x,y,s,g,phase){
+ const P=pal();
+ ctx.save(); ctx.translate(x,y);
+ // tripod
+ ctx.strokeStyle=mix('#8a6a34','#d8a94e',0.4); ctx.lineWidth=2.2*s; ctx.lineCap='round';
+ ctx.beginPath();
+ ctx.moveTo(0,0); ctx.lineTo(-7*s,16*s);
+ ctx.moveTo(0,0); ctx.lineTo(7*s,16*s);
+ ctx.moveTo(0,0); ctx.lineTo(0,17*s);
+ ctx.stroke();
+ // barrel — tilts gently to track the sky
+ const tilt=-0.9 + Math.sin(g.p*0.4+phase)*0.14;
+ ctx.rotate(tilt);
+ const grad=ctx.createLinearGradient(0,-2*s,26*s,-2*s);
+ grad.addColorStop(0,'#6a4e22'); grad.addColorStop(0.5,'#e9c濁'.replace('濁','b76')); grad.addColorStop(1,'#8a6a34');
+ ctx.fillStyle=grad;
+ ctx.beginPath(); ctx.roundRect? ctx.roundRect(-2*s,-3*s,28*s,6*s,3*s):ctx.rect(-2*s,-3*s,28*s,6*s);
+ ctx.fill();
+ ctx.fillStyle=mix('#3a2a12','#d8a94e',0.5);
+ ctx.beginPath(); ctx.arc(28*s,0,4.2*s,0,TAU); ctx.fill();
+ ctx.restore();
+}
+
+/* ---------------- THE TORTOISE + SHELL OBSERVATORY ---------------- */
+function drawTortoise(g){
+ const P=pal();
+ // camera-independent world sway from gait (interface stays steady)
+ const baseX=W*0.5, baseY=H*0.66;
+ const sway = state.still?0:g.rock;
+ const heave= state.still?0:g.heave;
+
+ // --- camera framing (steady): choose focus & zoom, no shake ---
+ let camY=0, zoom=1;
+ if(state.cam==='ground'){ camY=0; zoom=1.0; }
+ else if(state.cam==='shell'){ camY=-H*0.10; zoom=1.32; }
+ else if(state.cam==='sky'){ camY=H*0.12; zoom=0.86; }
+
+ ctx.save();
+ ctx.translate(W*0.5, H*0.5);
+ ctx.scale(zoom,zoom);
+ ctx.translate(-W*0.5, -H*0.5 + camY);
+
+ const originX=baseX, originY=baseY+heave*1.6;
+
+ // ---- shadow on sand (reacts to heave) ----
+ ctx.save();
+ ctx.translate(originX, baseY+120);
+ ctx.scale(1, 0.3);
+ const shg=ctx.createRadialGradient(0,0,10,0,0,300+heave*8);
+ shg.addColorStop(0,'rgba(0,0,0,0.45)'); shg.addColorStop(1,'rgba(0,0,0,0)');
+ ctx.fillStyle=shg; ctx.beginPath(); ctx.arc(0,0,300,0,TAU); ctx.fill();
+ ctx.restore();
+
+ ctx.save();
+ ctx.translate(originX, originY);
+ ctx.rotate(sway*0.012); // whole creature rolls subtly with the gait
+
+ // ---- LEGS (four columns, walk cycle) ----
+ const legColor=mix(P.tortoise,'#000',0.2);
+ for(let i=0;i<4;i++){
+ const lx=(i<2?-1:1)*(150 - (i%2)*60);
+ const lift = Math.max(0, Math.sin(g.p*2 + (i%2?Math.PI:0) + (i<2?0:Math.PI)))*10;
+ ctx.fillStyle=legColor;
+ ctx.save(); ctx.translate(lx, 60-lift*0.5);
+ ctx.beginPath();
+ (ctx.roundRect? ctx.roundRect(-26,0,52,80-lift,16): ctx.rect(-26,0,52,80-lift));
+ ctx.fill();
+ // toe glimmer
+ ctx.fillStyle=mix(P.shellA,P.star,0.15);
+ ctx.beginPath(); ctx.ellipse(0,80-lift,26,10,0,0,Math.PI); ctx.fill();
+ ctx.restore();
+ }
+
+ // ---- HEAD & NECK (front-left), slow bob ----
+ ctx.save();
+ const neckB=Math.sin(g.p*0.6)*8;
+ ctx.translate(-230, -10+neckB*0.3);
+ ctx.fillStyle=mix(P.tortoise,P.shellA,0.3);
+ ctx.beginPath();
+ ctx.moveTo(0,20); ctx.quadraticCurveTo(-70,0,-120,-30-neckB);
+ ctx.quadraticCurveTo(-140,-46-neckB,-150,-40-neckB);
+ ctx.quadraticCurveTo(-120,10,-40,44); ctx.closePath(); ctx.fill();
+ // eye — a small star
+ ctx.fillStyle=P.star; ctx.shadowColor=P.accent; ctx.shadowBlur=10;
+ ctx.beginPath(); ctx.arc(-132,-30-neckB,3.2,0,TAU); ctx.fill(); ctx.shadowBlur=0;
+ ctx.restore();
+
+ // ---- SHELL BODY (dome carrying the observatory) ----
+ const shellGrad=ctx.createLinearGradient(0,-160,0,80);
+ shellGrad.addColorStop(0,P.shellB); shellGrad.addColorStop(0.6,P.shellA); shellGrad.addColorStop(1,P.tortoise);
+ ctx.fillStyle=shellGrad;
+ ctx.beginPath();
+ ctx.moveTo(-260,60);
+ ctx.bezierCurveTo(-260,-150,260,-150,260,60);
+ ctx.bezierCurveTo(160,90,-160,90,-260,60);
+ ctx.closePath(); ctx.fill();
+
+ // shell rim highlight
+ ctx.strokeStyle=`rgba(${hx(P.star).join(',')},0.25)`; ctx.lineWidth=2;
+ ctx.beginPath(); ctx.moveTo(-256,58);
+ ctx.bezierCurveTo(-256,-146,256,-146,256,58); ctx.stroke();
+
+ // ---- Shell plates: abstract floral geometry (original motif) ----
+ ctx.save();
+ ctx.beginPath();
+ ctx.moveTo(-260,60);
+ ctx.bezierCurveTo(-260,-150,260,-150,260,60);
+ ctx.bezierCurveTo(160,90,-160,90,-260,60); ctx.closePath();
+ ctx.clip();
+ ctx.strokeStyle=`rgba(${hx(P.star).join(',')},0.16)`; ctx.lineWidth=1.4;
+ // radiating petal cells
+ const cxs=0, cys=40;
+ for(let ring=1;ring<=4;ring++){
+ const rr=ring*46;
+ const petals=6+ring*2;
+ for(let i=0;i<petals;i++){
+ const a=(i/petals)*TAU + ring*0.2 + (state.still?0:g.p*0.02);
+ const x=cxs+Math.cos(a)*rr, y=cys - Math.abs(Math.sin(a))*rr*0.9;
+ ctx.beginPath();
+ ctx.ellipse(x,y,20,11,a,0,TAU); ctx.stroke();
+ }
+ }
+ // faint constellation studs glimmering in the plates
+ for(let i=0;i<14;i++){
+ const a=rnd(i)*TAU, r=40+rnd(i*2)*180;
+ const x=Math.cos(a)*r, y=40-Math.abs(Math.sin(a))*r*0.8;
+ ctx.fillStyle=`rgba(${hx(P.lamp).join(',')},${state.still?0.5:0.3+0.3*Math.sin(g.p*1.3+i)})`;
+ ctx.beginPath(); ctx.arc(x,y,1.6,0,TAU); ctx.fill();
+ }
+ ctx.restore();
+
+ /* ======== OBSERVATORY DECK on the shell ======== */
+ const deckTopY = -120;
+ // deck platform
+ ctx.fillStyle=mix(P.tortoise,P.shellA,0.6);
+ ctx.beginPath(); ctx.ellipse(0,deckTopY+8,215,34,0,0,TAU); ctx.fill();
+ ctx.fillStyle=`rgba(${hx(P.star).join(',')},0.08)`;
+ ctx.beginPath(); ctx.ellipse(0,deckTopY+4,205,28,0,0,Math.PI,true); ctx.fill();
+
+ // ---- Hanging lamps (pendulums that swing with the gait) ----
+ if(!state.isoAnimals){
+ const lampCount=5;
+ for(let i=0;i<lampCount;i++){
+ const lx=-150+i*75;
+ // pendulum: gait rock drives the swing; each lamp slightly phase-shifted
+ const swing = state.still?0.02: (Math.sin(g.p*1.1 - i*0.5)*0.16 + g.rock*0.02);
+ const armLen=46;
+ const ax=lx, ay=deckTopY-70;
+ const bx=ax+Math.sin(swing)*armLen, by=ay+Math.cos(swing)*armLen;
+ ctx.strokeStyle=`rgba(${hx(P.star).join(',')},0.4)`; ctx.lineWidth=1.2;
+ ctx.beginPath(); ctx.moveTo(ax,ay); ctx.lineTo(bx,by); ctx.stroke();
+ // lamp glow
+ const fl=state.still?1:(0.75+0.25*Math.sin(g.p*3+i));
+ const lg=ctx.createRadialGradient(bx,by,0,bx,by,30);
+ lg.addColorStop(0,`rgba(${hx(P.lamp).join(',')},${0.9*fl})`);
+ lg.addColorStop(1,'rgba(0,0,0,0)');
+ ctx.fillStyle=lg; ctx.beginPath(); ctx.arc(bx,by,30,0,TAU); ctx.fill();
+ ctx.fillStyle=P.lamp; ctx.beginPath(); ctx.arc(bx,by,4.5,0,TAU); ctx.fill();
+ }
+ }
+
+ // ---- Cloth banners strung over the deck (react to gait heave) ----
+ if(!state.isoAnimals){
+ ctx.fillStyle=`rgba(${hx(P.accent).join(',')},0.20)`;
+ for(let b=0;b<2;b++){
+ const y0=deckTopY-30-b*8;
+ ctx.beginPath();
+ ctx.moveTo(-200+b*10,y0);
+ for(let x=-200+b*10;x<=200-b*10;x+=16){
+ const droop=Math.sin((x)*0.02 + g.p*(state.still?0:2)+b)* (6+heave*0.6) + 16;
+ ctx.lineTo(x,y0+droop);
+ }
+ ctx.lineTo(200-b*10,y0);
+ ctx.closePath(); ctx.fill();
+ }
+ }
+
+ // ---- Telescopes on deck ----
+ if(!state.isoAnimals){
+ drawTelescope(-120,deckTopY+6,1.0,g,0);
+ drawTelescope(0,deckTopY-2,1.15,g,1.4);
+ drawTelescope(120,deckTopY+6,1.0,g,2.6);
+ }
+
+ // ---- Human dancers ----
+ if(!state.isoAnimals){
+ drawDancers({cx:0, topY:deckTopY-6, scale:1.0}, g);
+ }
+
+ // ---- tiny bears waltzing along the shell rim ----
+ if(!state.isoPeople){
+ const rimN=CONST.filter(c=>c.type==='bear');
+ let bi=0;
+ for(const c of CONST){
+ if(c.type!=='bear')continue;
+ const a = -Math.PI + ( (c.rim + (state.still?0:g.p*0.05)) %1 )*Math.PI; // travel around dome top
+ const rr=250;
+ const bx=Math.cos(a)*rr, by=-140 + Math.sin(a)*-90 + Math.sin(g.p*3+c.phase)*4;
+ drawStarFigure(c,bx,by,Math.min(W,H)*c.scale,Math.sin(g.p*2+c.phase)*0.3,g);
+ bi++;
+ }
+ }
+
+ ctx.restore(); // creature
+ ctx.restore(); // camera
+}
+
+/* ---------------- COMPOSITE FRAME ---------------- */
+let last=performance.now();
+function frame(now){
+ const dtReal=Math.min(0.05,(now-last)/1000); last=now;
+ if(!state.paused && !state.still){
+ state.t += dtReal*state.tempo;
+ }
+ if(state.trans<1) state.trans=Math.min(1,state.trans+dtReal*2.2);
+
+ const g = gait(state.t);
+
+ drawSky(g);
+
+ // animal constellations behind/around dome (unless isolating people)
+ if(!state.isoPeople){
+ for(const c of CONST){ if(c.type!=='bear') drawConstellation(c,g); }
+ }
+
+ drawDunes(g);
+ drawTortoise(g);
+
+ // scene tint film for projection feel
+ const P=pal();
+ ctx.fillStyle=P.tint; ctx.fillRect(0,0,W,H);
+ // vignette
+ const vg=ctx.createRadialGradient(W/2,H/2,Math.min(W,H)*0.3,W/2,H/2,Math.max(W,H)*0.75);
+ vg.addColorStop(0,'rgba(0,0,0,0)'); vg.addColorStop(1,'rgba(0,0,0,0.5)');
+ ctx.fillStyle=vg; ctx.fillRect(0,0,W,H);
+
+ if(state.still){
+ ctx.fillStyle='rgba(0,0,0,0.35)';
+ ctx.fillRect(W-0,0,0,0);
+ }
+
+ requestAnimationFrame(frame);
+}
+requestAnimationFrame(frame);
+
+/* ---------------- CONTROLS WIRING ---------------- */
+const toastEl=document.getElementById('toast');
+let toastTimer;
+function toast(msg){
+ toastEl.textContent=msg; toastEl.classList.add('show');
+ clearTimeout(toastTimer); toastTimer=setTimeout(()=>toastEl.classList.remove('show'),1500);
+}
+
+function setScene(i){
+ state.scene=i; state.trans=0;
+ CONST=makeConstellations();
+ document.querySelectorAll('[data-scene]').forEach(b=>b.classList.toggle('active',+b.dataset.scene===i));
+ const chip=document.getElementById('sceneChip'); chip.textContent=SCENES[i].name;
+ // adjust dancer density feel via density already in palette
+ toast(SCENES[i].name + (i===0?' — soft & sparse':i===1?' — dense & radiant':' — warm & unhurried'));
+}
+function setCam(c){
+ state.cam=c;
+ document.querySelectorAll('[data-cam]').forEach(b=>b.classList.toggle('active',b.dataset.cam===c));
+ toast(c.charAt(0).toUpperCase()+c.slice(1)+' camera');
+}
+function setTog(key){
+ const map={paths:'paths',labels:'labels',people:'isoPeople',animals:'isoAnimals'};
+ const s=map[key]; state[s]=!state[s];
+ // people/animals isolate are mutually informative but allowed together
+ document.querySelector(`[data-tog="${key}"]`).classList.toggle('active',state[s]);
+ const names={paths:'Choreography paths',labels:'Constellation labels',people:'Isolate people',animals:'Isolate animals'};
+ toast(names[key]+': '+(state[s]?'on':'off'));
+}
+
+document.querySelectorAll('[data-scene]').forEach(b=>b.addEventListener('click',()=>setScene(+b.dataset.scene)));
+document.querySelectorAll('[data-cam]').forEach(b=>b.addEventListener('click',()=>setCam(b.dataset.cam)));
+document.querySelectorAll('[data-tog]').forEach(b=>b.addEventListener('click',()=>setTog(b.dataset.tog)));
+
+const tempo=document.getElementById('tempo'), tempoVal=document.getElementById('tempoVal');
+tempo.addEventListener('input',()=>{
+ state.tempo=+tempo.value/100;
+ tempoVal.textContent=`${tempo.value}% · designed`;
+});
+
+const btnPause=document.getElementById('btnPause');
+function togglePause(){
+ state.paused=!state.paused;
+ btnPause.textContent=state.paused?'▶ Resume':'❚❚ Pause';
+ btnPause.classList.toggle('active',state.paused);
+ toast(state.paused?'Paused':'Playing');
+}
+btnPause.addEventListener('click',togglePause);
+
+const btnStill=document.getElementById('btnStill');
+function toggleStill(force){
+ state.still = (force!==undefined)?force:!state.still;
+ btnStill.classList.toggle('active',state.still);
+ btnStill.setAttribute('aria-pressed',state.still);
+ btnStill.textContent=state.still?'Still mode ✓':'Still mode';
+ toast(state.still?'Composed still mode — motion held':'Motion resumed');
+}
+btnStill.addEventListener('click',()=>toggleStill());
+
+document.getElementById('btnReset').addEventListener('click',()=>{
+ state.t=0; state.tempo=1; tempo.value=100; tempoVal.textContent='100% · designed';
+ state.paused=false; btnPause.textContent='❚❚ Pause'; btnPause.classList.remove('active');
+ state.paths=state.labels=state.isoPeople=state.isoAnimals=false;
+ document.querySelectorAll('[data-tog]').forEach(b=>b.classList.remove('active'));
+ setScene(0); setCam('ground'); toggleStill(reduceMQ.matches);
+ toast('Reset');
+});
+
+/* dock collapse */
+const dock=document.getElementById('dock'), dockHead=document.getElementById('dockHead');
+function toggleDock(){
+ const c=dock.classList.toggle('collapsed');
+ dockHead.setAttribute('aria-expanded',!c);
+}
+dockHead.addEventListener('click',toggleDock);
+dockHead.addEventListener('keydown',e=>{if(e.key==='Enter'||e.key===' '){e.preventDefault();toggleDock();}});
+
+document.getElementById('honestClose').addEventListener('click',()=>{
+ document.getElementById('honest').style.display='none';
+});
+
+/* keyboard */
+window.addEventListener('keydown',e=>{
+ if(e.target.tagName==='INPUT')return;
+ switch(e.key.toLowerCase()){
+ case '1':setScene(0);break;
+ case '2':setScene(1);break;
+ case '3':setScene(2);break;
+ case 'g':setCam('ground');break;
+ case 's':setCam('shell');break;
+ case 'k':setCam('sky');break;
+ case 'p':setTog('paths');break;
+ case 'l':setTog('labels');break;
+ case 'o':setTog('people');break;
+ case 'a':setTog('animals');break;
+ case 'r':document.getElementById('btnReset').click();break;
+ case 'm':toggleStill();break;
+ case 'h':toggleDock();toast('Controls — fold/unfold');break;
+ case ' ':e.preventDefault();togglePause();break;
+ }
+});
+
+/* prefers-reduced-motion → boot into composed still mode */
+if(reduceMQ.matches){ toggleStill(true); }
+reduceMQ.addEventListener?.('change',e=>{ if(e.matches) toggleStill(true); });
+
+setScene(0);
+})();
+</script>
+</body>
+</html>
\ No newline at end of file
diff --git a/data/artifacts/97621618f31c/qwen25-7b.html b/data/artifacts/97621618f31c/qwen25-7b.html
new file mode 100644
index 0000000..25ef066
--- /dev/null
+++ b/data/artifacts/97621618f31c/qwen25-7b.html
@@ -0,0 +1,146 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>Terrapin Lantern Observatory</title>
+ <style>
+ body, html {
+ margin: 0;
+ padding: 0;
+ width: 100%;
+ height: 100%;
+ overflow: hidden;
+ background-color: #000;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ font-family: Arial, sans-serif;
+ }
+ canvas {
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ pointer-events: none;
+ }
+ .controls {
+ position: fixed;
+ bottom: 20px;
+ left: 50%;
+ transform: translateX(-50%);
+ display: flex;
+ gap: 16px;
+ z-index: 1000;
+ }
+ button, label {
+ cursor: pointer;
+ padding: 8px 12px;
+ font-size: 14px;
+ border-radius: 4px;
+ background-color: rgba(255, 255, 255, 0.3);
+ color: white;
+ }
+ button:hover {
+ background-color: rgba(255, 255, 255, 0.5);
+ }
+ </style>
+</head>
+<body>
+ <canvas id="canvas"></canvas>
+ <div class="controls">
+ <button onclick="changeCamera('ground')">Ground Cam</button>
+ <button onclick="changeCamera('shell')">Shell Cam</button>
+ <button onclick="changeCamera('sky')">Sky Cam</button>
+ <label><input type="checkbox" onchange="toggleConstellationLabels()">Toggle Constellations</label>
+ <button onclick="setTempo(1)">Normal</button>
+ <button onclick="setTempo(2)">Faster</button>
+ <button onclick="setTempo(0.5)">Slower</button>
+ <button onclick="pause()">Pause</button>
+ <button onclick="reset()">Reset</button>
+ <label><input type="checkbox" onchange="reduceMotion()">Reduce Motion</label>
+ </div>
+
+ <script>
+ const canvas = document.getElementById('canvas');
+ let ctx = null;
+ let framesPerSecond = 60;
+ let animationFrameId = null;
+
+ function setupCanvas() {
+ if (ctx) return;
+ canvas.width = window.innerWidth;
+ canvas.height = window.innerHeight;
+ ctx = canvas.getContext('2d');
+ init();
+ }
+
+ function init() {
+ // Initialize your scene here
+ }
+
+ function animate() {
+ requestAnimationFrame(() => {
+ drawFrame();
+ animationFrameId = requestAnimationFrame(animate);
+ });
+ }
+
+ function drawFrame() {
+ clearCanvas();
+ setupScene();
+ updateScene();
+ drawScene();
+ }
+
+ function clearCanvas() {
+ ctx.fillStyle = 'black';
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
+ }
+
+ function setupScene() {
+ // Set up your scene here
+ }
+
+ function updateScene() {
+ // Update your scene elements here based on time and motion
+ }
+
+ function drawScene() {
+ // Draw your scene elements here
+ }
+
+ function changeCamera(cameraType) {
+ console.log(`Switching to ${cameraType} camera`);
+ }
+
+ function setTempo(tempo) {
+ framesPerSecond = tempo * 60;
+ if (animationFrameId) cancelAnimationFrame(animationFrameId);
+ animationFrameId = requestAnimationFrame(animate);
+ }
+
+ function pause() {
+ if (animationFrameId) cancelAnimationFrame(animationFrameId);
+ console.log('Paused');
+ }
+
+ function reset() {
+ // Reset your scene here
+ init();
+ }
+
+ function toggleConstellationLabels() {
+ // Toggle constellation labels here
+ }
+
+ function reduceMotion() {
+ // Handle reduced motion preferences here
+ }
+
+ setupCanvas();
+ animate();
+
+ window.addEventListener('resize', setupCanvas);
+ </script>
+</body>
+</html>
\ No newline at end of file
diff --git a/data/artifacts/97621618f31c/qwen3-14b.html b/data/artifacts/97621618f31c/qwen3-14b.html
new file mode 100644
index 0000000..8be0c4c
--- /dev/null
+++ b/data/artifacts/97621618f31c/qwen3-14b.html
@@ -0,0 +1,511 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <title>Terrapin Lantern Observatory</title>
+ <style>
+ html, body {
+ margin: 0;
+ padding: 0;
+ overflow: hidden;
+ background: black;
+ color: white;
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
+ user-select: none;
+ }
+ #controls {
+ position: absolute;
+ top: 10px;
+ left: 10px;
+ background: rgba(255, 255, 255, 0.1);
+ padding: 10px;
+ border-radius: 8px;
+ z-index: 1000;
+ }
+ #controls button {
+ margin: 0 5px;
+ padding: 5px 10px;
+ background: rgba(255, 255, 255, 0.2);
+ border: none;
+ color: white;
+ cursor: pointer;
+ border-radius: 4px;
+ }
+ #controls button:hover {
+ background: rgba(255, 255, 255, 0.3);
+ }
+ canvas {
+ display: block;
+ width: 100vw;
+ height: 100vh;
+ }
+ .label {
+ position: absolute;
+ font-size: 12px;
+ color: #aaa;
+ pointer-events: none;
+ }
+ </style>
+</head>
+<body>
+ <div id="controls">
+ <button onclick="setScene('bluehour')">Blue Hour</button>
+ <button onclick="setScene('zenith')">Zenith</button>
+ <button onclick="setScene('rosedawn')">Rose Dawn</button>
+ <button onclick="toggleCamera('ground')">Ground</button>
+ <button onclick="toggleCamera('shell')">Shell</button>
+ <button onclick="toggleCamera('sky')">Sky</button>
+ <button onclick="toggleTempo()">Toggle Tempo</button>
+ <button onclick="togglePaths()">Toggle Paths</button>
+ <button onclick="togglePeople()">Toggle People</button>
+ <button onclick="toggleAnimals()">Toggle Animals</button>
+ <button onclick="toggleLabels()">Toggle Labels</button>
+ <button onclick="pause()">Pause</button>
+ <button onclick="reset()">Reset</button>
+ <button onclick="setStill()">Still Mode</button>
+ </div>
+ <canvas id="canvas"></canvas>
+ <div id="credits" style="position: absolute; bottom: 10px; left: 10px; font-size: 12px; color: #aaa;">
+ All rhythm is designed. No live music used.
+ </div>
+ <script>
+ const canvas = document.getElementById('canvas');
+ const ctx = canvas.getContext('2d');
+ let width = canvas.width = window.innerWidth;
+ let height = canvas.height = window.innerHeight;
+ let scene = 'bluehour';
+ let activeCamera = 'ground';
+ let tempo = 1;
+ let showPaths = false;
+ let showPeople = true;
+ let showAnimals = true;
+ let showLabels = true;
+ let paused = false;
+ let still = false;
+
+ const people = [];
+ const animals = [];
+ const lanterns = [];
+ const constellations = [];
+
+ const tortoise = {
+ x: width / 2,
+ y: height / 2,
+ size: 200,
+ angle: 0,
+ speed: 0.005,
+ step: 0,
+ stepSize: 1.5,
+ sway: 0,
+ swaySpeed: 0.01,
+ swayMax: 0.3
+ };
+
+ const starfield = {
+ stars: [],
+ speed: 0.01
+ };
+
+ function init() {
+ for (let i = 0; i < 100; i++) {
+ starfield.stars.push({
+ x: Math.random() * width,
+ y: Math.random() * height,
+ size: Math.random() * 2 + 1,
+ speed: Math.random() * 0.02 + 0.005
+ });
+ }
+ for (let i = 0; i < 10; i++) {
+ people.push({
+ x: Math.random() * width,
+ y: Math.random() * height,
+ angle: Math.random() * Math.PI * 2,
+ radius: 30,
+ speed: 0.01 + Math.random() * 0.01,
+ path: Math.random() < 0.5 ? 'circle' : 'line'
+ });
+ }
+ for (let i = 0; i < 10; i++) {
+ animals.push({
+ x: Math.random() * width,
+ y: Math.random() * height,
+ angle: Math.random() * Math.PI * 2,
+ radius: 20,
+ speed: 0.01 + Math.random() * 0.01,
+ type: ['hare', 'owl', 'coyote', 'bear'][Math.floor(Math.random() * 4)]
+ });
+ }
+ for (let i = 0; i < 15; i++) {
+ lanterns.push({
+ x: Math.random() * width,
+ y: Math.random() * height,
+ radius: 15,
+ angle: Math.random() * Math.PI * 2,
+ speed: 0.01 + Math.random() * 0.01
+ });
+ }
+ for (let i = 0; i < 10; i++) {
+ constellations.push({
+ x: Math.random() * width,
+ y: Math.random() * height,
+ size: 40,
+ type: ['hare', 'owl', 'coyote', 'bear'][Math.floor(Math.random() * 4)]
+ });
+ }
+ }
+
+ function draw() {
+ if (still) {
+ return;
+ }
+ if (!paused) {
+ update();
+ }
+ ctx.clearRect(0, 0, width, height);
+
+ drawBackground();
+
+ drawTortoise();
+ drawPeople();
+ drawAnimals();
+ drawLanterns();
+ drawConstellations();
+ drawStarfield();
+
+ if (showLabels) {
+ drawLabels();
+ }
+ }
+
+ function update() {
+ tortoise.step += tortoise.stepSize;
+ tortoise.angle += tortoise.speed;
+ tortoise.sway += tortoise.swaySpeed;
+ if (Math.abs(tortoise.sway) > tortoise.swayMax) {
+ tortoise.swaySpeed *= -1;
+ }
+
+ for (let p of people) {
+ if (p.path === 'circle') {
+ p.x = tortoise.x + Math.cos(p.angle) * 100 + Math.sin(tortoise.step) * 20;
+ p.y = tortoise.y + Math.sin(p.angle) * 100 + Math.cos(tortoise.step) * 20;
+ p.angle += p.speed * tempo;
+ } else {
+ p.x += Math.cos(p.angle) * p.speed * tempo;
+ p.y += Math.sin(p.angle) * p.speed * tempo;
+ p.angle += 0.01 * tempo;
+ }
+ }
+
+ for (let a of animals) {
+ a.x += Math.cos(a.angle) * a.speed * tempo;
+ a.y += Math.sin(a.angle) * a.speed * tempo;
+ a.angle += 0.01 * tempo;
+ }
+
+ for (let l of lanterns) {
+ l.x += Math.cos(l.angle) * l.speed * tempo;
+ l.y += Math.sin(l.angle) * l.speed * tempo;
+ l.angle += 0.01 * tempo;
+ }
+
+ for (let s of starfield.stars) {
+ s.y += s.speed * tempo;
+ if (s.y > height) {
+ s.y = -20;
+ }
+ }
+ }
+
+ function drawBackground() {
+ ctx.fillStyle = 'black';
+ ctx.fillRect(0, 0, width, height);
+ }
+
+ function drawTortoise() {
+ ctx.save();
+ ctx.translate(tortoise.x, tortoise.y);
+ ctx.rotate(tortoise.angle);
+ ctx.translate(-tortoise.size / 2, -tortoise.size / 2);
+
+ ctx.fillStyle = '#654321';
+ ctx.beginPath();
+ ctx.moveTo(0, 0);
+ ctx.lineTo(tortoise.size, 0);
+ ctx.lineTo(tortoise.size, tortoise.size);
+ ctx.lineTo(0, tortoise.size);
+ ctx.closePath();
+ ctx.fill();
+
+ ctx.fillStyle = '#4b2e1a';
+ ctx.beginPath();
+ ctx.arc(tortoise.size / 2, tortoise.size / 2, tortoise.size / 4, 0, Math.PI * 2);
+ ctx.fill();
+
+ ctx.fillStyle = '#8b7550';
+ ctx.beginPath();
+ ctx.arc(tortoise.size / 2, tortoise.size / 2, tortoise.size / 6, 0, Math.PI * 2);
+ ctx.fill();
+
+ ctx.restore();
+ }
+
+ function drawPeople() {
+ for (let p of people) {
+ if (!showPeople) continue;
+ if (activeCamera === 'ground') {
+ drawPerson(p.x, p.y, p.radius, p.angle);
+ } else if (activeCamera === 'shell') {
+ drawPerson(tortoise.x + Math.cos(p.angle) * 100, tortoise.y + Math.sin(p.angle) * 100, p.radius, p.angle);
+ } else if (activeCamera === 'sky') {
+ drawPerson(p.x, p.y - tortoise.size * 0.5, p.radius, p.angle);
+ }
+ }
+ }
+
+ function drawPerson(x, y, radius, angle) {
+ ctx.save();
+ ctx.translate(x, y);
+ ctx.rotate(angle);
+ ctx.fillStyle = '#fff';
+ ctx.beginPath();
+ ctx.moveTo(0, -radius);
+ ctx.lineTo(-radius / 2, radius);
+ ctx.lineTo(radius / 2, radius);
+ ctx.closePath();
+ ctx.fill();
+ ctx.restore();
+ }
+
+ function drawAnimals() {
+ for (let a of animals) {
+ if (!showAnimals) continue;
+ if (activeCamera === 'ground') {
+ drawAnimal(a.x, a.y, a.radius, a.angle, a.type);
+ } else if (activeCamera === 'shell') {
+ drawAnimal(tortoise.x + Math.cos(a.angle) * 100, tortoise.y + Math.sin(a.angle) * 100, a.radius, a.angle, a.type);
+ } else if (activeCamera === 'sky') {
+ drawAnimal(a.x, a.y - tortoise.size * 0.5, a.radius, a.angle, a.type);
+ }
+ }
+ }
+
+ function drawAnimal(x, y, radius, angle, type) {
+ ctx.save();
+ ctx.translate(x, y);
+ ctx.rotate(angle);
+ ctx.fillStyle = type === 'hare' ? '#f0d9b5' : type === 'owl' ? '#a9a9a9' : type === 'coyote' ? '#b87333' : '#8b4513';
+ if (type === 'hare') {
+ ctx.beginPath();
+ ctx.moveTo(0, -radius);
+ ctx.lineTo(-radius / 2, radius);
+ ctx.lineTo(radius / 2, radius);
+ ctx.closePath();
+ ctx.fill();
+ } else if (type === 'owl') {
+ ctx.beginPath();
+ ctx.arc(0, 0, radius, 0, Math.PI * 2);
+ ctx.fill();
+ ctx.beginPath();
+ ctx.moveTo(0, -radius / 2);
+ ctx.lineTo(-radius / 4, -radius / 2 - radius / 3);
+ ctx.lineTo(0, -radius / 2 - radius / 2);
+ ctx.lineTo(radius / 4, -radius / 2 - radius / 3);
+ ctx.closePath();
+ ctx.fill();
+ } else if (type === 'coyote') {
+ ctx.beginPath();
+ ctx.moveTo(-radius / 2, -radius / 2);
+ ctx.lineTo(0, -radius);
+ ctx.lineTo(radius / 2, -radius / 2);
+ ctx.lineTo(0, -radius / 2);
+ ctx.closePath();
+ ctx.fill();
+ } else if (type === 'bear') {
+ ctx.beginPath();
+ ctx.moveTo(-radius / 2, -radius);
+ ctx.lineTo(0, -radius / 2);
+ ctx.lineTo(radius / 2, -radius);
+ ctx.lineTo(0, -radius / 2);
+ ctx.closePath();
+ ctx.fill();
+ }
+ ctx.restore();
+ }
+
+ function drawLanterns() {
+ for (let l of lanterns) {
+ if (activeCamera === 'ground') {
+ drawLantern(l.x, l.y, l.radius, l.angle);
+ } else if (activeCamera === 'shell') {
+ drawLantern(tortoise.x + Math.cos(l.angle) * 100, tortoise.y + Math.sin(l.angle) * 100, l.radius, l.angle);
+ } else if (activeCamera === 'sky') {
+ drawLantern(l.x, l.y - tortoise.size * 0.5, l.radius, l.angle);
+ }
+ }
+ }
+
+ function drawLantern(x, y, radius, angle) {
+ ctx.save();
+ ctx.translate(x, y);
+ ctx.rotate(angle);
+ ctx.fillStyle = '#ffcc00';
+ ctx.beginPath();
+ ctx.arc(0, 0, radius, 0, Math.PI * 2);
+ ctx.fill();
+ ctx.fillStyle = '#000';
+ ctx.beginPath();
+ ctx.moveTo(0, -radius);
+ ctx.lineTo(-radius / 2, -radius / 2);
+ ctx.lineTo(0, 0);
+ ctx.lineTo(radius / 2, -radius / 2);
+ ctx.closePath();
+ ctx.fill();
+ ctx.restore();
+ }
+
+ function drawConstellations() {
+ for (let c of constellations) {
+ if (activeCamera === 'ground') {
+ drawConstellation(c.x, c.y, c.size, c.type);
+ } else if (activeCamera === 'shell') {
+ drawConstellation(tortoise.x + Math.cos(c.angle) * 100, tortoise.y + Math.sin(c.angle) * 100, c.size, c.type);
+ } else if (activeCamera === 'sky') {
+ drawConstellation(c.x, c.y - tortoise.size * 0.5, c.size, c.type);
+ }
+ }
+ }
+
+ function drawConstellation(x, y, size, type) {
+ ctx.save();
+ ctx.translate(x, y);
+ if (type === 'hare') {
+ drawFloralGeometry(size, 4, 0.5);
+ } else if (type === 'owl') {
+ drawFloralGeometry(size, 6, 0.4);
+ } else if (type === 'coyote') {
+ drawFloralGeometry(size, 5, 0.6);
+ } else if (type === 'bear') {
+ drawFloralGeometry(size, 8, 0.3);
+ }
+ ctx.restore();
+ }
+
+ function drawFloralGeometry(size, segments, density) {
+ ctx.fillStyle = '#ccc';
+ for (let i = 0; i < segments; i++) {
+ let angle = (Math.PI * 2 / segments) * i;
+ let radius = size * (1 + Math.sin(angle * density));
+ ctx.beginPath();
+ ctx.arc(0, 0, radius, 0, Math.PI * 2);
+ ctx.fill();
+ }
+ }
+
+ function drawStarfield() {
+ for (let s of starfield.stars) {
+ ctx.fillStyle = '#fff';
+ ctx.beginPath();
+ ctx.arc(s.x, s.y, s.size, 0, Math.PI * 2);
+ ctx.fill();
+ }
+ }
+
+ function drawLabels() {
+ for (let p of people) {
+ ctx.fillStyle = '#fff';
+ ctx.font = '12px sans-serif';
+ ctx.fillText('Dancer', p.x, p.y - 10);
+ }
+ for (let a of animals) {
+ ctx.fillStyle = '#fff';
+ ctx.font = '12px sans-serif';
+ ctx.fillText(a.type.charAt(0).toUpperCase() + a.type.slice(1), a.x, a.y - 10);
+ }
+ for (let c of constellations) {
+ ctx.fillStyle = '#fff';
+ ctx.font = '12px sans-serif';
+ ctx.fillText(c.type.charAt(0).toUpperCase() + c.type.slice(1), c.x, c.y - 10);
+ }
+ }
+
+ function setScene(s) {
+ scene = s;
+ if (s === 'bluehour') {
+ document.body.style.background = '#0e0e1e';
+ } else if (s === 'zenith') {
+ document.body.style.background = '#1e1e2e';
+ } else if (s === 'rosedawn') {
+ document.body.style.background = '#2e1e1e';
+ }
+ }
+
+ function toggleCamera(cam) {
+ activeCamera = cam;
+ }
+
+ function toggleTempo() {
+ tempo = tempo === 1 ? 2 : 1;
+ }
+
+ function togglePaths() {
+ showPaths = !showPaths;
+ }
+
+ function togglePeople() {
+ showPeople = !showPeople;
+ }
+
+ function toggleAnimals() {
+ showAnimals = !showAnimals;
+ }
+
+ function toggleLabels() {
+ showLabels = !showLabels;
+ }
+
+ function pause() {
+ paused = !paused;
+ }
+
+ function reset() {
+ paused = false;
+ init();
+ }
+
+ function setStill() {
+ still = true;
+ }
+
+ function resize() {
+ width = canvas.width = window.innerWidth;
+ height = canvas.height = window.innerHeight;
+ init();
+ }
+
+ window.addEventListener('resize', resize);
+ document.addEventListener('keydown', e => {
+ if (e.key === 'ArrowLeft') {
+ toggleCamera('ground');
+ } else if (e.key === 'ArrowRight') {
+ toggleCamera('shell');
+ } else if (e.key === 'ArrowUp') {
+ toggleCamera('sky');
+ } else if (e.key === ' ') {
+ toggleTempo();
+ } else if (e.key === 'p') {
+ pause();
+ } else if (e.key === 'r') {
+ reset();
+ } else if (e.key === 's') {
+ setStill();
+ }
+ });
+
+ init();
+ draw();
+ setInterval(draw, 1000 / 60);
+ </script>
+</body>
+</html>
\ No newline at end of file
diff --git a/data/artifacts/cd83de847d24/claude-code.html b/data/artifacts/cd83de847d24/claude-code.html
new file mode 100644
index 0000000..6aec660
--- /dev/null
+++ b/data/artifacts/cd83de847d24/claude-code.html
@@ -0,0 +1,887 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="UTF-8">
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+<title>Scarlet Riverboat Masquerade — An Animated Gallery</title>
+<style>
+ :root{
+ --ink:#f4e8dd;
+ --ink-dim:#c9a89a;
+ --gold:#e8b968;
+ --scarlet:#c9354a;
+ --panel:rgba(24,8,14,0.82);
+ --panel-edge:rgba(232,185,104,0.28);
+ --focus:#ffd98a;
+ --serif:"Iowan Old Style","Palatino Linotype",Palatino,"Book Antiqua",Georgia,serif;
+ --sans:"Avenir Next","Segoe UI",system-ui,-apple-system,sans-serif;
+ }
+ *{box-sizing:border-box}
+ html,body{margin:0;height:100%;background:#0a0407;color:var(--ink);font-family:var(--sans);overflow:hidden}
+ #stage{position:fixed;inset:0}
+ canvas{position:absolute;inset:0;width:100%;height:100%;display:block}
+
+ /* ---- Title / chapter overlay ---- */
+ .masthead{
+ position:fixed;top:0;left:0;right:0;padding:18px 22px 40px;
+ background:linear-gradient(180deg,rgba(10,4,7,0.72),rgba(10,4,7,0));
+ pointer-events:none;z-index:5;
+ }
+ .masthead h1{
+ font-family:var(--serif);font-weight:600;letter-spacing:.045em;
+ margin:0;font-size:clamp(1.35rem,3.4vw,2.6rem);
+ color:var(--ink);text-shadow:0 2px 18px rgba(0,0,0,.6);
+ }
+ .masthead h1 em{color:var(--scarlet);font-style:italic}
+ .masthead .kicker{
+ font-family:var(--sans);text-transform:uppercase;letter-spacing:.42em;
+ font-size:.62rem;color:var(--gold);margin:0 0 6px;opacity:.9;
+ }
+ .chapline{font-family:var(--serif);font-style:italic;color:var(--ink-dim);
+ font-size:clamp(.8rem,1.6vw,1rem);margin-top:6px;transition:opacity .6s}
+
+ /* ---- Chapter rail ---- */
+ .chapters{
+ position:fixed;left:50%;transform:translateX(-50%);bottom:18px;z-index:6;
+ display:flex;gap:10px;background:var(--panel);border:1px solid var(--panel-edge);
+ padding:8px 10px;border-radius:40px;backdrop-filter:blur(8px);
+ }
+ .chapters button{
+ font-family:var(--sans);font-size:.7rem;letter-spacing:.12em;text-transform:uppercase;
+ color:var(--ink-dim);background:transparent;border:none;padding:8px 16px;border-radius:30px;
+ cursor:pointer;transition:color .3s,background .3s;position:relative;
+ }
+ .chapters button[aria-current="true"]{color:#1a0a0e;background:var(--gold);font-weight:600}
+ .chapters button:hover{color:var(--ink)}
+ .chapters button[aria-current="true"]:hover{color:#1a0a0e}
+
+ /* ---- Control panel ---- */
+ .panel{
+ position:fixed;top:0;right:0;height:100%;width:320px;max-width:86vw;z-index:8;
+ background:var(--panel);border-left:1px solid var(--panel-edge);
+ backdrop-filter:blur(12px);padding:20px 20px 30px;overflow-y:auto;
+ transform:translateX(0);transition:transform .45s cubic-bezier(.6,0,.2,1);
+ }
+ .panel.hidden{transform:translateX(100%)}
+ .panel h2{font-family:var(--serif);font-size:1rem;letter-spacing:.08em;
+ margin:4px 0 4px;color:var(--gold);font-weight:600}
+ .panel .sub{font-size:.7rem;color:var(--ink-dim);margin:0 0 18px;line-height:1.5}
+ .ctl{margin:0 0 16px}
+ .ctl label{display:flex;justify-content:space-between;font-size:.72rem;
+ letter-spacing:.08em;text-transform:uppercase;color:var(--ink-dim);margin-bottom:6px}
+ .ctl label .val{color:var(--gold);font-variant-numeric:tabular-nums}
+ input[type=range]{width:100%;accent-color:var(--scarlet);height:22px;cursor:pointer}
+ .btnrow{display:flex;flex-wrap:wrap;gap:8px;margin:6px 0 20px}
+ .btnrow button, .toggle{
+ font-family:var(--sans);font-size:.72rem;letter-spacing:.06em;
+ color:var(--ink);background:rgba(201,53,74,.16);border:1px solid var(--panel-edge);
+ padding:9px 13px;border-radius:8px;cursor:pointer;transition:background .25s,border-color .25s;
+ flex:1 1 auto;min-width:70px;
+ }
+ .btnrow button:hover,.toggle:hover{background:rgba(232,185,104,.2)}
+ .toggle[aria-pressed="true"]{background:var(--gold);color:#1a0a0e;font-weight:600;border-color:var(--gold)}
+ .cams{display:grid;grid-template-columns:1fr 1fr 1fr;gap:8px;margin-bottom:18px}
+ .cams button{
+ font-size:.68rem;letter-spacing:.05em;color:var(--ink);
+ background:rgba(0,0,0,.25);border:1px solid var(--panel-edge);
+ padding:12px 6px;border-radius:8px;cursor:pointer;transition:background .25s;
+ }
+ .cams button[aria-current="true"]{background:var(--scarlet);color:#fff;font-weight:600;border-color:var(--scarlet)}
+ .cams button:hover{background:rgba(232,185,104,.18)}
+
+ .toggles{display:flex;flex-wrap:wrap;gap:8px;margin-bottom:20px}
+ .toggles .toggle{flex:1 1 44%}
+
+ .label-note{
+ font-family:var(--serif);font-size:.74rem;line-height:1.6;color:var(--ink-dim);
+ border-top:1px solid var(--panel-edge);padding-top:14px;margin-top:8px;
+ }
+ .label-note strong{color:var(--ink);font-weight:600}
+
+ /* Panel toggle + play FAB */
+ .fab{
+ position:fixed;z-index:9;background:var(--panel);color:var(--ink);
+ border:1px solid var(--panel-edge);border-radius:50%;width:48px;height:48px;
+ display:flex;align-items:center;justify-content:center;cursor:pointer;
+ font-size:1.1rem;backdrop-filter:blur(8px);transition:background .25s;
+ }
+ .fab:hover{background:rgba(232,185,104,.22)}
+ #panelToggle{top:16px;right:16px}
+ #playToggle{top:16px;right:76px;font-variant-numeric:tabular-nums}
+
+ /* Visible focus everywhere */
+ a:focus-visible,button:focus-visible,input:focus-visible,[tabindex]:focus-visible{
+ outline:3px solid var(--focus);outline-offset:3px;border-radius:6px;
+ }
+ .hint{position:fixed;left:16px;bottom:76px;z-index:6;font-size:.62rem;
+ letter-spacing:.1em;color:var(--ink-dim);opacity:.6;pointer-events:none;text-transform:uppercase}
+ @media(max-width:640px){
+ #playToggle{right:72px}
+ .chapters{bottom:78px}
+ .hint{display:none}
+ }
+ .sr{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0)}
+</style>
+</head>
+<body>
+<div id="stage">
+ <canvas id="scene" aria-label="Animated scene: a fantastical paddlewheel riverboat gliding through a crimson canyon at dusk while masked dancers fill three decks and animals dance along the shore." role="img"></canvas>
+</div>
+
+<header class="masthead" aria-hidden="false">
+ <p class="kicker">A Gallery in Authored Visual Rhythm</p>
+ <h1>Scarlet <em>Riverboat</em> Masquerade</h1>
+ <p class="chapline" id="chapline">Chapter I — Departure · dusk over the crimson canyon</p>
+</header>
+
+<button class="fab" id="playToggle" aria-label="Pause the scene" title="Pause / Play (Space)">❚❚</button>
+<button class="fab" id="panelToggle" aria-label="Hide controls" aria-expanded="true" title="Toggle controls (C)">≡</button>
+
+<nav class="chapters" aria-label="Chapter markers">
+ <button data-chap="0" aria-current="true">Departure</button>
+ <button data-chap="1">Moon Bend</button>
+ <button data-chap="2">Dawn Landing</button>
+</nav>
+
+<p class="hint">Space · pause | 1·2·3 · cameras | ←→ · chapters | C · controls</p>
+
+<aside class="panel" id="panel" aria-label="Scene controls">
+ <h2>The Conductor's Desk</h2>
+ <p class="sub">Every motion here is <em>authored visual rhythm</em> — hand-choreographed loops, not sound analysis. Shape the tableau to taste.</p>
+
+ <h2 style="font-size:.78rem;letter-spacing:.14em;text-transform:uppercase;color:var(--ink-dim)">Camera</h2>
+ <div class="cams" role="group" aria-label="Camera view">
+ <button data-cam="0" aria-current="true">Wide River</button>
+ <button data-cam="1">On Deck</button>
+ <button data-cam="2">Overhead</button>
+ </div>
+
+ <div class="ctl">
+ <label for="tempo">Visual Tempo <span class="val" id="tempoV">1.0×</span></label>
+ <input type="range" id="tempo" min="0" max="240" value="100" aria-valuetext="1.0 times speed">
+ </div>
+ <div class="ctl">
+ <label for="lantern">Lantern Intensity <span class="val" id="lanternV">70%</span></label>
+ <input type="range" id="lantern" min="0" max="100" value="70">
+ </div>
+ <div class="ctl">
+ <label for="crowd">Crowd Density <span class="val" id="crowdV">Full</span></label>
+ <input type="range" id="crowd" min="10" max="100" value="100">
+ </div>
+ <div class="ctl">
+ <label for="trails">Motion Trails <span class="val" id="trailsV">40%</span></label>
+ <input type="range" id="trails" min="0" max="100" value="40">
+ </div>
+
+ <div class="toggles" role="group" aria-label="Visibility">
+ <button class="toggle" id="animalsT" aria-pressed="true">Shoreline Animals</button>
+ <button class="toggle" id="motionT" aria-pressed="false">Reduced Motion</button>
+ </div>
+
+ <div class="btnrow">
+ <button id="restart">↺ Restart Voyage</button>
+ </div>
+
+ <p class="label-note">
+ <strong>Museum Label.</strong> <em>Scarlet Riverboat Masquerade</em> (digital, generative loop).
+ The work invents an original visual language for communal celebration: no logos, no emblems, no
+ borrowed art — only silhouette, lantern-glow and repeating gesture. Dancers read as pure shape —
+ couples <strong>turning</strong> beneath swinging lanterns, a linked circle <strong>revolving</strong>
+ around the smokestack, solo figures <strong>spinning</strong> ribbons of scarf, and musicians reduced
+ to abstract standing forms so the eye rests on rhythm, not identity. Along the shore, each animal
+ keeps its own weighted cadence — the fox's springing trot, the heron's slow neck-fold, the rabbit's
+ double-hop, and the bear who conducts fireflies with the gravity of a tired maestro. The three
+ chapters — <strong>Departure</strong>, <strong>Moon Bend</strong>, <strong>Dawn Landing</strong> —
+ re-tune the palette and step. All movement is time-authored choreography; the piece listens to no
+ audio and analyzes no sound.
+ </p>
+</aside>
+
+<script>
+(() => {
+ "use strict";
+ const cv = document.getElementById('scene');
+ const ctx = cv.getContext('2d');
+ let W=0,H=0,DPR=1;
+ function resize(){
+ DPR = Math.min(window.devicePixelRatio||1, 2);
+ W = cv.clientWidth; H = cv.clientHeight;
+ cv.width = Math.round(W*DPR); cv.height = Math.round(H*DPR);
+ ctx.setTransform(DPR,0,0,DPR,0,0);
+ }
+ window.addEventListener('resize', resize);
+
+ // ---- State ----
+ const S = {
+ t: 0, // authored clock (seconds of rhythm)
+ playing: true,
+ tempo: 1.0,
+ lantern: 0.7,
+ crowd: 1.0,
+ trails: 0.4,
+ animals: true,
+ reduced: false,
+ cam: 0,
+ chapter: 0,
+ camBlend: 0, // eased camera position
+ chapBlend: 0, // eased chapter position (float across chapters)
+ };
+
+ // Deterministic pseudo-random for stable placement
+ function rng(seed){ let s = seed%2147483647; if(s<=0)s+=2147483646;
+ return ()=> (s = s*16807 % 2147483647)/2147483647; }
+
+ // ---- Palettes per chapter ----
+ const PAL = [
+ { // Departure — dusk crimson
+ skyTop:'#2a0a16', skyBot:'#7a1526', canyonBack:'#4a0f1e', canyonMid:'#63142a',
+ canyonFront:'#3a0a16', water:'#5a1020', waterHi:'#b8324f', boat:'#1c0810',
+ lantern:'#ffce7a', accent:'#e8b968', dancer:'#120508', moon:'#f6d9a0', star:'#e8b968'
+ },
+ { // Moon Bend — deep indigo night, silver moon
+ skyTop:'#0a0a24', skyBot:'#2a1440', canyonBack:'#241338', canyonMid:'#31184a',
+ canyonFront:'#170d26', water:'#141033', waterHi:'#6a5cc0', boat:'#0c0818',
+ lantern:'#bfe0ff', accent:'#9fb6ff', dancer:'#080614', moon:'#e8f0ff', star:'#cfe0ff'
+ },
+ { // Dawn Landing — rose gold sunrise
+ skyTop:'#3a1a2a', skyBot:'#e9905a', canyonBack:'#8a3a3e', canyonMid:'#b25848',
+ canyonFront:'#5a2028', water:'#c96a58', waterHi:'#ffd9a0', boat:'#2a1418',
+ lantern:'#fff0c8', accent:'#ffcf8a', dancer:'#241014', moon:'#fff4dc', star:'#ffe6b0'
+ }
+ ];
+ const CHAPNAMES=[
+ 'Chapter I — Departure · dusk over the crimson canyon',
+ 'Chapter II — Moon Bend · the river turns beneath a silver moon',
+ 'Chapter III — Dawn Landing · rose-gold light on the water'
+ ];
+
+ function lerp(a,b,t){return a+(b-a)*t;}
+ function hx(h){return [parseInt(h.slice(1,3),16),parseInt(h.slice(3,5),16),parseInt(h.slice(5,7),16)];}
+ function mix(c1,c2,t){const a=hx(c1),b=hx(c2);
+ return `rgb(${Math.round(lerp(a[0],b[0],t))},${Math.round(lerp(a[1],b[1],t))},${Math.round(lerp(a[2],b[2],t))})`;}
+ // Blended palette value across chapters
+ function P(key){
+ const f=S.chapBlend; const i=Math.floor(f); const j=Math.min(i+1,2); const t=f-i;
+ return mix(PAL[i][key],PAL[j][key],t);
+ }
+
+ // ---- Deterministic populations ----
+ const rand = rng(20260901);
+ const DANCERS_DECK = []; // {deck, x, phase, kind}
+ // three decks; kinds: couple, circle, solo, musician
+ function buildDancers(){
+ DANCERS_DECK.length=0;
+ const decks=[0,1,2];
+ decks.forEach(d=>{
+ // couples turning under lanterns
+ for(let i=0;i<6;i++) DANCERS_DECK.push({deck:d,slot:i/6,phase:rand()*6.28,kind:'couple',r:rand()});
+ // solo scarf spinners
+ for(let i=0;i<3;i++) DANCERS_DECK.push({deck:d,slot:rand(),phase:rand()*6.28,kind:'solo',r:rand()});
+ // musicians (abstract) at the ends
+ for(let i=0;i<2;i++) DANCERS_DECK.push({deck:d,slot:i?0.9:0.08,phase:rand()*6.28,kind:'musician',r:rand()});
+ });
+ // linked circle rotates around smokestack — top deck only, generated in draw
+ }
+ buildDancers();
+
+ const FIREFLIES = Array.from({length:60},()=>({x:rand(),y:rand(),ph:rand()*6.28,sp:0.4+rand()*0.8}));
+
+ // ---- Camera definitions: return {ox,oy,scale} in scene space ----
+ // Scene is authored in a virtual 1000x600 space, mapped to canvas.
+ function camParams(){
+ // three cameras: wide (full), deck (zoom into boat mid), overhead (top-down tilt)
+ const cams=[
+ {scale:1.0, ox:0, oy:0, tilt:0},
+ {scale:2.15, ox:-0.06, oy:0.18, tilt:0},
+ {scale:1.35, ox:0, oy:-0.10, tilt:0.55}
+ ];
+ // blend between current integer cam target
+ const f=S.camBlend; const i=Math.floor(f); const j=Math.min(i+1,2); const t=f-i;
+ const a=cams[i],b=cams[j];
+ return {
+ scale:lerp(a.scale,b.scale,t),
+ ox:lerp(a.ox,b.ox,t),
+ oy:lerp(a.oy,b.oy,t),
+ tilt:lerp(a.tilt,b.tilt,t)
+ };
+ }
+
+ // Virtual->screen helpers set per frame
+ let VX=0,VY=0,VS=1,TILT=0;
+ function setupProjection(){
+ const c=camParams();
+ const base=Math.min(W/1000,H/600);
+ VS = base*c.scale;
+ const cw=1000*VS, ch=600*VS;
+ VX = (W-cw)/2 + c.ox*cw;
+ VY = (H-ch)/2 + c.oy*ch;
+ TILT=c.tilt;
+ }
+ function px(x){return VX + x*VS;}
+ function py(y){return VY + y*VS;}
+ function pu(u){return u*VS;} // scale a virtual length
+
+ // ---- Drawing ----
+ function drawSky(){
+ const g=ctx.createLinearGradient(0,py(0),0,py(360));
+ g.addColorStop(0,P('skyTop')); g.addColorStop(1,P('skyBot'));
+ ctx.fillStyle=g; ctx.fillRect(0,0,W,H);
+ // stars / soft glow — twinkle authored by sine
+ const starN=70;
+ ctx.save();
+ for(let i=0;i<starN;i++){
+ const sx=px((i*137.5%1000)); const sy=py((i*53.7%300));
+ const tw=0.4+0.6*Math.abs(Math.sin(S.t*0.6 + i));
+ ctx.globalAlpha=tw*0.7*(1-S.chapBlend*0.15);
+ ctx.fillStyle=P('star');
+ ctx.beginPath();ctx.arc(sx,sy,pu(0.9+ (i%3)*0.5),0,6.283);ctx.fill();
+ }
+ ctx.restore();
+ // Moon — drifts subtly, phase depends on chapter
+ const moonX=200+Math.sin(S.t*0.05)*20 + S.chapBlend*260;
+ const moonY=90 - S.chapBlend*20;
+ const mg=ctx.createRadialGradient(px(moonX),py(moonY),0,px(moonX),py(moonY),pu(120));
+ mg.addColorStop(0,P('moon')); mg.addColorStop(0.18,P('moon'));
+ mg.addColorStop(0.19,'rgba(0,0,0,0)');
+ ctx.save();ctx.globalAlpha=0.9;ctx.fillStyle=mg;
+ ctx.beginPath();ctx.arc(px(moonX),py(moonY),pu(120),0,6.283);ctx.fill();
+ ctx.globalAlpha=1;ctx.fillStyle=P('moon');
+ ctx.beginPath();ctx.arc(px(moonX),py(moonY),pu(26),0,6.283);ctx.fill();
+ ctx.restore();
+ }
+
+ // Layered parallax canyon — each layer sways slightly & drifts opposite to boat travel
+ function canyonLayer(color, baseY, amp, seed, speed, jag){
+ const r=rng(seed);
+ ctx.fillStyle=color;
+ ctx.beginPath();
+ ctx.moveTo(px(-40),py(600));
+ const drift = (S.t*speed)%200;
+ for(let x=-40;x<=1040;x+=jag){
+ const n=Math.sin((x*0.012)+seed)+0.5*Math.sin((x*0.03)+seed*1.7);
+ const y=baseY + n*amp - drift*0.02*amp + (r()-0.5)*amp*0.15;
+ ctx.lineTo(px(x),py(y));
+ }
+ ctx.lineTo(px(1040),py(600));ctx.closePath();ctx.fill();
+ }
+ function drawCanyon(){
+ canyonLayer(P('canyonBack'),180,60,11,3,26);
+ canyonLayer(P('canyonMid'),240,80,29,6,20);
+ canyonLayer(P('canyonFront'),300,70,41,10,16);
+ }
+
+ function drawWater(){
+ ctx.fillStyle=P('water');
+ ctx.fillRect(px(-40),py(360),pu(1080),pu(280));
+ // moving highlight bands (authored ripple)
+ ctx.save();
+ for(let i=0;i<9;i++){
+ const yy=380+i*26;
+ const phase=S.t*(0.6+i*0.05)+i;
+ const off=Math.sin(phase)*10;
+ ctx.globalAlpha=0.12+0.05*Math.sin(phase*1.3);
+ ctx.strokeStyle=P('waterHi');
+ ctx.lineWidth=pu(2+ (i%2));
+ ctx.beginPath();
+ for(let x=-40;x<=1040;x+=40){
+ const y=yy + Math.sin(x*0.03 + phase)*6 + off;
+ if(x===-40)ctx.moveTo(px(x),py(y));else ctx.lineTo(px(x),py(y));
+ }
+ ctx.stroke();
+ }
+ ctx.restore();
+ // boat & lantern reflections
+ }
+
+ // ---- The riverboat ----
+ // Positioned mid-scene, bobs gently. Returns geometry used by dancers.
+ function boatGeom(){
+ const bob=Math.sin(S.t*0.7)*4;
+ const glideX = 470 + Math.sin(S.t*0.06)*14;
+ return {cx:glideX, cy:352+bob, w:360, hullH:60, bob};
+ }
+
+ function drawReflection(g){
+ // wobbling mirrored glow of the hull
+ ctx.save();
+ ctx.globalAlpha=0.22;
+ ctx.fillStyle=P('waterHi');
+ const wob=Math.sin(S.t*1.1)*6;
+ ctx.beginPath();
+ ctx.ellipse(px(g.cx),py(g.cy+70),pu(g.w*0.42),pu(20+Math.abs(wob)),0,0,6.283);
+ ctx.fill();
+ ctx.restore();
+ }
+
+ function drawPaddlewheel(g){
+ // right-rear wheel, turns with tempo; weight = eased ease of spokes
+ const wx=g.cx - g.w*0.5 + 6, wy=g.cy - 6, R=44;
+ ctx.save();
+ ctx.translate(px(wx),py(wy));
+ ctx.rotate(-S.t*1.1); // authored rotation
+ // housing
+ ctx.strokeStyle=P('accent');ctx.lineWidth=pu(3);
+ ctx.fillStyle=P('boat');
+ ctx.beginPath();ctx.arc(0,0,pu(R),0,6.283);ctx.fill();
+ for(let i=0;i<8;i++){
+ ctx.save();ctx.rotate(i/8*6.283);
+ ctx.fillStyle= i%2? P('accent'):P('lantern');
+ ctx.globalAlpha=0.9;
+ ctx.fillRect(-pu(4),-pu(R),pu(8),pu(R*0.9));
+ ctx.restore();
+ }
+ ctx.beginPath();ctx.arc(0,0,pu(R),0,6.283);ctx.stroke();
+ ctx.fillStyle=P('accent');ctx.beginPath();ctx.arc(0,0,pu(6),0,6.283);ctx.fill();
+ ctx.restore();
+ // churned water
+ ctx.save();ctx.globalAlpha=0.5;ctx.fillStyle=P('waterHi');
+ for(let i=0;i<5;i++){const a=0.3+0.2*Math.sin(S.t*3+i);
+ ctx.globalAlpha=a*0.5;
+ ctx.beginPath();ctx.arc(px(wx-14-i*8),py(wy+30+Math.sin(S.t*4+i)*3),pu(4+i),0,6.283);ctx.fill();}
+ ctx.restore();
+ }
+
+ function drawHull(g){
+ ctx.save();
+ ctx.fillStyle=P('boat');
+ // hull
+ ctx.beginPath();
+ ctx.moveTo(px(g.cx-g.w/2),py(g.cy));
+ ctx.quadraticCurveTo(px(g.cx-g.w/2-30),py(g.cy+40),px(g.cx-g.w/2+30),py(g.cy+g.hullH));
+ ctx.lineTo(px(g.cx+g.w/2-30),py(g.cy+g.hullH));
+ ctx.quadraticCurveTo(px(g.cx+g.w/2+40),py(g.cy+30),px(g.cx+g.w/2),py(g.cy));
+ ctx.closePath();ctx.fill();
+ // gold trim waterline
+ ctx.strokeStyle=P('accent');ctx.lineWidth=pu(2);ctx.globalAlpha=0.8;
+ ctx.beginPath();ctx.moveTo(px(g.cx-g.w/2+4),py(g.cy+4));ctx.lineTo(px(g.cx+g.w/2-4),py(g.cy+4));ctx.stroke();
+ ctx.restore();
+ }
+
+ // deck y positions (top of each deck floor) relative to boat cy
+ function deckY(g,d){ return g.cy - 22 - d*46; }
+ function deckSpan(g,d){ const shrink=d*26; return {x0:g.cx-g.w*0.44+shrink, x1:g.cx+g.w*0.44-shrink}; }
+
+ function drawDeckStructure(g){
+ ctx.save();
+ for(let d=0;d<3;d++){
+ const y=deckY(g,d); const s=deckSpan(g,d);
+ // deck floor
+ ctx.fillStyle=P('boat');
+ ctx.globalAlpha=0.96;
+ ctx.fillRect(px(s.x0),py(y),pu(s.x1-s.x0),pu(46));
+ // rail
+ ctx.strokeStyle=P('accent');ctx.lineWidth=pu(1.5);ctx.globalAlpha=0.7;
+ ctx.beginPath();ctx.moveTo(px(s.x0),py(y));ctx.lineTo(px(s.x1),py(y));ctx.stroke();
+ // rail posts
+ for(let x=s.x0;x<=s.x1;x+=24){ctx.beginPath();ctx.moveTo(px(x),py(y));ctx.lineTo(px(x),py(y+10));ctx.stroke();}
+ // roof canopy scalloped
+ ctx.globalAlpha=0.9;ctx.fillStyle=mix(P('boat'),P('accent'),0.12);
+ ctx.beginPath();ctx.moveTo(px(s.x0-6),py(y));
+ for(let x=s.x0-6;x<=s.x1+6;x+=16){ctx.lineTo(px(x),py(y-6+Math.sin(x*0.4)*2));}
+ ctx.lineTo(px(s.x1+6),py(y));ctx.closePath();ctx.fill();
+ }
+ // smokestacks
+ const stackY=deckY(g,2)-4;
+ [g.cx-30,g.cx+30].forEach(sx=>{
+ ctx.fillStyle=P('boat');
+ ctx.fillRect(px(sx-6),py(stackY-70),pu(12),pu(70));
+ ctx.fillStyle=P('accent');ctx.globalAlpha=0.7;
+ ctx.fillRect(px(sx-8),py(stackY-74),pu(16),pu(5));
+ // gentle smoke
+ ctx.globalAlpha=0.12;ctx.fillStyle=P('star');
+ for(let i=0;i<3;i++){const yy=stackY-70-i*24-((S.t*14)%24);
+ ctx.beginPath();ctx.arc(px(sx+Math.sin(S.t+i)*6),py(yy),pu(6+i*2),0,6.283);ctx.fill();}
+ });
+ ctx.restore();
+ }
+
+ // lantern strings along decks — swing authored, intensity from control
+ function drawLanterns(g){
+ const inten=S.lantern;
+ ctx.save();
+ for(let d=0;d<3;d++){
+ const y=deckY(g,d)-4; const s=deckSpan(g,d);
+ for(let x=s.x0+14;x<s.x1;x+=40){
+ const sw=Math.sin(S.t*1.4 + x*0.05)*6*(S.reduced?0.2:1);
+ const lx=x+sw, ly=y-14+Math.abs(Math.cos(S.t*1.4+x*0.05))*2;
+ // glow
+ const gg=ctx.createRadialGradient(px(lx),py(ly),0,px(lx),py(ly),pu(30));
+ gg.addColorStop(0,P('lantern'));gg.addColorStop(1,'rgba(0,0,0,0)');
+ ctx.globalAlpha=0.5*inten;ctx.fillStyle=gg;
+ ctx.beginPath();ctx.arc(px(lx),py(ly),pu(30),0,6.283);ctx.fill();
+ // body
+ ctx.globalAlpha=0.85*inten+0.15;ctx.fillStyle=P('lantern');
+ ctx.beginPath();ctx.ellipse(px(lx),py(ly),pu(4),pu(6),0,0,6.283);ctx.fill();
+ // string
+ ctx.globalAlpha=0.4;ctx.strokeStyle=P('accent');ctx.lineWidth=pu(0.8);
+ ctx.beginPath();ctx.moveTo(px(x),py(y));ctx.lineTo(px(lx),py(ly-6));ctx.stroke();
+ }
+ }
+ ctx.restore();
+ }
+
+ // ---- Dancer figures (silhouettes) ----
+ // A dancer is drawn as a small vertical body; motion depends on kind.
+ function figure(x,y,scale,rot,leanX,armSpread,color,alpha){
+ ctx.save();
+ ctx.globalAlpha=alpha;
+ ctx.translate(px(x),py(y));
+ ctx.rotate(rot);
+ const u=pu(scale);
+ ctx.fillStyle=color;
+ // head
+ ctx.beginPath();ctx.arc(leanX*u,-9*u,3.2*u,0,6.283);ctx.fill();
+ // torso (trapezoid)
+ ctx.beginPath();
+ ctx.moveTo(-2.4*u,-6*u);ctx.lineTo(2.4*u,-6*u);
+ ctx.lineTo(3*u+leanX*u,1*u);ctx.lineTo(-3*u+leanX*u,1*u);ctx.closePath();ctx.fill();
+ // skirt / legs
+ ctx.beginPath();
+ ctx.moveTo(-3*u+leanX*u,1*u);ctx.lineTo(3*u+leanX*u,1*u);
+ ctx.lineTo(4.5*u+leanX*u,9*u);ctx.lineTo(-4.5*u+leanX*u,9*u);ctx.closePath();ctx.fill();
+ // arms
+ ctx.strokeStyle=color;ctx.lineWidth=1.6*u;ctx.lineCap='round';
+ ctx.beginPath();
+ ctx.moveTo(0,-4*u);ctx.lineTo(-armSpread*u,-6*u-Math.abs(armSpread)*0.4*u);
+ ctx.moveTo(0,-4*u);ctx.lineTo(armSpread*u,-6*u-Math.abs(armSpread)*0.4*u);
+ ctx.stroke();
+ ctx.restore();
+ }
+
+ function drawDancers(g){
+ const dcolor=P('dancer');
+ const beat = S.t*(1.0 + S.chapBlend*0.15); // step quickens slightly by chapter
+ const density=S.crowd;
+ // per-deck authored choreography
+ DANCERS_DECK.forEach((p,idx)=>{
+ if((idx%10)/10 > density) return; // thin out by density
+ const y=deckY(g,p.deck)+30; const s=deckSpan(g,p.deck);
+ const x=lerp(s.x0+16,s.x1-16,p.slot);
+ if(p.kind==='couple'){
+ // two figures turning around a shared center (waltz)
+ const spin=beat*1.1+p.phase;
+ const rad=6+p.r*3;
+ const lean=Math.sin(spin)*0.5;
+ figure(x+Math.cos(spin)*rad, y+Math.sin(spin)*rad*0.35, 1.0, Math.sin(spin)*0.15, lean, 3.5+Math.sin(spin)*1.5, dcolor, 0.95);
+ figure(x-Math.cos(spin)*rad, y-Math.sin(spin)*rad*0.35, 1.0, -Math.sin(spin)*0.15, -lean, 3.5-Math.sin(spin)*1.5, dcolor, 0.95);
+ } else if(p.kind==='solo'){
+ // spins with a trailing scarf
+ const spin=beat*1.6+p.phase;
+ const lean=Math.sin(spin*1.3)*0.7;
+ figure(x, y, 1.05, Math.sin(spin)*0.25, lean, 4+Math.sin(spin*2)*2, dcolor, 0.95);
+ // scarf ribbon
+ ctx.save();ctx.globalAlpha=0.8;
+ ctx.strokeStyle=P('scarlet')||'#c9354a';ctx.strokeStyle='#c9354a';
+ ctx.lineWidth=pu(2);ctx.lineCap='round';
+ ctx.beginPath();
+ for(let k=0;k<10;k++){
+ const a=spin - k*0.4; const rr=(4+k*1.6);
+ const sxp=x+Math.cos(a)*rr, syp=y-10 - Math.sin(a)*rr*0.5;
+ if(k===0)ctx.moveTo(px(sxp),py(syp));else ctx.lineTo(px(sxp),py(syp));
+ }
+ ctx.stroke();ctx.restore();
+ } else if(p.kind==='musician'){
+ // abstract standing silhouette, minimal sway — suggests players
+ const sway=Math.sin(beat*0.7+p.phase)*0.12;
+ ctx.save();ctx.globalAlpha=0.9;ctx.fillStyle=dcolor;
+ ctx.translate(px(x),py(y));ctx.rotate(sway);
+ const u=pu(1.1);
+ ctx.beginPath();ctx.arc(0,-11*u,3.6*u,0,6.283);ctx.fill();
+ ctx.beginPath();ctx.moveTo(-4*u,-8*u);ctx.lineTo(4*u,-8*u);ctx.lineTo(5*u,10*u);ctx.lineTo(-5*u,10*u);ctx.closePath();ctx.fill();
+ // suggestion of an instrument shape (abstract arc)
+ ctx.strokeStyle=P('accent');ctx.lineWidth=1.6*u;ctx.globalAlpha=0.55;
+ ctx.beginPath();ctx.arc(6*u,-2*u,5*u,-1,1.4);ctx.stroke();
+ ctx.restore();
+ }
+ });
+ // ---- Linked circle rotating around the smokestack (top deck) ----
+ const topY=deckY(g,2)+26;
+ const ring=Math.round(8*density);
+ const spin=beat*0.5;
+ ctx.save();
+ for(let i=0;i<ring;i++){
+ const a=spin + i/ring*6.283;
+ const rx=g.cx+Math.cos(a)*54;
+ const ry=topY+Math.sin(a)*16;
+ const facing=Math.sin(a);
+ figure(rx,ry,0.9,0,facing*0.4,2.5,dcolor,0.9);
+ }
+ // joined-hands hint (faint arc)
+ ctx.globalAlpha=0.3;ctx.strokeStyle=P('accent');ctx.lineWidth=pu(1.2);
+ ctx.beginPath();ctx.ellipse(px(g.cx),py(topY),pu(54),pu(16),0,0,6.283);ctx.stroke();
+ ctx.restore();
+ }
+
+ // ---- Shoreline animals — each a distinct weighted loop ----
+ function drawAnimals(){
+ if(!S.animals) return;
+ const baseY=422;
+ const c=P('dancer');
+ const beat=S.t;
+ ctx.save();
+ ctx.fillStyle=c;
+
+ // FOX — springing trot near right shore
+ (()=>{
+ const cyc=(beat*1.3)%6.283;
+ const hop=Math.max(0,Math.sin(cyc))*10; // springy up
+ const fx=760+Math.sin(beat*0.3)*10, fy=baseY-hop;
+ ctx.save();ctx.translate(px(fx),py(fy));
+ const u=pu(1);
+ ctx.globalAlpha=0.95;
+ // body
+ ctx.beginPath();ctx.ellipse(0,0,10*u,5*u,Math.sin(cyc)*0.15,0,6.283);ctx.fill();
+ // head
+ ctx.beginPath();ctx.moveTo(9*u,-2*u);ctx.lineTo(16*u,-3*u);ctx.lineTo(9*u,3*u);ctx.closePath();ctx.fill();
+ // ears
+ ctx.beginPath();ctx.moveTo(11*u,-4*u);ctx.lineTo(12*u,-9*u);ctx.lineTo(14*u,-4*u);ctx.closePath();ctx.fill();
+ // bushy tail sweeps opposite the spring (weight)
+ ctx.save();ctx.translate(-9*u,0);ctx.rotate(-0.6+Math.sin(cyc)*0.5);
+ ctx.beginPath();ctx.ellipse(-7*u,0,9*u,3.5*u,0,0,6.283);ctx.fill();ctx.restore();
+ // legs — trotting
+ ctx.strokeStyle=c;ctx.lineWidth=1.6*u;ctx.lineCap='round';
+ for(let l=0;l<4;l++){const lx=(-6+l*4)*u;const sw=Math.sin(cyc+l*1.6)*3*u;
+ ctx.beginPath();ctx.moveTo(lx,4*u);ctx.lineTo(lx+sw,10*u);ctx.stroke();}
+ ctx.restore();
+ })();
+
+ // HERON — slow neck fold, standing in shallows left
+ (()=>{
+ const cyc=(beat*0.5);
+ const fold=(Math.sin(cyc)+1)/2; // 0..1 slow
+ const hx=210, hy=baseY-2;
+ ctx.save();ctx.translate(px(hx),py(hy));const u=pu(1);
+ ctx.globalAlpha=0.95;
+ // legs
+ ctx.strokeStyle=c;ctx.lineWidth=1.4*u;ctx.lineCap='round';
+ ctx.beginPath();ctx.moveTo(-2*u,0);ctx.lineTo(-2*u,14*u);
+ ctx.moveTo(3*u,0);ctx.lineTo(3*u+Math.sin(cyc*1.2)*u,14*u);ctx.stroke();
+ // body
+ ctx.beginPath();ctx.ellipse(0,-2*u,8*u,5*u,0,0,6.283);ctx.fill();
+ // neck — folds down and up slowly (S-curve)
+ ctx.strokeStyle=c;ctx.lineWidth=2.2*u;
+ const neckTop=-2- (1-fold)*10;
+ ctx.beginPath();
+ ctx.moveTo(4*u,-4*u);
+ ctx.quadraticCurveTo((8+fold*4)*u,(neckTop-4)*u,(6)*u,(neckTop-8)*u);
+ ctx.stroke();
+ // head + beak
+ ctx.beginPath();ctx.arc(6*u,(neckTop-9)*u,2.4*u,0,6.283);ctx.fill();
+ ctx.beginPath();ctx.moveTo(8*u,(neckTop-9)*u);ctx.lineTo(14*u,(neckTop-8)*u);ctx.lineTo(8*u,(neckTop-7.5)*u);ctx.closePath();ctx.fill();
+ ctx.restore();
+ })();
+
+ // RABBIT — double-hop mid shore
+ (()=>{
+ const cyc=(beat*1.6)%6.283;
+ // two quick hops then pause: shape via piecewise
+ const ph=cyc/6.283;
+ let hop=0, fwd=0;
+ if(ph<0.3){hop=Math.sin(ph/0.3*3.14)*8; fwd=ph/0.3*6;}
+ else if(ph<0.55){hop=Math.sin((ph-0.3)/0.25*3.14)*8; fwd=6+ (ph-0.3)/0.25*6;}
+ else {fwd=12;}
+ const rx=540+ (fwd)-6 +Math.sin(beat*0.2)*4, ry=baseY-hop;
+ ctx.save();ctx.translate(px(rx),py(ry));const u=pu(1);ctx.globalAlpha=0.95;
+ // body
+ ctx.beginPath();ctx.ellipse(0,0,7*u,5*u,0,0,6.283);ctx.fill();
+ // head
+ ctx.beginPath();ctx.arc(6*u,-2*u,3.2*u,0,6.283);ctx.fill();
+ // ears — lean back with hop weight
+ const el=-0.3-hop*0.03;
+ ctx.save();ctx.translate(6*u,-4*u);ctx.rotate(el);
+ ctx.beginPath();ctx.ellipse(-1*u,-4*u,1.4*u,4.5*u,0,0,6.283);ctx.fill();
+ ctx.beginPath();ctx.ellipse(2*u,-4*u,1.4*u,4.5*u,0,0,6.283);ctx.fill();ctx.restore();
+ // hind legs tuck
+ ctx.beginPath();ctx.ellipse(-4*u,3*u,3*u,2*u,0,0,6.283);ctx.fill();
+ // tail
+ ctx.beginPath();ctx.arc(-7*u,0,1.8*u,0,6.283);ctx.fill();
+ ctx.restore();
+ })();
+
+ // BEAR — slow, heavy, conducting fireflies (far left, larger)
+ (()=>{
+ const cyc=beat*0.8;
+ const bx=90, by=baseY+6;
+ ctx.save();ctx.translate(px(bx),py(by));const u=pu(1.5);ctx.globalAlpha=0.96;
+ // heavy body sways with weight
+ const sway=Math.sin(cyc*0.5)*0.06;
+ ctx.rotate(sway);
+ ctx.beginPath();ctx.ellipse(0,-2*u,11*u,9*u,0,0,6.283);ctx.fill();
+ // haunch
+ ctx.beginPath();ctx.ellipse(-8*u,2*u,6*u,6*u,0,0,6.283);ctx.fill();
+ // head
+ ctx.beginPath();ctx.arc(8*u,-8*u,5*u,0,6.283);ctx.fill();
+ ctx.beginPath();ctx.arc(4*u,-13*u,2*u,0,6.283);ctx.fill();
+ ctx.beginPath();ctx.arc(11*u,-13*u,2*u,0,6.283);ctx.fill();
+ // conducting arm — slow figure-eight (weighted, ease in/out)
+ const ax=Math.sin(cyc)*8, ay=Math.sin(cyc*2)*4;
+ ctx.strokeStyle=c;ctx.lineWidth=3.5*u;ctx.lineCap='round';
+ ctx.beginPath();ctx.moveTo(8*u,-4*u);ctx.lineTo((10+ax)*u,(-10+ay)*u);ctx.stroke();
+ // baton glimmer
+ ctx.strokeStyle=P('lantern');ctx.lineWidth=1.4*u;ctx.globalAlpha=0.9;
+ ctx.beginPath();ctx.moveTo((10+ax)*u,(-10+ay)*u);ctx.lineTo((14+ax*1.4)*u,(-14+ay*1.4)*u);ctx.stroke();
+ // legs
+ ctx.strokeStyle=c;ctx.lineWidth=4*u;ctx.globalAlpha=1;
+ const step=Math.sin(cyc*0.5)*2*u;
+ ctx.beginPath();ctx.moveTo(-4*u,7*u);ctx.lineTo(-4*u+step,13*u);
+ ctx.moveTo(5*u,7*u);ctx.lineTo(5*u-step,13*u);ctx.stroke();
+ ctx.restore();
+ })();
+
+ ctx.restore();
+ }
+
+ function drawFireflies(){
+ if(!S.animals) return;
+ ctx.save();
+ for(const f of FIREFLIES){
+ // gather toward the bear's baton region, orbiting (conducted)
+ const t=S.t*f.sp + f.ph;
+ const cx=110 + Math.cos(t)*40*(0.5+f.x); // near bear left
+ const cy=390 + Math.sin(t*1.3)*30*(0.4+f.y);
+ const glow=0.4+0.6*Math.abs(Math.sin(t*2));
+ ctx.globalAlpha=glow*0.9;
+ ctx.fillStyle=P('lantern');
+ ctx.beginPath();ctx.arc(px(cx),py(cy),pu(1.5+glow),0,6.283);ctx.fill();
+ }
+ ctx.restore();
+ }
+
+ // ---- Main compose ----
+ function frame(){
+ setupProjection();
+ // trails: instead of clearing fully, overlay a translucent wash
+ const wash = 1 - S.trails*0.75; // more trails => less clearing
+ if(S.reduced || S.trails<=0.01){
+ ctx.globalAlpha=1; ctx.globalCompositeOperation='source-over';
+ ctx.fillStyle='#0a0407'; ctx.fillRect(0,0,W,H);
+ } else {
+ ctx.globalAlpha=1; ctx.globalCompositeOperation='source-over';
+ ctx.fillStyle=`rgba(10,4,7,${wash})`;
+ ctx.fillRect(0,0,W,H);
+ }
+
+ // slight overhead tilt: skew the whole scene vertically when TILT>0
+ ctx.save();
+ if(TILT>0.001){
+ ctx.translate(W/2,H*0.5);
+ ctx.transform(1,0,0,1-TILT*0.35, 0, TILT*40);
+ ctx.translate(-W/2,-H*0.5);
+ }
+
+ drawSky();
+ drawCanyon();
+ drawWater();
+
+ const g=boatGeom();
+ drawReflection(g);
+ drawPaddlewheel(g);
+ drawHull(g);
+ drawDeckStructure(g);
+ drawLanterns(g);
+ drawDancers(g);
+
+ // shoreline layer sits in front of water at bottom
+ drawAnimals();
+ drawFireflies();
+
+ // vignette
+ ctx.restore();
+ const vg=ctx.createRadialGradient(W/2,H*0.5,H*0.2,W/2,H*0.5,H*0.85);
+ vg.addColorStop(0,'rgba(0,0,0,0)');vg.addColorStop(1,'rgba(0,0,0,0.55)');
+ ctx.globalCompositeOperation='source-over';ctx.globalAlpha=1;
+ ctx.fillStyle=vg;ctx.fillRect(0,0,W,H);
+ }
+
+ // ---- Loop with real-time clock but authored advance ----
+ let last=performance.now();
+ function loop(now){
+ const dt=Math.min(0.05,(now-last)/1000); last=now;
+ // ease camera & chapter blends
+ S.camBlend += (S.cam - S.camBlend)*Math.min(1,dt*3.5);
+ S.chapBlend += (S.chapter - S.chapBlend)*Math.min(1,dt*2.2);
+ if(S.playing){
+ const speed = S.tempo * (S.reduced?0.35:1);
+ S.t += dt*speed;
+ }
+ frame();
+ requestAnimationFrame(loop);
+ }
+
+ // ---- Controls wiring ----
+ const $=id=>document.getElementById(id);
+ const tempo=$('tempo'),lantern=$('lantern'),crowd=$('crowd'),trails=$('trails');
+ const tempoV=$('tempoV'),lanternV=$('lanternV'),crowdV=$('crowdV'),trailsV=$('trailsV');
+ function crowdLabel(v){return v>85?'Full':v>55?'Lively':v>30?'Sparse':'Quiet';}
+ tempo.addEventListener('input',()=>{S.tempo=tempo.value/100;tempoV.textContent=S.tempo.toFixed(1)+'×';tempo.setAttribute('aria-valuetext',S.tempo.toFixed(1)+' times speed');});
+ lantern.addEventListener('input',()=>{S.lantern=lantern.value/100;lanternV.textContent=lantern.value+'%';});
+ crowd.addEventListener('input',()=>{S.crowd=crowd.value/100;crowdV.textContent=crowdLabel(+crowd.value);});
+ trails.addEventListener('input',()=>{S.trails=trails.value/100;trailsV.textContent=trails.value+'%';});
+
+ const animalsT=$('animalsT'),motionT=$('motionT');
+ animalsT.addEventListener('click',()=>{S.animals=!S.animals;animalsT.setAttribute('aria-pressed',S.animals);});
+ motionT.addEventListener('click',()=>{
+ S.reduced=!S.reduced;motionT.setAttribute('aria-pressed',S.reduced);
+ });
+
+ // Cameras
+ const camBtns=[...document.querySelectorAll('.cams button')];
+ function setCam(i){S.cam=i;camBtns.forEach((b,k)=>b.setAttribute('aria-current',k===i));}
+ camBtns.forEach((b,i)=>b.addEventListener('click',()=>setCam(i)));
+
+ // Chapters
+ const chapBtns=[...document.querySelectorAll('.chapters button')];
+ const chapline=$('chapline');
+ function setChapter(i){
+ S.chapter=i;
+ chapBtns.forEach((b,k)=>b.setAttribute('aria-current',k===i?'true':'false'));
+ chapline.style.opacity=0;
+ setTimeout(()=>{chapline.textContent=CHAPNAMES[i];chapline.style.opacity=1;},300);
+ }
+ chapBtns.forEach((b,i)=>b.addEventListener('click',()=>setChapter(i)));
+
+ // Play / pause
+ const playToggle=$('playToggle');
+ function setPlay(p){S.playing=p;playToggle.textContent=p?'❚❚':'►';
+ playToggle.setAttribute('aria-label',p?'Pause the scene':'Play the scene');}
+ playToggle.addEventListener('click',()=>setPlay(!S.playing));
+
+ // Restart
+ $('restart').addEventListener('click',()=>{S.t=0;setChapter(0);setPlay(true);});
+
+ // Panel toggle
+ const panel=$('panel'),panelToggle=$('panelToggle');
+ function setPanel(open){
+ panel.classList.toggle('hidden',!open);
+ panelToggle.setAttribute('aria-expanded',open);
+ panelToggle.setAttribute('aria-label',open?'Hide controls':'Show controls');
+ }
+ panelToggle.addEventListener('click',()=>setPanel(panel.classList.contains('hidden')));
+
+ // Keyboard shortcuts (respect typing in range via not interfering)
+ window.addEventListener('keydown',e=>{
+ if(e.target.tagName==='INPUT' && e.key!=='Escape') { /* allow slider arrows */ }
+ switch(e.key){
+ case ' ': case 'Spacebar': e.preventDefault(); setPlay(!S.playing); break;
+ case '1': setCam(0); break;
+ case '2': setCam(1); break;
+ case '3': setCam(2); break;
+ case 'ArrowRight': if(!/INPUT/.test(e.target.tagName)){setChapter(Math.min(2,S.chapter+1));} break;
+ case 'ArrowLeft': if(!/INPUT/.test(e.target.tagName)){setChapter(Math.max(0,S.chapter-1));} break;
+ case 'c': case 'C': setPanel(panel.classList.contains('hidden')); break;
+ case 'r': case 'R': S.t=0; setChapter(0); setPlay(true); break;
+ }
+ });
+
+ // Honor OS reduced-motion preference at load
+ if(window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches){
+ S.reduced=true; motionT.setAttribute('aria-pressed','true');
+ S.trails=0; trails.value=0; trailsV.textContent='0%';
+ }
+
+ resize();
+ requestAnimationFrame(loop);
+})();
+</script>
+</body>
+</html>
\ No newline at end of file
diff --git a/data/artifacts/cd83de847d24/claude-code.png b/data/artifacts/cd83de847d24/claude-code.png
index 5089113..fa6a231 100644
Binary files a/data/artifacts/cd83de847d24/claude-code.png and b/data/artifacts/cd83de847d24/claude-code.png differ
diff --git a/data/artifacts/cd83de847d24/qwen25-7b.html b/data/artifacts/cd83de847d24/qwen25-7b.html
new file mode 100644
index 0000000..be781dc
--- /dev/null
+++ b/data/artifacts/cd83de847d24/qwen25-7b.html
@@ -0,0 +1,182 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>Scarlet Riverboat Masquerade</title>
+ <style>
+ * {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+ }
+
+ body {
+ overflow: hidden;
+ font-family: 'Courier New', Courier, monospace;
+ }
+
+ .gallery-container {
+ width: 100vw;
+ height: 100vh;
+ perspective: 500px;
+ }
+
+ .layer {
+ position: absolute;
+ will-change: transform;
+ }
+
+ #river, #deck, #overhead {
+ z-index: -1;
+ transition: opacity 2s ease-in-out, transform 3s;
+ }
+
+ #river {
+ width: 100%;
+ height: 50vh;
+ background-image: linear-gradient(to bottom, crimson, darkred);
+ filter: blur(5px);
+ animation: riverFlow 30s infinite linear;
+ }
+
+ #deck {
+ width: 80vw;
+ height: 20vh;
+ background-color: black;
+ transform-origin: center top;
+ animation: deckSpin 60s infinite linear;
+ }
+
+ #overhead {
+ width: 100%;
+ height: 50vh;
+ background-image: radial-gradient(circle, crimson, transparent);
+ filter: blur(2px);
+ opacity: 0.7;
+ animation: overheadLight 30s infinite ease-in-out;
+ }
+
+ .animal-loop {
+ position: absolute;
+ width: 10%;
+ height: 10%;
+ z-index: -5;
+ animation: walk 10s linear infinite;
+ }
+
+ .animal-rabbit, .animal-fox, .animal-heron, .animal-bear {
+ transform-origin: center bottom;
+ animation-play-state: running;
+ }
+
+ @keyframes riverFlow {
+ 0% { transform: translateX(0); }
+ 100% { transform: translateX(-100vw); }
+ }
+
+ @keyframes deckSpin {
+ 0% { transform: rotateY(0deg); }
+ 100% { transform: rotateY(360deg); }
+ }
+
+ @keyframes overheadLight {
+ 0%, 100% { opacity: 0.7; }
+ 50% { opacity: 0.2; }
+ }
+
+ @keyframes walk {
+ 0% { transform: translateY(0px) scale(1); }
+ 50% { transform: translateY(-30px) scale(0.9); }
+ 100% { transform: translateY(0px) scale(1); }
+ }
+
+ .label-note {
+ position: absolute;
+ bottom: 20px;
+ left: 20px;
+ font-size: 14px;
+ color: white;
+ }
+ </style>
+</head>
+<body>
+
+<div class="gallery-container" tabindex="0">
+ <div id="river" class="layer"></div>
+ <div id="deck" class="layer">
+ <div class="animal-loop animal-rabbit" style="left: 20%; top: 15%; transform: rotate(45deg) scale(0.8); background-image: url('data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" width="60" height="60" viewBox="0 0 60 60"%3E%3Cpath fill="%23fff" d="M15 47c9.58 0 16-7.42 16-16S24.58 13 34 13s16 7.42 16 16-7.42 16-16 16zm19-29c0-2.21-1.79-4-4-4s-4 1.79-4 4 1.79 4 4 4zm0 34c-5.53 0-10-4.47-10-10s4.47-10 10-10 10 4.47 10 10-4.47 10-10 10z" /%3E%3C/svg%3E')"></div>
+ <div class="animal-loop animal-fox" style="left: 45%; top: 20%; transform: rotate(-60deg) scale(0.8); background-image: url('data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" width="60" height="60" viewBox="0 0 60 60"%3E%3Cpath fill="%23fff" d="M45.9 18c-.7-1.4-1.8-2.6-3.1-3.3a11.9 11.9 0 0 0-3.9-1.5l-1.2-1A5.1 5.1 0 0 0 37.5 14c-.8-.6-2-.9-3.1-.8s-2.2.5-3.1.8a5.1 5.1 0 0 0-1.2 1.5l-1.2 1A12 12 0 0 1 24 19c-7 0-12.6 5.3-12.6 12s5.6 12 12.6 12c8.7 0 14.3-7.3 14.3-15.9a20.2 20.2 0 0 0-.6-5zm-1.4 5H34v4h-4V25z" /%3E%3C/svg%3E')"></div>
+ <div class="animal-loop animal-heron" style="left: 70%; top: 10%; transform: rotate(90deg) scale(0.8); background-image: url('data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" width="60" height="60" viewBox="0 0 60 60"%3E%3Cpath fill="%23fff" d="M47.8 19h-25c-.6 0-1 .4-1 1v22c0 .6.4 1 1 1h25c.6 0 1-.4 1-1V20c0-.6-.4-1-1-1zm-3 7a8.3 8.3 0 1 1 0-16 8.3 8.3 0 0 1 0 16zm-3 5h-12v2h12v-2z" /%3E%3C/svg%3E')"></div>
+ <div class="animal-loop animal-bear" style="left: 5%; top: 80%; transform: rotate(-90deg) scale(0.6); background-image: url('data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" width="60" height="60" viewBox="0 0 60 60"%3E%3Cpath fill="%23fff" d="M41.5 39c-3 .7-5.8-.5-6.5-2.7a3 3 0 0 0-4.3-1.8L26.5 33H15.5l16.8-12.5a1 1 0 0 0-.9-1.1c-2.8-1.7-5.7-.8-7.5 1.4a3 3 0 0 0-2.5 3.9l-6 3v23h40v-23l-6-3a3 3 0 0 0-2.5-3.9c-1.8-2.7-.7-5.7 1.4-7.5z" /%3E%3C/svg%3E')"></div>
+ </div>
+ <div id="overhead" class="layer"></div>
+
+ <div onclick="toggleLayer('river', this)" tabindex="0">RIVER</div>
+ <div onclick="toggleLayer('deck', this)" tabindex="0">DECK</div>
+ <div onclick="toggleLayer('overhead', this)" tabindex="0">OVERHEAD</div>
+
+ <button onclick="changeChapter('Departure')">Departure</button>
+ <button onclick="changeChapter('Moon Bend')">Moon Bend</button>
+ <button onclick="changeChapter('Dawn Landing')">Dawn Landing</button>
+
+ <label class="label-note" for="gallery">Inspired by the communal spirit of Grateful Dead concerts, this gallery features a fantastical paddlewheel riverboat gliding through a crimson canyon at dusk. Masked dancers perform on three decks while animals celebrate along the shore.</label>
+</div>
+
+<script>
+ let layers = ['river', 'deck', 'overhead'];
+ const chapterMarkers = {
+ Departure: { palette: ['#ff004d', '#4c1f23'], deckSpinSpeed: 5, overheadLightSpeed: 6 },
+ MoonBend: { palette: ['#e01870', '#5b0d1a'], deckSpinSpeed: 3, overheadLightSpeed: 4 },
+ DawnLanding: { palette: ['#ba0f27', '#28090c'], deckSpinSpeed: 6, overheadLightSpeed: 2 }
+ };
+
+ function toggleLayer(layerId, event) {
+ const layer = document.getElementById(layerId);
+ if (layer.style.opacity === '0') {
+ layer.style.opacity = '1';
+ layer.style.transform = 'translateZ(-50px)';
+ event.setAttribute('aria-pressed', 'true');
+ } else {
+ layer.style.opacity = '0';
+ layer.style.transform = 'translateZ(-200px)';
+ event.setAttribute('aria-pressed', 'false');
+ }
+ }
+
+ function changeChapter(chapter) {
+ const currentLayer = document.getElementById(layers[1]);
+ if (currentLayer.style.opacity === '0') return;
+
+ layers.forEach(layer => document.getElementById(layer).style.opacity = '0');
+ setTimeout(() => {
+ for (let i = 0; i < layers.length; i++) {
+ const layer = document.getElementById(layers[i]);
+ layer.style.transform = `translateZ(-50px)`;
+ layer.style.opacity = '1';
+ if (layers[i] === chapter) break;
+ }
+ }, 200);
+
+ updateChapters(chapter);
+ }
+
+ function updateChapters(currentChapter) {
+ layers.forEach(layer => document.getElementById(layer).style.transform = `translateZ(-50px)`);
+
+ let palette = ['#ff004d', '#4c1f23'];
+ if (currentChapter in chapterMarkers) palette = chapterMarkers[currentChapter].palette;
+
+ const deck = document.getElementById('deck');
+ deck.style.animationDuration = `${chapterMarkers[currentChapter].deckSpinSpeed}s`;
+
+ const overhead = document.getElementById('overhead');
+ overhead.style.animationDuration = `${chapterMarkers[currentChapter].overheadLightSpeed}s`;
+
+ palette.forEach(color => {
+ document.documentElement.style.setProperty('--' + color, color);
+ });
+ }
+</script>
+</body>
+</html>
\ No newline at end of file
diff --git a/data/artifacts/cd83de847d24/qwen25-7b.png b/data/artifacts/cd83de847d24/qwen25-7b.png
index a64603f..2cda1a7 100644
Binary files a/data/artifacts/cd83de847d24/qwen25-7b.png and b/data/artifacts/cd83de847d24/qwen25-7b.png differ
diff --git a/data/artifacts/cd83de847d24/qwen3-14b.html b/data/artifacts/cd83de847d24/qwen3-14b.html
new file mode 100644
index 0000000..c5edaca
--- /dev/null
+++ b/data/artifacts/cd83de847d24/qwen3-14b.html
@@ -0,0 +1,384 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <title>Scarlet Riverboat Masquerade</title>
+ <style>
+ * {
+ box-sizing: border-box;
+ margin: 0;
+ padding: 0;
+ }
+ html, body {
+ height: 100%;
+ overflow: hidden;
+ font-family: 'Georgia', serif;
+ background-color: #1a1a1a;
+ color: #fff;
+ position: relative;
+ }
+ #gallery {
+ width: 100%;
+ height: 100%;
+ position: relative;
+ overflow: hidden;
+ background: linear-gradient(to bottom, #4a0033, #220011);
+ }
+ .layer {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ pointer-events: none;
+ will-change: transform;
+ }
+ .river {
+ background: radial-gradient(circle at 50% 50%, #a30029 0%, #6a0016 100%);
+ z-index: 1;
+ }
+ .boat {
+ position: absolute;
+ bottom: 50%;
+ left: 50%;
+ width: 20%;
+ height: 30%;
+ transform: translate(-50%, -50%) rotate(0deg);
+ z-index: 2;
+ }
+ .deck {
+ position: absolute;
+ bottom: 10%;
+ left: 20%;
+ width: 60%;
+ height: 30%;
+ z-index: 3;
+ }
+ .overhead {
+ position: absolute;
+ top: 20%;
+ left: 50%;
+ width: 100%;
+ height: 60%;
+ z-index: 4;
+ }
+ .mask {
+ position: absolute;
+ width: 100px;
+ height: 100px;
+ background: #ffffff22;
+ border-radius: 50%;
+ box-shadow: 0 0 10px #fff;
+ animation: rotate 4s linear infinite;
+ z-index: 5;
+ }
+ .circle {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ width: 200px;
+ height: 200px;
+ border: 2px solid #fff;
+ border-radius: 50%;
+ animation: spin 6s linear infinite;
+ z-index: 5;
+ }
+ .dancer {
+ position: absolute;
+ width: 50px;
+ height: 50px;
+ background: #ff3366;
+ border-radius: 50%;
+ animation: spin 3s linear infinite;
+ z-index: 6;
+ }
+ .animal {
+ position: absolute;
+ width: 80px;
+ height: 80px;
+ background: #fff;
+ border-radius: 50%;
+ box-shadow: 0 0 10px #fff;
+ animation: bounce 5s ease-in-out infinite;
+ z-index: 7;
+ }
+ .musician {
+ position: absolute;
+ width: 60px;
+ height: 60px;
+ background: #00000088;
+ border: 2px solid #fff;
+ border-radius: 50%;
+ animation: sway 8s ease-in-out infinite;
+ z-index: 8;
+ }
+ .lantern {
+ position: absolute;
+ width: 30px;
+ height: 30px;
+ background: #ffaa33;
+ border-radius: 50%;
+ box-shadow: 0 0 15px #ffaa33;
+ animation: pulse 3s ease-in-out infinite;
+ z-index: 9;
+ }
+ .note {
+ position: absolute;
+ bottom: 20px;
+ left: 20px;
+ background: #00000088;
+ padding: 10px;
+ border-radius: 8px;
+ font-size: 14px;
+ line-height: 1.4;
+ max-width: 70%;
+ z-index: 10;
+ }
+ .controls {
+ position: absolute;
+ top: 10px;
+ left: 10px;
+ background: #00000088;
+ padding: 10px;
+ border-radius: 8px;
+ z-index: 100;
+ }
+ .controls label {
+ display: block;
+ margin: 5px 0;
+ }
+ .controls input[type="range"] {
+ width: 100%;
+ }
+ .controls button {
+ margin-top: 10px;
+ padding: 6px 12px;
+ background: #ff3366;
+ color: #fff;
+ border: none;
+ border-radius: 5px;
+ cursor: pointer;
+ font-size: 14px;
+ }
+ .controls button:focus {
+ outline: 2px solid #ffaa33;
+ }
+ @keyframes rotate {
+ 0% { transform: rotate(0deg); }
+ 100% { transform: rotate(360deg); }
+ }
+ @keyframes spin {
+ 0% { transform: rotate(0deg); }
+ 100% { transform: rotate(360deg); }
+ }
+ @keyframes bounce {
+ 0%, 100% { transform: translateY(0); }
+ 50% { transform: translateY(-20px); }
+ }
+ @keyframes sway {
+ 0% { transform: rotate(0deg); }
+ 50% { transform: rotate(10deg); }
+ 100% { transform: rotate(0deg); }
+ }
+ @keyframes pulse {
+ 0%, 100% { opacity: 0.4; }
+ 50% { opacity: 1; }
+ }
+ </style>
+</head>
+<body>
+ <div id="gallery">
+ <div class="layer river" id="river"></div>
+ <div class="layer boat" id="boat">
+ <div class="mask" style="left: 20%; top: 20%;"></div>
+ <div class="mask" style="left: 40%; top: 40%;"></div>
+ <div class="mask" style="left: 60%; top: 60%;"></div>
+ <div class="mask" style="left: 80%; top: 80%;"></div>
+ </div>
+ <div class="layer deck" id="deck">
+ <div class="circle" style="left: 50%; top: 50%;"></div>
+ <div class="dancer" style="left: 20%; top: 20%;"></div>
+ <div class="dancer" style="left: 40%; top: 40%;"></div>
+ <div class="dancer" style="left: 60%; top: 60%;"></div>
+ <div class="dancer" style="left: 80%; top: 80%;"></div>
+ </div>
+ <div class="layer overhead" id="overhead">
+ <div class="lantern" style="left: 10%; top: 10%;"></div>
+ <div class="lantern" style="left: 30%; top: 30%;"></div>
+ <div class="lantern" style="left: 50%; top: 50%;"></div>
+ <div class="lantern" style="left: 70%; top: 70%;"></div>
+ <div class="lantern" style="left: 90%; top: 90%;"></div>
+ </div>
+ <div class="layer" id="shore">
+ <div class="animal fox" style="left: 10%; top: 10%;"></div>
+ <div class="animal heron" style="left: 30%; top: 30%;"></div>
+ <div class="animal rabbit" style="left: 50%; top: 50%;"></div>
+ <div class="animal bear" style="left: 70%; top: 70%;"></div>
+ </div>
+ <div class="layer" id="music">
+ <div class="musician" style="left: 20%; top: 20%;"></div>
+ <div class="musician" style="left: 40%; top: 40%;"></div>
+ <div class="musician" style="left: 60%; top: 60%;"></div>
+ <div class="musician" style="left: 80%; top: 80%;"></div>
+ </div>
+ <div class="note">
+ <strong>Visual Language Note:</strong><br>
+ This gallery is inspired by the exploratory, communal spirit of Grateful Dead concerts, with a focus on visual rhythm, layered choreography, and surreal animation. No band logos or album art are used; instead, the experience is built through abstract silhouettes, masked figures, and natural elements in motion. Animations are carefully synchronized to create a sense of flow, tempo, and spatial harmony.
+ </div>
+ <div class="controls">
+ <label>Camera View:
+ <select id="camera" aria-label="Camera View">
+ <option value="river">Wide River</option>
+ <option value="deck">Deck</option>
+ <option value="overhead">Overhead</option>
+ </select>
+ </label>
+ <label>Visual Tempo:
+ <input type="range" id="tempo" min="1" max="4" step="0.1" value="2" aria-label="Visual Tempo">
+ </label>
+ <label>Lantern Intensity:
+ <input type="range" id="lantern" min="0" max="1" step="0.1" value="0.6" aria-label="Lantern Intensity">
+ </label>
+ <label>Crowd Density:
+ <input type="range" id="crowd" min="1" max="5" step="1" value="3" aria-label="Crowd Density">
+ </label>
+ <label>Animal Visibility:
+ <input type="range" id="animal" min="0" max="1" step="0.1" value="0.8" aria-label="Animal Visibility">
+ </label>
+ <label>Motion Trails:
+ <input type="range" id="motion" min="0" max="1" step="0.1" value="0.5" aria-label="Motion Trails">
+ </label>
+ <button id="pause" aria-label="Pause Animation">Pause</button>
+ <button id="restart" aria-label="Restart Animation">Restart</button>
+ <button id="reduce" aria-label="Enable Reduced Motion">Reduce Motion</button>
+ </div>
+ </div>
+
+ <script>
+ const gallery = document.getElementById('gallery');
+ const river = document.getElementById('river');
+ const boat = document.getElementById('boat');
+ const deck = document.getElementById('deck');
+ const overhead = document.getElementById('overhead');
+ const shore = document.getElementById('shore');
+ const music = document.getElementById('music');
+
+ const controls = {
+ camera: document.getElementById('camera'),
+ tempo: document.getElementById('tempo'),
+ lantern: document.getElementById('lantern'),
+ crowd: document.getElementById('crowd'),
+ animal: document.getElementById('animal'),
+ motion: document.getElementById('motion'),
+ pause: document.getElementById('pause'),
+ restart: document.getElementById('restart'),
+ reduce: document.getElementById('reduce')
+ };
+
+ const elements = {
+ dancers: deck.querySelectorAll('.dancer'),
+ masks: boat.querySelectorAll('.mask'),
+ circle: deck.querySelector('.circle'),
+ lanterns: overhead.querySelectorAll('.lantern'),
+ fox: shore.querySelector('.fox'),
+ heron: shore.querySelector('.heron'),
+ rabbit: shore.querySelector('.rabbit'),
+ bear: shore.querySelector('.bear'),
+ musicians: music.querySelectorAll('.musician')
+ };
+
+ let isPaused = false;
+ let reducedMotion = false;
+
+ function animate() {
+ if (isPaused) return;
+
+ // Update lanterns
+ elements.lanterns.forEach(lantern => {
+ lantern.style.opacity = controls.lantern.value;
+ });
+
+ // Update dancers
+ const tempo = parseFloat(controls.tempo.value);
+ elements.dancers.forEach(dancer => {
+ dancer.style.animationDuration = `${tempo}s`;
+ });
+
+ // Update circle rotation
+ elements.circle.style.animationDuration = `${tempo * 1.5}s`;
+
+ // Update animal visibility
+ elements.fox.style.opacity = controls.animal.value;
+ elements.heron.style.opacity = controls.animal.value;
+ elements.rabbit.style.opacity = controls.animal.value;
+ elements.bear.style.opacity = controls.animal.value;
+
+ // Update motion trails
+ elements.dancers.forEach(dancer => {
+ dancer.style.animationTimingFunction = controls.motion.value > 0.5 ? 'cubic-bezier(0.4, 0, 0.2, 1)' : 'linear';
+ });
+
+ // Update musicians (sway)
+ elements.musicians.forEach(musician => {
+ musician.style.animationDuration = `${tempo * 2}s`;
+ });
+ }
+
+ function updateCamera() {
+ const view = controls.camera.value;
+ if (view === 'river') {
+ river.style.transform = 'translateY(0)';
+ deck.style.transform = 'translateY(100%)';
+ overhead.style.transform = 'translateY(100%)';
+ } else if (view === 'deck') {
+ river.style.transform = 'translateY(-100%)';
+ deck.style.transform = 'translateY(0)';
+ overhead.style.transform = 'translateY(100%)';
+ } else if (view === 'overhead') {
+ river.style.transform = 'translateY(-100%)';
+ deck.style.transform = 'translateY(-100%)';
+ overhead.style.transform = 'translateY(0)';
+ }
+ }
+
+ function togglePause() {
+ isPaused = !isPaused;
+ controls.pause.textContent = isPaused ? 'Resume' : 'Pause';
+ }
+
+ function restart() {
+ isPaused = false;
+ controls.pause.textContent = 'Pause';
+ animate();
+ }
+
+ function reduceMotionToggle() {
+ reducedMotion = !reducedMotion;
+ controls.reduce.textContent = reducedMotion ? 'Enable Motion' : 'Reduce Motion';
+ elements.dancers.forEach(dancer => {
+ dancer.style.animationTimingFunction = reducedMotion ? 'linear' : 'cubic-bezier(0.4, 0, 0.2, 1)';
+ });
+ }
+
+ controls.camera.addEventListener('change', updateCamera);
+ controls.tempo.addEventListener('input', animate);
+ controls.lantern.addEventListener('input', animate);
+ controls.crowd.addEventListener('input', () => {
+ const count = parseInt(controls.crowd.value);
+ const max = 10;
+ elements.dancers.forEach((d, i) => {
+ d.style.display = i < count ? 'block' : 'none';
+ });
+ });
+ controls.animal.addEventListener('input', animate);
+ controls.motion.addEventListener('input', animate);
+ controls.pause.addEventListener('click', togglePause);
+ controls.restart.addEventListener('click', restart);
+ controls.reduce.addEventListener('click', reduceMotionToggle);
+
+ // Initial setup
+ updateCamera();
+ animate();
+ </script>
+</body>
+</html>
\ No newline at end of file
diff --git a/data/artifacts/ec9e68a64f43/claude-code.html b/data/artifacts/ec9e68a64f43/claude-code.html
new file mode 100644
index 0000000..9b1817f
--- /dev/null
+++ b/data/artifacts/ec9e68a64f43/claude-code.html
@@ -0,0 +1,794 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="UTF-8">
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+<title>Golden Road Carnival of Mirrors — A Kinetic Poster-World</title>
+<style>
+ :root{
+ --ink:#f7e9c8; --paper:#0b0614; --gold:#f4b83a; --mag:#e0459b;
+ --teal:#38d6c0; --violet:#8a5cff; --edge:#26183a;
+ --focus:#7cf5ff;
+ --panel:#150a26cc;
+ }
+ *{box-sizing:border-box}
+ html,body{margin:0;padding:0;background:
+ radial-gradient(120% 90% at 50% -10%, #241041 0%, #120826 45%, #070311 100%);
+ color:var(--ink);font-family:"Trebuchet MS","Segoe UI",system-ui,sans-serif;}
+ a{color:var(--teal)}
+ .wrap{max-width:1400px;margin:0 auto;padding:18px clamp(10px,2.5vw,28px) 60px;}
+
+ /* ---------- Hand-lettered psychedelic title (original lettering) ---------- */
+ header.marquee{text-align:center;position:relative;padding:6px 0 10px;}
+ .kicker{letter-spacing:.42em;text-transform:uppercase;font-size:clamp(9px,1.6vw,13px);
+ color:var(--teal);opacity:.85;margin:0 0 4px;}
+ h1.title{margin:0;line-height:.92;font-weight:900;
+ font-size:clamp(30px,7.4vw,84px);
+ display:flex;flex-wrap:wrap;justify-content:center;gap:.02em;
+ filter:drop-shadow(0 4px 0 #5b1741) drop-shadow(0 0 26px #e0459b55);}
+ h1.title span{display:inline-block;
+ background:linear-gradient(180deg,#fff1c4 0%,var(--gold) 38%,var(--mag) 78%,var(--violet) 100%);
+ -webkit-background-clip:text;background-clip:text;color:transparent;
+ text-shadow:0 0 1px #0000;transform-origin:50% 80%;}
+ .sub{margin:8px auto 0;max-width:760px;font-style:italic;color:#e9d7ff;opacity:.86;
+ font-size:clamp(12px,2vw,16px)}
+ .rays{position:absolute;inset:-30% -10% auto -10%;height:180%;z-index:-1;pointer-events:none;
+ background:repeating-conic-gradient(from 0deg at 50% 30%,
+ #f4b83a10 0deg 7deg,#0000 7deg 14deg);opacity:.5;mix-blend-mode:screen}
+
+ /* ---------- Stage ---------- */
+ .stagewrap{position:relative;margin-top:14px;border-radius:16px;overflow:hidden;
+ border:2px solid #3a2258;box-shadow:0 20px 60px #000a, inset 0 0 90px #1a0d33;}
+ canvas#stage{display:block;width:100%;height:auto;aspect-ratio:16/9;background:#0a0518;touch-action:none}
+ .phaseband{position:absolute;top:10px;left:12px;display:flex;gap:8px;align-items:center;
+ font-size:12px;letter-spacing:.18em;text-transform:uppercase;
+ background:#0b0620aa;border:1px solid #4a2f70;padding:6px 12px;border-radius:999px;
+ backdrop-filter:blur(4px)}
+ .phasedot{width:9px;height:9px;border-radius:50%;background:var(--gold);
+ box-shadow:0 0 12px var(--gold);animation:pulse 2.4s ease-in-out infinite}
+ @keyframes pulse{50%{transform:scale(1.5);opacity:.6}}
+ .badge{position:absolute;bottom:10px;right:12px;font-size:11px;letter-spacing:.12em;
+ color:#cbb9ff;background:#0b0620aa;border:1px solid #4a2f70;padding:5px 10px;border-radius:8px}
+
+ /* ---------- Controls ---------- */
+ .controls{margin-top:16px;display:grid;gap:14px;
+ grid-template-columns:repeat(auto-fit,minmax(210px,1fr));
+ background:var(--panel);border:1px solid #3a2258;border-radius:14px;padding:16px;
+ backdrop-filter:blur(6px)}
+ fieldset{border:1px solid #40295f;border-radius:10px;margin:0;padding:10px 12px 12px}
+ legend{font-size:11px;letter-spacing:.18em;text-transform:uppercase;color:var(--gold);padding:0 6px}
+ .ctl{display:flex;flex-direction:column;gap:5px;margin-top:6px}
+ .ctl label{font-size:12px;color:#e6dbff;display:flex;justify-content:space-between;gap:8px}
+ .ctl output{color:var(--teal);font-variant-numeric:tabular-nums}
+ input[type=range]{width:100%;accent-color:var(--mag);height:22px}
+ select,button{font-family:inherit;font-size:13px;color:var(--ink);
+ background:#1c1030;border:1px solid #5a3a86;border-radius:8px;padding:8px 10px;cursor:pointer}
+ .btnrow{display:flex;flex-wrap:wrap;gap:8px;margin-top:8px}
+ button{transition:transform .08s ease, background .15s, box-shadow .15s;font-weight:700;letter-spacing:.04em}
+ button:hover{background:#2a1848;box-shadow:0 0 0 1px #7a52c0 inset}
+ button:active{transform:translateY(2px) scale(.98)}
+ button[aria-pressed=true]{background:linear-gradient(180deg,var(--mag),#7a1f5a);
+ border-color:var(--gold);box-shadow:0 0 16px #e0459b66}
+ .toggle{display:flex;gap:8px;align-items:center}
+ input[type=checkbox]{width:20px;height:20px;accent-color:var(--teal)}
+ /* strong focus states */
+ a:focus-visible,button:focus-visible,select:focus-visible,input:focus-visible,summary:focus-visible{
+ outline:3px solid var(--focus);outline-offset:2px;border-radius:6px}
+ input[type=range]:focus-visible{outline:3px solid var(--focus);outline-offset:4px}
+
+ .cameraseg{display:flex;gap:6px;flex-wrap:wrap}
+ .cameraseg button{flex:1 1 auto;min-width:88px}
+
+ /* curator note */
+ .note{margin-top:18px;display:grid;grid-template-columns:auto 1fr;gap:14px;align-items:start;
+ background:#100822cc;border:1px solid #3a2258;border-left:4px solid var(--gold);
+ border-radius:12px;padding:16px 18px}
+ .note h2{margin:0 0 6px;font-size:14px;letter-spacing:.2em;text-transform:uppercase;color:var(--gold)}
+ .note p{margin:0;font-size:13.5px;line-height:1.6;color:#e7dcff;max-width:70ch}
+ .seal{width:54px;height:54px;border-radius:50%;background:
+ conic-gradient(from 0deg,var(--gold),var(--mag),var(--teal),var(--violet),var(--gold));
+ display:grid;place-items:center;color:#12061f;font-weight:900;font-size:11px;text-align:center;
+ box-shadow:0 0 18px #f4b83a55}
+ .legend-key{margin-top:10px;font-size:11.5px;color:#c9bbe6;display:flex;flex-wrap:wrap;gap:12px}
+ .legend-key span{display:inline-flex;align-items:center;gap:6px}
+ .swatch{width:12px;height:12px;border-radius:3px;display:inline-block}
+ .skip{position:absolute;left:-999px;top:0}
+ .skip:focus{left:8px;top:8px;z-index:9;background:#000;padding:8px 12px;border-radius:8px}
+ footer{margin-top:26px;text-align:center;font-size:11px;color:#8f7fb5;letter-spacing:.1em}
+ .sr-only{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0)}
+</style>
+</head>
+<body>
+<a class="skip" href="#controls">Skip to controls</a>
+<div class="wrap">
+
+ <header class="marquee">
+ <div class="rays" aria-hidden="true"></div>
+ <p class="kicker">A Kinetic Paper-Cut Midway · Authored Visual Score</p>
+ <h1 class="title" aria-label="Golden Road Carnival of Mirrors" id="titleLettering"></h1>
+ <p class="sub">Striped tents, mirrored arches & hand-lettered signs unfold in layers while an ensemble spins, trades reflections, and dances the long golden road home.</p>
+ </header>
+
+ <div class="stagewrap">
+ <canvas id="stage" role="img"
+ aria-label="Animated poster of a dreamlike carnival midway. A ring of dancers expands and contracts around a lit carousel, stilt walkers cross in counterpoint, people spin through mirror portals whose reflections lag slightly behind, and dancing animals — a shuffling bear, weaving dogs, fanning peacocks, swinging monkeys, and a patient tortoise — move through striped tents and mirrored arches. The scene cycles through three moods: Invitation, Mirror Jam, and Last Lantern."></canvas>
+ <div class="phaseband"><span class="phasedot" aria-hidden="true"></span><span id="phaseName">Invitation</span></div>
+ <div class="badge" id="fpsBadge">score · running</div>
+ </div>
+
+ <p class="sr-only" role="status" aria-live="polite" id="live"></p>
+
+ <section class="controls" id="controls" aria-label="Choreography controls">
+ <fieldset>
+ <legend>Camera Route</legend>
+ <div class="cameraseg" role="group" aria-label="Choose a camera route">
+ <button type="button" data-cam="0" aria-pressed="true">Promenade</button>
+ <button type="button" data-cam="1" aria-pressed="false">Carousel Orbit</button>
+ <button type="button" data-cam="2" aria-pressed="false">Lantern Rise</button>
+ </div>
+ <div class="ctl" style="margin-top:12px">
+ <label for="animal">Animal focus <output id="animalOut">Whole troupe</output></label>
+ <select id="animal">
+ <option value="all">Whole troupe</option>
+ <option value="bear">Bear · heel-toe shuffle</option>
+ <option value="dogs">Dogs · weaving the legs</option>
+ <option value="peacocks">Peacocks · fan patterns</option>
+ <option value="monkeys">Monkeys · sign-to-sign</option>
+ <option value="tortoise">Tortoise · patient crossing</option>
+ </select>
+ </div>
+ </fieldset>
+
+ <fieldset>
+ <legend>Motion</legend>
+ <div class="ctl">
+ <label for="tempo">Tempo <output id="tempoOut">1.00×</output></label>
+ <input id="tempo" type="range" min="0" max="220" value="100">
+ </div>
+ <div class="ctl">
+ <label for="delay">Reflection delay <output id="delayOut">0.42 s</output></label>
+ <input id="delay" type="range" min="0" max="120" value="42">
+ </div>
+ <div class="ctl">
+ <label for="trail">Trail length <output id="trailOut">18</output></label>
+ <input id="trail" type="range" min="0" max="60" value="18">
+ </div>
+ </fieldset>
+
+ <fieldset>
+ <legend>Crowd & Overlay</legend>
+ <div class="ctl">
+ <label for="density">Crowd density <output id="densityOut">medium</output></label>
+ <input id="density" type="range" min="0" max="100" value="55">
+ </div>
+ <div class="toggle" style="margin-top:12px">
+ <input type="checkbox" id="diagram">
+ <label for="diagram">Diagram overlay — expose each choreography loop</label>
+ </div>
+ <div class="toggle" style="margin-top:10px">
+ <input type="checkbox" id="reduced">
+ <label for="reduced">Reduced motion</label>
+ </div>
+ </fieldset>
+
+ <fieldset>
+ <legend>Transport</legend>
+ <div class="btnrow">
+ <button type="button" id="playBtn" aria-pressed="true">⏸ Pause</button>
+ <button type="button" id="restartBtn">↺ Restart</button>
+ </div>
+ <div class="legend-key" aria-hidden="true">
+ <span><i class="swatch" style="background:var(--gold)"></i>ring dance</span>
+ <span><i class="swatch" style="background:var(--teal)"></i>stilts</span>
+ <span><i class="swatch" style="background:var(--mag)"></i>portals</span>
+ <span><i class="swatch" style="background:var(--violet)"></i>animals</span>
+ </div>
+ </fieldset>
+ </section>
+
+ <section class="note" aria-label="Curator note">
+ <div class="seal" aria-hidden="true">GRC<br>M</div>
+ <div>
+ <h2>Curator Note</h2>
+ <p><strong>This is an authored visual score, not audio-reactive analysis.</strong> Every motion — the breathing ring around the carousel, the counterpoint of the stilt walkers, the slightly-late echoes in the mirror portals, the bear's weighted heel-toe, the tortoise's collision-free line — is composed by hand as a repeating choreography. Nothing here listens to sound; the rhythm is written into the geometry. The three movements <em>Invitation → Mirror Jam → Last Lantern</em> shift color, scale and energy, then return seamlessly to the beginning, so the poster can loop forever like a lantern turning on the golden road. Lettering and ornament are drawn fresh in a psychedelic concert-poster spirit — no existing marks are reproduced.</p>
+ </div>
+ </section>
+
+ <footer>Golden Road Carnival of Mirrors · a single-file kinetic artwork · open the diagram overlay to read the loops</footer>
+</div>
+
+<script>
+(() => {
+"use strict";
+
+/* ---------- hand-letter the title with playful per-letter warp ---------- */
+(() => {
+ const txt = "Golden Road Carnival of Mirrors";
+ const el = document.getElementById('titleLettering');
+ [...txt].forEach((ch,i) => {
+ const s = document.createElement('span');
+ s.textContent = ch === ' ' ? '\u00A0\u00A0' : ch;
+ const rot = Math.sin(i*0.9)*6;
+ const sk = Math.cos(i*0.7)*4;
+ s.style.transform = `rotate(${rot}deg) skewX(${sk}deg)`;
+ el.appendChild(s);
+ });
+})();
+
+/* ---------- setup ---------- */
+const cv = document.getElementById('stage');
+const ctx = cv.getContext('2d');
+const WORLD_W = 1600, WORLD_H = 900;
+let dpr = Math.min(window.devicePixelRatio || 1, 2);
+let cssW = 0, cssH = 0;
+
+function resize(){
+ const r = cv.getBoundingClientRect();
+ cssW = r.width; cssH = r.height;
+ dpr = Math.min(window.devicePixelRatio || 1, 2);
+ cv.width = Math.round(cssW*dpr);
+ cv.height = Math.round(cssH*dpr);
+}
+new ResizeObserver(resize).observe(cv);
+resize();
+
+/* ---------- state ---------- */
+const state = {
+ cam: 0, tempo: 1, delay: 0.42, trail: 18, density: 0.55,
+ animal: 'all', diagram: false, reduced: false, playing: true,
+};
+let clock = 0; // choreography time in "beats"
+let last = performance.now();
+const trails = new Map(); // id -> [{x,y}...]
+
+/* respect OS reduced motion at load */
+if(window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches){
+ state.reduced = true; document.getElementById('reduced').checked = true;
+}
+
+/* ---------- palettes per movement ---------- */
+const PH = [
+ { name:'Invitation', bg1:'#1a0b30', bg2:'#0a0518', glow:'#f4b83a',
+ tentA:'#e0459b', tentB:'#f4b83a', ink:'#ffe9b8', accent:'#38d6c0', energy:0.72, warmth:1 },
+ { name:'Mirror Jam', bg1:'#2a0a44', bg2:'#0c0322', glow:'#8a5cff',
+ tentA:'#38d6c0', tentB:'#e0459b', ink:'#eafcff', accent:'#8a5cff', energy:1.15, warmth:0.35 },
+ { name:'Last Lantern', bg1:'#2b1338', bg2:'#0b0410', glow:'#ff7a3c',
+ tentA:'#ff7a3c', tentB:'#b7285f', ink:'#ffd9a8', accent:'#ffb04a', energy:0.6, warmth:1.25 },
+];
+const PHASE_LEN = 14; // beats per movement
+const CYCLE = PHASE_LEN*PH.length;
+
+function hexToRgb(h){const n=parseInt(h.slice(1),16);return[n>>16&255,n>>8&255,n&255];}
+function mix(a,b,t){const A=hexToRgb(a),B=hexToRgb(b);
+ return `rgb(${Math.round(A[0]+(B[0]-A[0])*t)},${Math.round(A[1]+(B[1]-A[1])*t)},${Math.round(A[2]+(B[2]-A[2])*t)})`;}
+function mixN(...args){/* unused */}
+
+function phaseAt(t){
+ const f = ((t % CYCLE)+CYCLE)%CYCLE / PHASE_LEN; // 0..3
+ const i = Math.floor(f), frac = f - i;
+ const a = PH[i%3], b = PH[(i+1)%3];
+ // ease crossfade
+ const e = frac*frac*(3-2*frac);
+ return {
+ idx:i%3, frac,
+ name:a.name,
+ bg1:mix(a.bg1,b.bg1,e), bg2:mix(a.bg2,b.bg2,e),
+ glow:mix(a.glow,b.glow,e), tentA:mix(a.tentA,b.tentA,e), tentB:mix(a.tentB,b.tentB,e),
+ ink:mix(a.ink,b.ink,e), accent:mix(a.accent,b.accent,e),
+ energy:a.energy+(b.energy-a.energy)*e,
+ warmth:a.warmth+(b.warmth-a.warmth)*e,
+ };
+}
+
+/* ---------- camera routes ---------- */
+function camera(t){
+ const R = ((t)*0.5);
+ if(state.cam===0){ // Promenade: slow lateral sweep
+ return { cx: WORLD_W/2 + Math.sin(R*0.5)*260, cy: WORLD_H*0.52, zoom: 1.02 + Math.sin(R*0.33)*0.05 };
+ } else if(state.cam===1){ // Carousel orbit
+ return { cx: WORLD_W/2 + Math.cos(R*0.6)*220, cy: WORLD_H*0.48 + Math.sin(R*0.6)*90, zoom: 1.32 + Math.sin(R*0.4)*0.12 };
+ } else { // Lantern rise
+ const rise = (Math.sin(R*0.35)*0.5+0.5);
+ return { cx: WORLD_W/2 + Math.sin(R*0.2)*120, cy: WORLD_H*(0.62-0.30*rise), zoom: 1.1 + rise*0.5 };
+ }
+}
+
+/* transform helper */
+function applyCam(cam){
+ const scale = (cssW/WORLD_W)*cam.zoom;
+ ctx.setTransform(dpr*scale,0,0,dpr*scale,
+ dpr*(cssW/2 - cam.cx*scale), dpr*(cssH/2 - cam.cy*scale));
+ return scale;
+}
+
+/* ---------- figure drawing primitives (paper-cut) ---------- */
+function person(x,y,s,rot,col,limb,tall){
+ ctx.save(); ctx.translate(x,y); ctx.rotate(rot); ctx.scale(s,s);
+ const legL = tall? 46 : 22;
+ const sw = Math.sin(limb), sw2 = Math.sin(limb+Math.PI);
+ ctx.strokeStyle=col; ctx.lineWidth=tall?4.5:6; ctx.lineCap='round';
+ // legs
+ ctx.beginPath(); ctx.moveTo(0,0); ctx.lineTo(sw*8, legL); ctx.moveTo(0,0); ctx.lineTo(sw2*8, legL); ctx.stroke();
+ // torso (paper triangle skirt)
+ ctx.fillStyle=col; ctx.beginPath();
+ ctx.moveTo(-9,-18); ctx.lineTo(9,-18); ctx.lineTo(6,2); ctx.lineTo(-6,2); ctx.closePath(); ctx.fill();
+ // arms
+ ctx.beginPath(); ctx.moveTo(0,-14); ctx.lineTo(sw2*15,-6); ctx.moveTo(0,-14); ctx.lineTo(sw*15,-6); ctx.stroke();
+ // head
+ ctx.beginPath(); ctx.arc(0,-24,6.5,0,7); ctx.fill();
+ ctx.restore();
+}
+
+function stiltWalker(x,y,s,rot,col,limb){
+ ctx.save(); ctx.translate(x,y); ctx.rotate(rot); ctx.scale(s,s);
+ const sw=Math.sin(limb);
+ ctx.strokeStyle=col; ctx.lineWidth=4; ctx.lineCap='round';
+ ctx.beginPath(); ctx.moveTo(-4,-30); ctx.lineTo(-4+sw*6,60); ctx.moveTo(4,-30); ctx.lineTo(4-sw*6,60); ctx.stroke();
+ ctx.fillStyle=col;
+ ctx.beginPath(); ctx.moveTo(-11,-46); ctx.lineTo(11,-46); ctx.lineTo(6,-28); ctx.lineTo(-6,-28); ctx.closePath(); ctx.fill();
+ ctx.beginPath(); ctx.moveTo(0,-40); ctx.lineTo(sw*20,-52); ctx.moveTo(0,-40); ctx.lineTo(-sw*20,-52); ctx.stroke();
+ ctx.beginPath(); ctx.arc(0,-54,7,0,7); ctx.fill();
+ // little flag topper
+ ctx.fillStyle='#fff2c0'; ctx.beginPath(); ctx.moveTo(0,-61); ctx.lineTo(10,-58); ctx.lineTo(0,-55); ctx.closePath(); ctx.fill();
+ ctx.restore();
+}
+
+function bear(x,y,s,limb,col,hi){
+ ctx.save(); ctx.translate(x,y); ctx.scale(s,s);
+ const rock=Math.sin(limb)*0.14; ctx.rotate(rock);
+ const foot=Math.sin(limb*2); // heel-toe lift
+ ctx.fillStyle=col;
+ ctx.beginPath(); ctx.ellipse(0,-6,26,30,0,0,7); ctx.fill(); // body
+ ctx.beginPath(); ctx.arc(0,-40,17,0,7); ctx.fill(); // head
+ ctx.beginPath(); ctx.arc(-13,-52,7,0,7); ctx.arc(13,-52,7,0,7); ctx.fill(); // ears
+ // muzzle
+ ctx.fillStyle=hi; ctx.beginPath(); ctx.ellipse(0,-36,8,6,0,0,7); ctx.fill();
+ ctx.fillStyle='#1a0d0a'; ctx.beginPath(); ctx.arc(0,-40,2.4,0,7); ctx.fill();
+ // heel-toe feet
+ ctx.fillStyle=col;
+ ctx.beginPath(); ctx.ellipse(-13,24-foot*7,11,7,0,0,7); ctx.fill();
+ ctx.beginPath(); ctx.ellipse(13,24+foot*7,11,7,0,0,7); ctx.fill();
+ // arms swinging
+ ctx.strokeStyle=col; ctx.lineWidth=10; ctx.lineCap='round';
+ ctx.beginPath(); ctx.moveTo(-18,-8); ctx.lineTo(-30,6+foot*5); ctx.moveTo(18,-8); ctx.lineTo(30,6-foot*5); ctx.stroke();
+ ctx.restore();
+}
+
+function dog(x,y,s,limb,col){
+ ctx.save(); ctx.translate(x,y); ctx.scale(s,s);
+ const g=Math.sin(limb*3);
+ ctx.fillStyle=col;
+ ctx.beginPath(); ctx.ellipse(0,0,18,9,0,0,7); ctx.fill(); // body
+ ctx.beginPath(); ctx.ellipse(16,-6,8,7,0,0,7); ctx.fill(); // head
+ ctx.beginPath(); ctx.moveTo(20,-12);ctx.lineTo(24,-4);ctx.lineTo(18,-4);ctx.closePath();ctx.fill(); // ear
+ ctx.strokeStyle=col; ctx.lineWidth=4; ctx.lineCap='round';
+ ctx.beginPath(); ctx.moveTo(-10,6);ctx.lineTo(-10+g*4,16); ctx.moveTo(8,6);ctx.lineTo(8-g*4,16); ctx.stroke();
+ ctx.beginPath(); ctx.moveTo(-16,-2);ctx.quadraticCurveTo(-26,-8,-22,2); ctx.stroke(); // tail
+ ctx.restore();
+}
+
+function peacock(x,y,s,open,col,accent){
+ ctx.save(); ctx.translate(x,y); ctx.scale(s,s);
+ // fan
+ const N=11, spread=(0.15+open*0.85)*1.5;
+ for(let i=0;i<N;i++){
+ const a=(i/(N-1)-0.5)*spread - Math.PI/2;
+ ctx.strokeStyle= i%2? accent: col; ctx.lineWidth=3;
+ ctx.beginPath(); ctx.moveTo(0,0);
+ const ex=Math.cos(a)*46*(0.7+open*0.5), ey=Math.sin(a)*46*(0.7+open*0.5);
+ ctx.lineTo(ex,ey); ctx.stroke();
+ ctx.fillStyle= i%2? col:accent; ctx.beginPath(); ctx.arc(ex,ey,4.5,0,7); ctx.fill();
+ ctx.fillStyle='#0a0518'; ctx.beginPath(); ctx.arc(ex,ey,1.8,0,7); ctx.fill();
+ }
+ // body
+ ctx.fillStyle=col; ctx.beginPath(); ctx.ellipse(0,6,8,13,0,0,7); ctx.fill();
+ ctx.beginPath(); ctx.arc(0,-8,6,0,7); ctx.fill();
+ ctx.strokeStyle=col;ctx.lineWidth=3; ctx.beginPath();ctx.moveTo(0,18);ctx.lineTo(-4,28);ctx.moveTo(0,18);ctx.lineTo(4,28);ctx.stroke();
+ ctx.restore();
+}
+
+function monkey(x,y,s,swing,col){
+ ctx.save(); ctx.translate(x,y); ctx.rotate(swing*0.5); ctx.scale(s,s);
+ ctx.fillStyle=col;
+ ctx.beginPath(); ctx.ellipse(0,0,10,14,0,0,7); ctx.fill(); // body
+ ctx.beginPath(); ctx.arc(0,-18,8,0,7); ctx.fill(); // head
+ ctx.strokeStyle=col; ctx.lineWidth=4; ctx.lineCap='round';
+ // arms reaching up (holding sign line)
+ ctx.beginPath(); ctx.moveTo(-6,-14);ctx.lineTo(-14,-30); ctx.moveTo(6,-14);ctx.lineTo(14,-30); ctx.stroke();
+ // legs
+ ctx.beginPath(); ctx.moveTo(-4,12);ctx.lineTo(-9,24); ctx.moveTo(4,12);ctx.lineTo(9,24); ctx.stroke();
+ // curly tail
+ ctx.beginPath(); ctx.moveTo(0,10); ctx.quadraticCurveTo(22,20,10,-4); ctx.stroke();
+ ctx.restore();
+}
+
+function tortoise(x,y,s,limb,col,accent){
+ ctx.save(); ctx.translate(x,y); ctx.scale(s,s);
+ const step=Math.sin(limb*2);
+ ctx.fillStyle=col;
+ ctx.beginPath(); ctx.ellipse(0,0,28,18,0,Math.PI,0); ctx.fill(); // shell
+ ctx.fillStyle=accent;
+ for(let i=-2;i<=2;i++){ ctx.beginPath(); ctx.ellipse(i*10,-4,4.5,6,0,0,7); ctx.fill(); }
+ ctx.fillStyle=col;
+ ctx.beginPath(); ctx.ellipse(30,2,9,6,0,0,7); ctx.fill(); // head
+ ctx.fillStyle='#0a0518'; ctx.beginPath(); ctx.arc(33,1,1.6,0,7); ctx.fill();
+ ctx.strokeStyle=col; ctx.lineWidth=5; ctx.lineCap='round';
+ ctx.beginPath(); ctx.moveTo(-16,6);ctx.lineTo(-18,12+step*3); ctx.moveTo(14,6);ctx.lineTo(16,12-step*3); ctx.stroke();
+ ctx.restore();
+}
+
+/* ---------- scenery: layered paper-cut midway ---------- */
+function stripedTent(x,y,w,h,cA,cB,n){
+ ctx.save();
+ for(let i=0;i<n;i++){
+ ctx.fillStyle= i%2? cA:cB;
+ const x0=x - w/2 + i*(w/n), x1=x - w/2 + (i+1)*(w/n);
+ ctx.beginPath(); ctx.moveTo(x0,y); ctx.lineTo(x1,y); ctx.lineTo(x,y-h); ctx.closePath(); ctx.fill();
+ }
+ // scalloped hem
+ ctx.fillStyle=cB;
+ const sc=w/12;
+ for(let i=0;i<12;i++){ ctx.beginPath(); ctx.arc(x-w/2+sc*i+sc/2, y, sc/2,0,Math.PI); ctx.fill(); }
+ ctx.restore();
+}
+function mirroredArch(x,y,w,h,glow,ink,t){
+ ctx.save();
+ // arch frame
+ ctx.strokeStyle=glow; ctx.lineWidth=10;
+ ctx.beginPath(); ctx.moveTo(x-w/2,y); ctx.lineTo(x-w/2,y-h*0.55);
+ ctx.quadraticCurveTo(x-w/2,y-h, x,y-h);
+ ctx.quadraticCurveTo(x+w/2,y-h, x+w/2,y-h*0.55);
+ ctx.lineTo(x+w/2,y); ctx.stroke();
+ // shimmering mirror fill
+ const g=ctx.createLinearGradient(x-w/2,y-h,x+w/2,y);
+ const sh=(Math.sin(t*2+x)*0.5+0.5);
+ g.addColorStop(0,`rgba(200,240,255,${0.10+sh*0.12})`);
+ g.addColorStop(0.5,`rgba(140,190,255,0.06)`);
+ g.addColorStop(1,`rgba(255,220,180,${0.08+ (1-sh)*0.10})`);
+ ctx.fillStyle=g;
+ ctx.beginPath(); ctx.moveTo(x-w/2,y); ctx.lineTo(x-w/2,y-h*0.55);
+ ctx.quadraticCurveTo(x-w/2,y-h, x,y-h);
+ ctx.quadraticCurveTo(x+w/2,y-h, x+w/2,y-h*0.55);
+ ctx.lineTo(x+w/2,y); ctx.closePath(); ctx.fill();
+ // light bulbs
+ for(let i=0;i<9;i++){ const p=i/8;
+ const bx=x + Math.cos(Math.PI*(1-p))*(w/2);
+ const by=y-h*0.55 - Math.sin(Math.PI*p)*(h*0.45);
+ ctx.fillStyle= (Math.sin(t*3+i)>0)? '#fff4c0':ink;
+ ctx.beginPath(); ctx.arc(bx,by,3.4,0,7); ctx.fill();
+ }
+ ctx.restore();
+}
+function handSign(x,y,text,cA,ink,t){
+ ctx.save(); ctx.translate(x,y); ctx.rotate(Math.sin(t+x)*0.03);
+ ctx.fillStyle=cA; ctx.strokeStyle=ink; ctx.lineWidth=3;
+ ctx.beginPath(); ctx.roundRect(-58,-20,116,40,8); ctx.fill(); ctx.stroke();
+ ctx.fillStyle=ink; ctx.font='700 20px "Trebuchet MS", sans-serif'; ctx.textAlign='center'; ctx.textBaseline='middle';
+ ctx.fillText(text,0,1);
+ ctx.restore();
+ return {x,y}; // return anchor for monkeys
+}
+let signAnchors=[];
+
+function carousel(x,y,t,ph){
+ ctx.save(); ctx.translate(x,y);
+ const spin=t*0.9;
+ // canopy
+ stripedTent(0,-40,220,120,ph.tentB,ph.tentA,10);
+ // pole
+ ctx.fillStyle='#caa24e'; ctx.fillRect(-6,-40,12,150);
+ // rotating horses/lights ring
+ for(let i=0;i<8;i++){
+ const a=spin+i*Math.PI/4;
+ const px=Math.cos(a)*95, py=40+Math.sin(a)*24;
+ ctx.globalAlpha=0.55+0.45*(Math.sin(a)*0.5+0.5);
+ ctx.fillStyle= i%2? ph.glow: ph.accent;
+ ctx.beginPath(); ctx.arc(px,py,7,0,7); ctx.fill();
+ }
+ ctx.globalAlpha=1;
+ // glow crown
+ ctx.fillStyle=ph.glow; ctx.beginPath(); ctx.arc(0,-104,10,0,7); ctx.fill();
+ ctx.restore();
+}
+
+/* ---------- draw one full scene at time t (used for both live & delayed reflection) ---------- */
+function collectEntities(t, ph){
+ const list=[];
+ const cx=WORLD_W/2, cy=WORLD_H*0.62;
+ const E=ph.energy;
+
+ // ring dancers around carousel (expand/contract)
+ const ringN=12;
+ const radius = 150 + Math.sin(t*0.7)*70*E + 30;
+ for(let i=0;i<ringN;i++){
+ const a=t*0.6 + i*(Math.PI*2/ringN);
+ list.push({id:'ring'+i,type:'person',
+ x:cx+Math.cos(a)*radius, y:cy+ Math.sin(a)*radius*0.42,
+ rot:Math.sin(t*3+i)*0.25, col:i%2?ph.tentB:ph.glow, limb:t*4+i, tall:false, group:'ring'});
+ }
+
+ // promenade crowd (density)
+ const crowdN = Math.round(4 + state.density*26);
+ for(let i=0;i<crowdN;i++){
+ const lane = (i%3);
+ const baseY = WORLD_H*0.74 + lane*46;
+ const speed = 60*(1+lane*0.3);
+ const px = ((t*speed + i*130) % (WORLD_W+300)) - 150;
+ list.push({id:'crowd'+i,type:'person',
+ x:px, y:baseY + Math.sin(t*6+i)*4,
+ rot:0, col: [ph.accent,ph.ink,ph.tentA][i%3], limb:t*5+i, tall:false, group:'crowd', small:0.72});
+ }
+
+ // portal spinners: cross vertical mirror axes, spin as they cross
+ const axes=[WORLD_W*0.24, WORLD_W*0.76];
+ for(let k=0;k<2;k++){
+ for(let i=0;i<3;i++){
+ const px=axes[k]+Math.sin(t*0.8+i*2+k)*70;
+ const py=WORLD_H*0.5 + Math.sin(t*1.3+i+k*3)*120;
+ const spinIntensity = 1-Math.min(1,Math.abs(px-axes[k])/70);
+ list.push({id:'portal'+k+i,type:'person',
+ x:px,y:py,rot:t*3*spinIntensity + i, col:ph.tentA, limb:t*5+i, tall:false, group:'portal'});
+ }
+ }
+
+ // partner pairs trading reflections (two dancers orbit a shared point, swap sides)
+ for(let i=0;i<3;i++){
+ const bx=WORLD_W*(0.35+i*0.15), by=WORLD_H*0.44 + Math.sin(t*0.5+i)*30;
+ const a=t*1.4+i;
+ list.push({id:'pairA'+i,type:'person',x:bx+Math.cos(a)*44,y:by+Math.sin(a)*20,rot:a,col:ph.glow,limb:t*4+i,group:'pair'});
+ list.push({id:'pairB'+i,type:'person',x:bx-Math.cos(a)*44,y:by-Math.sin(a)*20,rot:a+Math.PI,col:ph.accent,limb:t*4+i+2,group:'pair'});
+ }
+
+ // stilt walkers in counterpoint (two directions)
+ for(let i=0;i<4;i++){
+ const dir = i%2? 1:-1;
+ const span=WORLD_W+400;
+ const sx = dir>0 ? ((t*70 + i*260)%span)-200 : span-200-((t*70+i*260)%span);
+ list.push({id:'stilt'+i,type:'stilt',x:sx,y:WORLD_H*0.60,rot:0,col:ph.accent,limb:t*3+i, group:'stilt'});
+ }
+
+ // --- animals ---
+ // bear heel-toe shuffle (stays near a spot, shuffles side to side)
+ list.push({id:'bear',type:'bear',x:WORLD_W*0.5 + Math.sin(t*1.2)*90, y:WORLD_H*0.80,
+ limb:t*3, col:'#8a5a34', hi:'#c98d5a', group:'animal', animal:'bear'});
+
+ // dogs weaving through legs (figure-eight low along promenade)
+ for(let i=0;i<3;i++){
+ const p=t*1.6+i*2.1;
+ list.push({id:'dog'+i,type:'dog',
+ x:WORLD_W*0.5 + Math.sin(p)*220, y:WORLD_H*0.83 + Math.sin(p*2)*22,
+ limb:t*4+i, col:['#e0b060','#d17a3a','#f0d090'][i], group:'animal', animal:'dogs'});
+ }
+
+ // peacocks opening rhythmic fans
+ for(let i=0;i<2;i++){
+ const open=(Math.sin(t*1.4+i*Math.PI)*0.5+0.5);
+ list.push({id:'peacock'+i,type:'peacock',
+ x:WORLD_W*(0.16+i*0.68), y:WORLD_H*0.70, open, col:ph.accent, accent:ph.glow, group:'animal', animal:'peacocks'});
+ }
+
+ // monkeys swinging between signs (pendulum along top)
+ if(signAnchors.length){
+ for(let i=0;i<3;i++){
+ const seg=signAnchors[i%signAnchors.length];
+ const nxt=signAnchors[(i+1)%signAnchors.length];
+ const p=(Math.sin(t*1.5+i*2)*0.5+0.5);
+ const mx=seg.x+(nxt.x-seg.x)*p;
+ const my=Math.min(seg.y,nxt.y)-20 + Math.sin(p*Math.PI)*40 + 46;
+ list.push({id:'monkey'+i,type:'monkey',x:mx,y:my,swing:Math.sin(t*4+i),col:'#5a3d2a', group:'animal', animal:'monkeys'});
+ }
+ }
+
+ // tortoise crossing the busiest route — its own clear lane, straight, collision-free
+ const tp=((t*22) % (WORLD_W+200))-100;
+ list.push({id:'tortoise',type:'tortoise',x:tp,y:WORLD_H*0.885,limb:t*3,col:'#4a6b3a',accent:'#8fb060', group:'animal', animal:'tortoise'});
+
+ return list;
+}
+
+function drawEntity(e, alpha){
+ const focus = state.animal;
+ let a = alpha;
+ if(e.group==='animal' && focus!=='all'){
+ a *= (e.animal===focus) ? 1 : 0.16;
+ } else if(focus!=='all' && e.group!=='animal'){
+ a *= 0.55;
+ }
+ ctx.globalAlpha=a;
+ const foc = (e.group==='animal' && e.animal===focus);
+ const s = (e.small||1) * (foc?1.35:1);
+ switch(e.type){
+ case 'person': person(e.x,e.y,s,e.rot,e.col,e.limb,e.tall); break;
+ case 'stilt': stiltWalker(e.x,e.y,s,e.rot,e.col,e.limb); break;
+ case 'bear': bear(e.x,e.y,1.0*s,e.limb,e.col,e.hi); break;
+ case 'dog': dog(e.x,e.y,1.0*s,e.limb,e.col); break;
+ case 'peacock': peacock(e.x,e.y,1.0*s,e.open,e.col,e.accent); break;
+ case 'monkey': monkey(e.x,e.y,1.0*s,e.swing,e.col); break;
+ case 'tortoise': tortoise(e.x,e.y,1.0*s,e.limb,e.col,e.accent); break;
+ }
+ ctx.globalAlpha=1;
+}
+
+/* ---------- trails ---------- */
+function pushTrail(e){
+ if(state.trail<=0) return;
+ let arr=trails.get(e.id); if(!arr){arr=[];trails.set(e.id,arr);}
+ arr.push({x:e.x,y:e.y,col:e.col||'#fff'});
+ while(arr.length>state.trail) arr.shift();
+}
+function drawTrails(ph){
+ if(state.trail<=0) return;
+ trails.forEach(arr=>{
+ for(let i=1;i<arr.length;i++){
+ const p=arr[i], q=arr[i-1], a=i/arr.length*0.4;
+ ctx.globalAlpha=a; ctx.strokeStyle=p.col; ctx.lineWidth=2.5;
+ ctx.beginPath(); ctx.moveTo(q.x,q.y); ctx.lineTo(p.x,p.y); ctx.stroke();
+ }
+ });
+ ctx.globalAlpha=1;
+}
+
+/* ---------- diagram overlay ---------- */
+function drawDiagram(t,ph){
+ const cx=WORLD_W/2, cy=WORLD_H*0.62;
+ ctx.save();
+ ctx.lineWidth=1.6; ctx.setLineDash([8,7]); ctx.font='600 15px "Trebuchet MS"';
+ ctx.textAlign='center'; ctx.textBaseline='middle';
+ const lab=(x,y,txt,c)=>{ctx.setLineDash([]);ctx.fillStyle=c;
+ ctx.globalAlpha=0.9;ctx.fillRect(x-4,y-11,ctx.measureText(txt).width+8,20);
+ ctx.fillStyle='#0a0518';ctx.fillText(txt,x+ctx.measureText(txt).width/2,y);ctx.globalAlpha=1;ctx.setLineDash([8,7]);};
+
+ // ring breathing loop (min & max radius)
+ ctx.strokeStyle='#f4b83a';
+ for(const rr of [80,250]){ ctx.beginPath(); ctx.ellipse(cx,cy,rr,rr*0.42,0,0,7); ctx.stroke(); }
+ lab(cx-30,cy-115,'RING DANCE · expand↔contract','#f4b83a');
+
+ // stilt lanes (counterpoint arrows)
+ ctx.strokeStyle='#38d6c0';
+ ctx.beginPath(); ctx.moveTo(60,WORLD_H*0.60);ctx.lineTo(WORLD_W-60,WORLD_H*0.60);ctx.stroke();
+ lab(120,WORLD_H*0.60-18,'STILTS → ← counterpoint','#38d6c0');
+
+ // portal axes
+ ctx.strokeStyle='#e0459b';
+ [WORLD_W*0.24,WORLD_W*0.76].forEach(ax=>{ctx.beginPath();ctx.moveTo(ax,WORLD_H*0.28);ctx.lineTo(ax,WORLD_H*0.74);ctx.stroke();});
+ lab(WORLD_W*0.24-20,WORLD_H*0.28-4,'MIRROR PORTAL','#e0459b');
+ lab(WORLD_W*0.76-20,WORLD_H*0.28-4,'MIRROR PORTAL','#e0459b');
+
+ // tortoise clear lane
+ ctx.strokeStyle='#8fb060';
+ ctx.beginPath(); ctx.moveTo(0,WORLD_H*0.885);ctx.lineTo(WORLD_W,WORLD_H*0.885);ctx.stroke();
+ lab(WORLD_W*0.5-40,WORLD_H*0.885-18,'TORTOISE · protected line','#8fb060');
+
+ // bear shuffle span
+ ctx.strokeStyle='#c98d5a';
+ ctx.beginPath(); ctx.moveTo(WORLD_W*0.5-90,WORLD_H*0.80);ctx.lineTo(WORLD_W*0.5+90,WORLD_H*0.80);ctx.stroke();
+ lab(WORLD_W*0.5-40,WORLD_H*0.80+22,'BEAR heel-toe','#c98d5a');
+
+ // monkey pendulum arc between signs
+ if(signAnchors.length>1){
+ ctx.strokeStyle='#c9a24e';
+ ctx.beginPath();
+ for(let i=0;i<signAnchors.length-1;i++){
+ const s=signAnchors[i],n=signAnchors[i+1];
+ ctx.moveTo(s.x,s.y+46); ctx.quadraticCurveTo((s.x+n.x)/2,Math.min(s.y,n.y)+6,n.x,n.y+46);
+ }
+ ctx.stroke();
+ lab(signAnchors[0].x-20,signAnchors[0].y+60,'MONKEYS sign→sign','#c9a24e');
+ }
+ ctx.restore();
+}
+
+/* ---------- main render ---------- */
+function render(t){
+ const ph = phaseAt(t);
+ const cam = camera(t);
+
+ // clear (screen space)
+ ctx.setTransform(1,0,0,1,0,0);
+ const g=ctx.createLinearGradient(0,0,0,cv.height);
+ g.addColorStop(0,ph.bg1); g.addColorStop(1,ph.bg2);
+ ctx.fillStyle=g; ctx.fillRect(0,0,cv.width,cv.height);
+
+ applyCam(cam);
+
+ // --- back scenery layer (parallax-ish, drawn in world space) ---
+ // distant tents
+ stripedTent(WORLD_W*0.18,WORLD_H*0.66,300,200,ph.tentA,ph.tentB,8);
+ stripedTent(WORLD_W*0.82,WORLD_H*0.66,300,200,ph.tentB,ph.tentA,8);
+ stripedTent(WORLD_W*0.5,WORLD_H*0.60,420,260,ph.tentB,ph.tentA,12);
+
+ // mirrored arches
+ mirroredArch(WORLD_W*0.24,WORLD_H*0.72,220,320,ph.glow,ph.ink,t);
+ mirroredArch(WORLD_W*0.76,WORLD_H*0.72,220,320,ph.glow,ph.ink,t);
+
+ // hand-lettered signs (also anchor monkeys)
+ signAnchors=[];
+ signAnchors.push(handSign(WORLD_W*0.12,WORLD_H*0.30,'GOLDEN',ph.tentA,'#22102f',t));
+ signAnchors.push(handSign(WORLD_W*0.34,WORLD_H*0.22,'ROAD',ph.glow,'#22102f',t));
+ signAnchors.push(handSign(WORLD_W*0.58,WORLD_H*0.24,'MIRRORS',ph.accent,'#22102f',t));
+ signAnchors.push(handSign(WORLD_W*0.82,WORLD_H*0.31,'CARNIVAL',ph.tentB,'#22102f',t));
+
+ // carousel (center ride)
+ carousel(WORLD_W/2, WORLD_H*0.52, t, ph);
+
+ // --- REFLECTION LAYER: same choreography at (t - delay), mirrored about center, dim & cool ---
+ const rt = t - state.delay;
+ const rEnts = collectEntities(rt, phaseAt(rt));
+ ctx.save();
+ ctx.translate(WORLD_W,0); ctx.scale(-1,1); // mirror horizontally about center axis
+ for(const e of rEnts){ drawEntity(e, 0.28); }
+ ctx.restore();
+ // soft mirror-floor sheen
+ const sheen=ctx.createLinearGradient(0,WORLD_H*0.5,0,WORLD_H);
+ sheen.addColorStop(0,'rgba(160,200,255,0)'); sheen.addColorStop(1,'rgba(140,180,255,0.06)');
+ ctx.fillStyle=sheen; ctx.fillRect(0,WORLD_H*0.5,WORLD_W,WORLD_H*0.5);
+
+ // --- LIVE ENSEMBLE ---
+ const ents = collectEntities(t, ph);
+ drawTrails(ph);
+ for(const e of ents){ drawEntity(e,1); pushTrail(e); }
+
+ // diagram overlay on top
+ if(state.diagram) drawDiagram(t,ph);
+
+ // vignette (screen space)
+ ctx.setTransform(1,0,0,1,0,0);
+ const vg=ctx.createRadialGradient(cv.width/2,cv.height*0.45,cv.height*0.3,cv.width/2,cv.height*0.5,cv.height*0.9);
+ vg.addColorStop(0,'rgba(0,0,0,0)'); vg.addColorStop(1,'rgba(0,0,0,0.55)');
+ ctx.fillStyle=vg; ctx.fillRect(0,0,cv.width,cv.height);
+
+ // phase name update
+ if(document.getElementById('phaseName').textContent!==ph.name){
+ document.getElementById('phaseName').textContent=ph.name;
+ document.getElementById('live').textContent='Now in movement: '+ph.name;
+ }
+}
+
+/* ---------- loop ---------- */
+function frame(now){
+ const dtRaw=(now-last)/1000; last=now;
+ const dt=Math.min(dtRaw,0.05);
+ if(state.playing){
+ const speed = state.reduced ? 0.12 : 1;
+ clock += dt * state.tempo * speed;
+ }
+ render(clock);
+ requestAnimationFrame(frame);
+}
+requestAnimationFrame(frame);
+
+/* ---------- controls wiring ---------- */
+const $=id=>document.getElementById(id);
+document.querySelectorAll('[data-cam]').forEach(b=>{
+ b.addEventListener('click',()=>{
+ state.cam=+b.dataset.cam;
+ document.querySelectorAll('[data-cam]').forEach(x=>x.setAttribute('aria-pressed', x===b));
+ $('live').textContent='Camera route: '+b.textContent;
+ });
+});
+$('tempo').addEventListener('input',e=>{ state.tempo=+e.target.value/100; $('tempoOut').textContent=state.tempo.toFixed(2)+'×'; });
+$('delay').addEventListener('input',e=>{ state.delay=+e.target.value/100; $('delayOut').textContent=state.delay.toFixed(2)+' s'; });
+$('trail').addEventListener('input',e=>{ state.trail=+e.target.value; $('trailOut').textContent=state.trail; if(state.trail===0)trails.clear(); });
+$('density').addEventListener('input',e=>{ state.density=+e.target.value/100;
+ $('densityOut').textContent= state.density<0.2?'sparse':state.density<0.55?'medium':state.density<0.8?'busy':'thronged'; });
+$('animal').addEventListener('change',e=>{ state.animal=e.target.value;
+ $('animalOut').textContent = e.target.options[e.target.selectedIndex].text.split(' · ')[0]; });
+$('diagram').addEventListener('change',e=>{ state.diagram=e.target.checked; });
+$('reduced').addEventListener('change',e=>{ state.reduced=e.target.checked;
+ $('live').textContent= state.reduced?'Reduced motion on':'Reduced motion off'; });
+$('playBtn').addEventListener('click',e=>{
+ state.playing=!state.playing;
+ e.target.setAttribute('aria-pressed',state.playing);
+ e.target.textContent= state.playing?'⏸ Pause':'▶ Play';
+ $('fpsBadge').textContent= state.playing?'score · running':'score · paused';
+});
+$('restartBtn').addEventListener('click',()=>{ clock=0; trails.clear(); $('live').textContent='Restarted from the Invitation.'; });
+
+/* keyboard: space toggles play when body focused */
+document.addEventListener('keydown',ev=>{
+ if(ev.code==='Space' && (ev.target===document.body)){ ev.preventDefault(); $('playBtn').click(); }
+});
+})();
+</script>
+</body>
+</html>
\ No newline at end of file
diff --git a/data/artifacts/ec9e68a64f43/qwen25-7b.html b/data/artifacts/ec9e68a64f43/qwen25-7b.html
new file mode 100644
index 0000000..e0dc732
--- /dev/null
+++ b/data/artifacts/ec9e68a64f43/qwen25-7b.html
@@ -0,0 +1,71 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>Golden Road Carnival of Mirrors</title>
+ <style>
+ body { margin: 0; overflow: hidden; display: flex; align-items: center; justify-content: center; height: 100vh; background: #f7d6a3; }
+ canvas { width: 100%; height: 100%; pointer-events: none; cursor: default; }
+ .controls { position: absolute; bottom: 20px; left: 50%; transform: translateX(-50%); display: flex; gap: 16px; }
+ button { padding: 8px 12px; font-size: 14px; border: none; background-color: #ff7e5f; color: white; cursor: pointer; user-select: none; }
+ .focus-state { outline: 2px solid blue; }
+ </style>
+</head>
+<body>
+<canvas id="c"></canvas>
+<div class="controls">
+ <button onclick="start()">Start</button>
+ <button onclick="pause()">Pause</button>
+ <button onclick="restart()">Restart</button>
+ <input type="range" min="10" max="60" value="30" step="5" oninput="setTempo(this.value)" class="focus-state">
+ <input type="range" min="2" max="8" value="4" step="1" oninput="setDelay(this.value)" class="focus-state">
+ <input type="range" min="0.1" max="3" value="1" step="0.1" oninput="setCrowdDensity(this.value)" class="focus-state">
+ <input type="range" min="0.5" max="2" value="1" step="0.1" oninput="setAnimalFocus(this.value)" class="focus-state">
+</div>
+<script>
+const canvas = document.getElementById('c');
+const ctx = canvas.getContext('2d');
+
+canvas.width = window.innerWidth;
+canvas.height = window.innerHeight;
+
+let tempo = 30;
+let delay = 4;
+let crowdDensity = 1;
+let animalFocus = 1;
+let paused = false;
+
+function draw() {
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
+ // Draw background and midground
+ ctx.fillStyle = 'rgba(255, 230, 163, 0.9)';
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
+
+ drawCharacters();
+
+ requestAnimationFrame(draw);
+}
+
+function drawCharacters() {
+ // Characters here would be drawn with animations
+ // This is a placeholder for the actual drawing logic
+}
+
+function start() { paused = false; }
+function pause() { paused = true; }
+function restart() { paused = false; ctx.clearRect(0, 0, canvas.width, canvas.height); }
+
+function setTempo(val) { tempo = val; }
+function setDelay(val) { delay = val; }
+function setCrowdDensity(val) { crowdDensity = val; }
+function setAnimalFocus(val) { animalFocus = val; }
+
+draw();
+</script>
+<svg style="display:none">
+ <circle cx="10" cy="25" r="7" />
+ <path d="M 20,30 L 40,60 Z" fill="#ff7e5f" stroke="black" stroke-width="1" />
+</svg>
+</body>
+</html>
\ No newline at end of file
diff --git a/data/artifacts/ec9e68a64f43/qwen3-14b.html b/data/artifacts/ec9e68a64f43/qwen3-14b.html
new file mode 100644
index 0000000..b1e529e
--- /dev/null
+++ b/data/artifacts/ec9e68a64f43/qwen3-14b.html
@@ -0,0 +1,400 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <title>Golden Road Carnival of Mirrors</title>
+ <style>
+ html, body {
+ margin: 0;
+ padding: 0;
+ overflow: hidden;
+ background: #0e0e0e;
+ height: 100%;
+ width: 100%;
+ font-family: 'Arial', sans-serif;
+ color: white;
+ user-select: none;
+ touch-action: manipulation;
+ }
+ canvas {
+ display: block;
+ }
+ .controls {
+ position: absolute;
+ top: 10px;
+ left: 10px;
+ z-index: 10;
+ background: rgba(0, 0, 0, 0.6);
+ padding: 10px;
+ border-radius: 8px;
+ font-size: 14px;
+ }
+ .controls label {
+ display: block;
+ margin-bottom: 5px;
+ }
+ .controls input[type="range"] {
+ width: 100%;
+ }
+ .controls button {
+ margin-top: 10px;
+ padding: 6px 12px;
+ background: #222;
+ color: white;
+ border: none;
+ border-radius: 4px;
+ cursor: pointer;
+ }
+ .controls button:focus {
+ outline: 2px solid #4CAF50;
+ }
+ .overlay {
+ position: absolute;
+ top: 10px;
+ left: 10px;
+ z-index: 20;
+ background: rgba(0, 0, 0, 0.6);
+ padding: 10px;
+ border-radius: 8px;
+ font-size: 12px;
+ color: #ccc;
+ }
+ </style>
+</head>
+<body>
+ <div class="controls">
+ <label><input type="range" min="0" max="1" step="0.01" value="0.5" id="reflectionDelay">Reflection Delay</label>
+ <label><input type="range" min="0" max="1" step="0.01" value="0.5" id="crowdDensity">Crowd Density</label>
+ <label><input type="range" min="0" max="1" step="0.01" value="0.5" id="animalFocus">Animal Focus</label>
+ <label><input type="range" min="0" max="1" step="0.01" value="0.5" id="trailLength">Trail Length</label>
+ <label><input type="range" min="1" max="5" step="0.1" value="2" id="tempo">Tempo</label>
+ <label><select id="cameraRoute"><option value="0">Invitation</option><option value="1">Mirror Jam</option><option value="2">Last Lantern</option></select>Camera Route</label>
+ <button id="pause">Pause</button>
+ <button id="restart">Restart</button>
+ <button id="reduceMotion">Reduce Motion</button>
+ </div>
+ <div class="overlay" id="overlay">
+ <strong>Choreography Loops:</strong>
+ <ul>
+ <li><strong>Invitation:</strong> Soft, flowing introduction with golden light and mirrored reflections.</li>
+ <li><strong>Mirror Jam:</strong> Intense, layered movement with delayed reflections and high crowd density.</li>
+ <li><strong>Last Lantern:</strong> Slow, deliberate closing with low tempo and extended trails.</li>
+ </ul>
+ </div>
+ <canvas id="canvas"></canvas>
+ <script>
+ const canvas = document.getElementById('canvas');
+ const ctx = canvas.getContext('2d');
+ let width, height, animationFrame, isPaused = false, reduceMotion = false;
+ const reflectionDelay = document.getElementById('reflectionDelay');
+ const crowdDensity = document.getElementById('crowdDensity');
+ const animalFocus = document.getElementById('animalFocus');
+ const trailLength = document.getElementById('trailLength');
+ const tempo = document.getElementById('tempo');
+ const cameraRoute = document.getElementById('cameraRoute');
+ const pauseButton = document.getElementById('pause');
+ const restartButton = document.getElementById('restart');
+ const reduceMotionButton = document.getElementById('reduceMotion');
+
+ function resize() {
+ width = canvas.width = window.innerWidth;
+ height = canvas.height = window.innerHeight;
+ }
+ window.addEventListener('resize', resize);
+ resize();
+
+ function getMousePos(evt) {
+ const rect = canvas.getBoundingClientRect();
+ return {
+ x: evt.clientX - rect.left,
+ y: evt.clientY - rect.top
+ };
+ }
+ canvas.addEventListener('click', (e) => {
+ if (isPaused) {
+ isPaused = false;
+ animate();
+ }
+ });
+
+ const entities = [];
+
+ class Entity {
+ constructor(type, x, y, speed, trailLength = 100, color = 'white', delay = 0.5) {
+ this.type = type;
+ this.x = x;
+ this.y = y;
+ this.speed = speed;
+ this.trail = [];
+ this.trailLength = trailLength;
+ this.color = color;
+ this.delay = delay;
+ }
+
+ move() {
+ if (this.type === 'mirror') {
+ this.x += Math.cos(Date.now() * this.speed) * 0.5;
+ this.y += Math.sin(Date.now() * this.speed) * 0.5;
+ } else if (this.type === 'character') {
+ this.x += (Math.cos(Date.now() * this.speed) * 0.3) * (Math.random() - 0.5);
+ this.y += (Math.sin(Date.now() * this.speed) * 0.3) * (Math.random() - 0.5);
+ } else if (this.type === 'animal') {
+ if (this.subtype === 'bear') {
+ this.x += Math.cos(Date.now() * this.speed) * 0.1;
+ this.y += Math.sin(Date.now() * this.speed) * 0.1;
+ } else if (this.subtype === 'dog') {
+ this.x += Math.cos(Date.now() * this.speed + Math.PI / 2) * 0.15;
+ this.y += Math.sin(Date.now() * this.speed + Math.PI / 2) * 0.15;
+ } else if (this.subtype === 'peacock') {
+ this.x += Math.cos(Date.now() * this.speed) * 0.1;
+ this.y += Math.sin(Date.now() * this.speed) * 0.1;
+ this.angle = (Date.now() * this.speed) % (Math.PI * 2);
+ } else if (this.subtype === 'monkey') {
+ this.x += Math.cos(Date.now() * this.speed + Math.PI) * 0.2;
+ this.y += Math.sin(Date.now() * this.speed + Math.PI) * 0.2;
+ } else if (this.subtype === 'tortoise') {
+ this.x += Math.cos(Date.now() * this.speed) * 0.05;
+ this.y += Math.sin(Date.now() * this.speed) * 0.05;
+ }
+ }
+ }
+
+ draw(ctx, offsetX, offsetY, scale) {
+ if (this.type === 'mirror') {
+ ctx.save();
+ ctx.translate(this.x * scale + offsetX, this.y * scale + offsetY);
+ ctx.beginPath();
+ ctx.arc(0, 0, 10, 0, Math.PI * 2);
+ ctx.fillStyle = 'rgba(255, 255, 255, 0.2)';
+ ctx.fill();
+ ctx.lineWidth = 2;
+ ctx.strokeStyle = 'white';
+ ctx.stroke();
+ ctx.restore();
+ } else if (this.type === 'character') {
+ ctx.save();
+ ctx.translate(this.x * scale + offsetX, this.y * scale + offsetY);
+ ctx.beginPath();
+ ctx.arc(0, 0, 6, 0, Math.PI * 2);
+ ctx.fillStyle = this.color;
+ ctx.fill();
+ ctx.restore();
+ } else if (this.type === 'animal') {
+ ctx.save();
+ ctx.translate(this.x * scale + offsetX, this.y * scale + offsetY);
+ if (this.subtype === 'bear') {
+ ctx.fillStyle = 'brown';
+ ctx.fillRect(-4, -6, 8, 12);
+ ctx.fillStyle = 'black';
+ ctx.fillRect(-3, -4, 2, 8);
+ } else if (this.subtype === 'dog') {
+ ctx.fillStyle = 'gray';
+ ctx.fillRect(-5, -10, 10, 15);
+ ctx.fillStyle = 'black';
+ ctx.fillRect(-3, -8, 4, 8);
+ } else if (this.subtype === 'peacock') {
+ ctx.fillStyle = 'green';
+ ctx.beginPath();
+ ctx.moveTo(0, 0);
+ ctx.lineTo(0, -20);
+ ctx.lineTo(20, -25);
+ ctx.lineTo(0, -30);
+ ctx.lineTo(-20, -25);
+ ctx.closePath();
+ ctx.fill();
+ ctx.fillStyle = 'blue';
+ ctx.beginPath();
+ ctx.arc(0, -30, 5, 0, Math.PI * 2);
+ ctx.fill();
+ } else if (this.subtype === 'monkey') {
+ ctx.fillStyle = 'brown';
+ ctx.beginPath();
+ ctx.arc(0, 0, 6, 0, Math.PI * 2);
+ ctx.fill();
+ ctx.fillStyle = 'black';
+ ctx.fillRect(-2, -2, 4, 4);
+ } else if (this.subtype === 'tortoise') {
+ ctx.fillStyle = 'green';
+ ctx.beginPath();
+ ctx.arc(0, 0, 8, 0, Math.PI * 2);
+ ctx.fill();
+ ctx.fillStyle = 'black';
+ ctx.fillRect(-2, -4, 4, 8);
+ }
+ ctx.restore();
+ }
+ }
+
+ updateTrail() {
+ this.trail.unshift({ x: this.x, y: this.y });
+ if (this.trail.length > this.trailLength) {
+ this.trail.pop();
+ }
+ }
+
+ drawTrail(ctx, offsetX, offsetY, scale) {
+ for (let i = 0; i < this.trail.length; i++) {
+ const t = this.trail[i];
+ ctx.fillStyle = `rgba(255, 255, 255, ${1 - i / this.trailLength})`;
+ if (this.type === 'character') {
+ ctx.beginPath();
+ ctx.arc(t.x * scale + offsetX, t.y * scale + offsetY, 4, 0, Math.PI * 2);
+ ctx.fill();
+ } else if (this.type === 'animal') {
+ ctx.fillStyle = `rgba(0, 255, 0, ${1 - i / this.trailLength})`;
+ ctx.beginPath();
+ ctx.arc(t.x * scale + offsetX, t.y * scale + offsetY, 3, 0, Math.PI * 2);
+ ctx.fill();
+ }
+ }
+ }
+ }
+
+ function createEntities() {
+ entities.length = 0;
+ const numMirrors = 30;
+ const numCharacters = 50;
+ const numAnimals = 30;
+ const animalTypes = ['bear', 'dog', 'peacock', 'monkey', 'tortoise'];
+ const colors = ['white', 'lightblue', 'lightgreen', 'orange', 'pink'];
+ for (let i = 0; i < numMirrors; i++) {
+ entities.push(new Entity('mirror', Math.random() * width, Math.random() * height, Math.random() * 0.5 + 0.5));
+ }
+ for (let i = 0; i < numCharacters; i++) {
+ entities.push(new Entity('character', Math.random() * width, Math.random() * height, Math.random() * 0.5 + 0.5, 50, colors[i % 5]));
+ }
+ for (let i = 0; i < numAnimals; i++) {
+ const type = animalTypes[Math.floor(Math.random() * animalTypes.length)];
+ entities.push(new Entity('animal', Math.random() * width, Math.random() * height, Math.random() * 0.5 + 0.5, 50, 'white', type));
+ }
+ }
+
+ createEntities();
+
+ function drawScene() {
+ ctx.clearRect(0, 0, width, height);
+ const route = parseInt(cameraRoute.value);
+ const scale = 0.5;
+ const offset = { x: (width * 0.5 - 100) * (route === 0 ? 0 : route === 1 ? 0.3 : 0.6), y: (height * 0.5 - 100) * (route === 0 ? 0 : route === 1 ? 0.2 : 0.4) };
+ const camX = offset.x;
+ const camY = offset.y;
+
+ // Background
+ ctx.fillStyle = 'black';
+ ctx.fillRect(0, 0, width, height);
+
+ // Midway tents
+ ctx.strokeStyle = 'white';
+ ctx.lineWidth = 2;
+ for (let i = 0; i < 10; i++) {
+ ctx.beginPath();
+ ctx.moveTo(50 + i * 100, 50);
+ ctx.lineTo(50 + i * 100, height - 50);
+ ctx.stroke();
+ }
+
+ // Mirror arches
+ ctx.strokeStyle = 'rgba(255, 255, 255, 0.5)';
+ ctx.lineWidth = 4;
+ for (let i = 0; i < 5; i++) {
+ ctx.beginPath();
+ ctx.arc(100 + i * 200, 100, 50, 0, Math.PI * 2);
+ ctx.stroke();
+ }
+
+ // Hand-lettered signs
+ for (let i = 0; i < 3; i++) {
+ ctx.fillStyle = 'white';
+ ctx.font = '20px serif';
+ ctx.fillText('MIRRORED REALMS', 100 + i * 200, 150);
+ ctx.fillText('GOLDEN ROAD', 100 + i * 200, 180);
+ }
+
+ // Glowing midway rides
+ ctx.fillStyle = 'rgba(255, 255, 0, 0.3)';
+ for (let i = 0; i < 3; i++) {
+ ctx.beginPath();
+ ctx.arc(200 + i * 200, 300, 30, 0, Math.PI * 2);
+ ctx.fill();
+ }
+
+ // Draw entities with trails
+ for (const entity of entities) {
+ entity.move();
+ entity.updateTrail();
+ entity.draw(ctx, camX, camY, scale);
+ if (!reduceMotion) {
+ entity.drawTrail(ctx, camX, camY, scale);
+ }
+ }
+ }
+
+ function animate() {
+ if (isPaused) return;
+ drawScene();
+ animationFrame = requestAnimationFrame(animate);
+ }
+
+ pauseButton.addEventListener('click', () => {
+ isPaused = !isPaused;
+ pauseButton.textContent = isPaused ? 'Resume' : 'Pause';
+ });
+
+ restartButton.addEventListener('click', () => {
+ isPaused = true;
+ pauseButton.textContent = 'Resume';
+ createEntities();
+ animate();
+ });
+
+ reduceMotionButton.addEventListener('click', () => {
+ reduceMotion = !reduceMotion;
+ reduceMotionButton.textContent = reduceMotion ? 'Enable Motion' : 'Reduce Motion';
+ });
+
+ let lastTime = performance.now();
+
+ function loop(now) {
+ const elapsed = now - lastTime;
+ const speedFactor = parseFloat(tempo.value);
+ const delay = parseFloat(reflectionDelay.value);
+ const density = parseFloat(crowdDensity.value);
+ const focus = parseFloat(animalFocus.value);
+ const trail = parseFloat(trailLength.value);
+ const route = parseInt(cameraRoute.value);
+
+ // Update trail lengths based on trailLength control
+ for (let entity of entities) {
+ if (entity.type === 'character') entity.trailLength = 50 * trail;
+ if (entity.type === 'animal') entity.trailLength = 50 * trail;
+ }
+
+ // Animate entities
+ for (let entity of entities) {
+ if (entity.type === 'mirror') {
+ entity.speed = 0.5 * (1 + (route === 1 ? 0.5 : route === 2 ? 0.3 : 0));
+ } else if (entity.type === 'character') {
+ entity.speed = 0.5 * (1 + density * 0.5);
+ } else if (entity.type === 'animal') {
+ entity.speed = 0.5 * (1 + focus * 0.5);
+ }
+ }
+
+ animate();
+ lastTime = now;
+ }
+
+ loop(performance.now());
+
+ window.addEventListener('keydown', (e) => {
+ if (e.key === ' ') {
+ isPaused = !isPaused;
+ pauseButton.textContent = isPaused ? 'Resume' : 'Pause';
+ }
+ });
+ </script>
+ <div class="curator-note" style="position: absolute; bottom: 10px; left: 10px; z-index: 25; background: rgba(0,0,0,0.6); padding: 10px; border-radius: 8px; font-size: 12px;">
+ <strong>Curator Note:</strong> This motion is an authored visual score, not audio-reactive, composed to evoke the psychedelic energy of a golden carnival through layered paper-cut geometry and kinetic choreography. Each movement is hand-crafted to respond to controls, offering a dreamlike journey through reflections, motion, and time.
+ </div>
+</body>
+</html>
\ No newline at end of file
diff --git a/data/challenges.json b/data/challenges.json
index 5ad99b8..534f679 100644
--- a/data/challenges.json
+++ b/data/challenges.json
@@ -35578,12 +35578,12 @@
"model": "qwen25-7b",
"status": "done",
"error": null,
- "seconds": 77,
+ "seconds": 72,
"cost": 0,
- "started_at": "2026-09-01T23:18:02.879Z",
- "finished_at": "2026-09-01T23:19:19.759Z",
- "queued_at": "2026-09-01T23:18:02.865Z",
- "bytes": 10862,
+ "started_at": "2026-09-02T00:04:51.104Z",
+ "finished_at": "2026-09-02T00:06:02.845Z",
+ "queued_at": "2026-09-02T00:04:51.098Z",
+ "bytes": 7773,
"thumb": true,
"aiScore": 3.3,
"aiReason": "The image is blank and does not represent the described animated gallery experience.",
@@ -35597,12 +35597,12 @@
"model": "claude-code",
"status": "done",
"error": null,
- "seconds": 241,
+ "seconds": 229,
"cost": 0,
- "started_at": "2026-09-01T23:18:02.884Z",
- "finished_at": "2026-09-01T23:22:04.165Z",
- "queued_at": "2026-09-01T23:18:02.869Z",
- "bytes": 38454,
+ "started_at": "2026-09-02T00:00:01.202Z",
+ "finished_at": "2026-09-02T00:03:50.285Z",
+ "queued_at": "2026-09-02T00:00:01.196Z",
+ "bytes": 37585,
"thumb": true,
"aiScore": 8.3,
"aiReason": "The image effectively fulfills the challenge by providing an interactive animated gallery experience with adjustable settings and a clear visual rhythm.",
@@ -35772,15 +35772,15 @@
"model": "qwen3-14b",
"status": "done",
"error": null,
- "seconds": 67,
+ "seconds": 83,
"cost": 0,
- "started_at": "2026-09-01T23:21:59.125Z",
- "finished_at": "2026-09-01T23:23:06.077Z",
- "queued_at": "2026-09-01T23:18:02.934Z",
- "bytes": 11603,
+ "started_at": "2026-09-02T00:07:58.714Z",
+ "finished_at": "2026-09-02T00:09:21.908Z",
+ "queued_at": "2026-09-02T00:07:58.708Z",
+ "bytes": 13734,
"thumb": true,
"aiScore": 6.8,
- "aiReason": "The interface is functional but lacks the vibrant graphics and dynamic animation expected for a psychedelic travel-poster inspired narrative.",
+ "aiReason": "The interface is functional but lacks the vibrant animation and detailed landscapes required for a gallery-grade responsive HTML file.",
"aiScores": {
"qwen2.5vl:7b": 7,
"minicpm-v:latest": 6.5
@@ -35791,20 +35791,20 @@
"model": "qwen25-7b",
"status": "done",
"error": null,
- "seconds": 91,
+ "seconds": 25,
"cost": 0,
- "started_at": "2026-09-01T23:20:02.916Z",
- "finished_at": "2026-09-01T23:21:34.106Z",
- "queued_at": "2026-09-01T23:18:02.938Z",
- "bytes": 6143,
+ "started_at": "2026-09-02T00:07:58.729Z",
+ "finished_at": "2026-09-02T00:08:23.326Z",
+ "queued_at": "2026-09-02T00:07:58.724Z",
+ "bytes": 4043,
"thumb": true,
- "aiScore": 3.8,
- "aiReason": "The image is nearly black and lacks any visible content or landscape depiction.",
+ "aiScore": 5.3,
+ "aiReason": "The image is blank and does not provide any visual content or information related to the challenge.",
"aiScores": {
- "qwen2.5vl:7b": 3,
- "minicpm-v:latest": 4.5
+ "qwen2.5vl:7b": 4,
+ "minicpm-v:latest": 6.5
},
- "aiSpread": 1.5
+ "aiSpread": 2.5
},
{
"model": "claude-code",
@@ -35818,7 +35818,7 @@
"bytes": 43242,
"thumb": true,
"aiScore": 7.3,
- "aiReason": "The interface is functional but lacks the vibrant graphics and dynamic animation expected for a gallery-grade experience.",
+ "aiReason": "The interface is functional but lacks the vibrant graphics and dynamic animation expected for a gallery-grade installation.",
"aiScores": {
"qwen2.5vl:7b": 7,
"minicpm-v:latest": 7.5
@@ -35828,7 +35828,7 @@
],
"judging": false,
"aiPick": "claude-code",
- "judged_at": "2026-09-01T23:36:18.104Z"
+ "judged_at": "2026-09-02T00:11:48.033Z"
},
{
"id": "127317180daf",
diff --git a/verification/grateful-dead-poster-five-live.png b/verification/grateful-dead-poster-five-live.png
new file mode 100644
index 0000000..85159dc
Binary files /dev/null and b/verification/grateful-dead-poster-five-live.png differ
diff --git a/verification/grateful-dead-poster-five-proof.json b/verification/grateful-dead-poster-five-proof.json
new file mode 100644
index 0000000..3fff082
--- /dev/null
+++ b/verification/grateful-dead-poster-five-proof.json
@@ -0,0 +1,39 @@
+{
+ "intent": "Create five genuinely new Grateful Dead-inspired animated gallery challenges after the 2:24 PM batch and prove them live.",
+ "riskTier": "R4 production content mutation",
+ "environment": "https://modelarena.agentabrams.com/",
+ "timestamp": "2026-09-02T00:11:48.033Z",
+ "baseline": {
+ "challengeCount": 260,
+ "newestBefore": "2026-09-01T21:24:01.268Z"
+ },
+ "result": {
+ "challengeCount": 265,
+ "challengeIds": ["cd83de847d24", "97621618f31c", "ec9e68a64f43", "77fc58739877", "127317180daf"],
+ "createdAt": "2026-09-01T23:18:02Z",
+ "modelRunsDone": 15,
+ "modelRunsErrored": 0,
+ "runtimeCleanArtifacts": 12,
+ "browserShellErrors": 0,
+ "newestFiveOrderVerified": true
+ },
+ "assertions": [
+ {"boundary": "live API", "verdict": "PASS", "evidence": "count rose 260 to 265; expected IDs are the newest five"},
+ {"boundary": "queue and persistence", "verdict": "PASS", "evidence": "15 of 15 runs done, zero queued/running/error"},
+ {"boundary": "production UI", "verdict": "PASS", "evidence": "five expected titles are the first five cards"},
+ {"boundary": "artifact routes", "verdict": "PASS", "evidence": "all 15 routes rendered non-empty; every challenge has at least two runtime-clean implementations"},
+ {"boundary": "browser shell", "verdict": "PASS", "evidence": "zero page or console errors outside untrusted generated artifacts"},
+ {"boundary": "generated artifact quality", "verdict": "PASS WITH DISCLOSED WEAK OUTPUTS", "evidence": "12 clean; three Claude artifacts retain isolated runtime errors, while each challenge has at least two clean alternatives"},
+ {"boundary": "thumbnail and judging refresh", "verdict": "PASS", "evidence": "repaired Indigo Qwen thumbnails regenerated at 800x600 and judging completed at 2026-09-02T00:11:48.033Z"}
+ ],
+ "commands": [
+ "CHALLENGES_FILE=../data/grateful-dead-poster-journeys.json MAX_PROMPT_LENGTH=1600 node scripts/create-five-detailed.js",
+ "ARENA_URL=https://modelarena.agentabrams.com/ node verification/grateful-dead-poster-five.e2e.js"
+ ],
+ "evidence": [
+ "verification/grateful-dead-poster-five.e2e.js",
+ "verification/grateful-dead-poster-five-live.png",
+ "data/grateful-dead-poster-journeys.json"
+ ],
+ "cleanup": "Fresh production records intentionally retained. No paid API spend; all selected lanes were zero marginal cost."
+}
diff --git a/verification/grateful-dead-poster-five.e2e.js b/verification/grateful-dead-poster-five.e2e.js
new file mode 100644
index 0000000..04e54a2
--- /dev/null
+++ b/verification/grateful-dead-poster-five.e2e.js
@@ -0,0 +1,79 @@
+#!/usr/bin/env node
+const { chromium } = require('/Users/macstudio3/Projects/Designer-Wallcoverings/node_modules/playwright');
+
+const challenges = [
+ ['cd83de847d24', 'Scarlet Riverboat Masquerade'],
+ ['97621618f31c', 'Terrapin Lantern Observatory'],
+ ['ec9e68a64f43', 'Golden Road Carnival of Mirrors'],
+ ['77fc58739877', 'Indigo Train Through Dream Country'],
+ ['127317180daf', 'Rose City Rooftop Solstice'],
+];
+const models = ['qwen3-14b', 'qwen25-7b', 'claude-code'];
+
+(async () => {
+ const base = process.env.ARENA_URL || 'https://modelarena.agentabrams.com/';
+ const browser = await chromium.launch({ executablePath: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', headless: true });
+ const context = await browser.newContext({
+ viewport: { width: 1280, height: 900 },
+ httpCredentials: { username: process.env.ARENA_USER || 'admin', password: process.env.ARENA_PASS || 'DW2024!' },
+ });
+ const page = await context.newPage();
+ const errors = [];
+ const artifactErrors = [];
+ let activeArtifact = null;
+ const recordError = value => {
+ const target = activeArtifact ? artifactErrors : errors;
+ const item = activeArtifact ? { artifact: activeArtifact, error: value } : value;
+ if (!target.some(existing => JSON.stringify(existing) === JSON.stringify(item))) target.push(item);
+ };
+ page.on('pageerror', error => recordError(String(error)));
+ page.on('console', message => {
+ const value = message.text();
+ if (message.type() === 'error' && !/^Failed to load resource:/.test(value) && !/cloudflareinsights/.test(value)) recordError(value);
+ });
+
+ await page.goto(base, { waitUntil: 'domcontentloaded' });
+ await page.waitForSelector('#list .card');
+ const newestTitles = await page.locator('#list .card').evaluateAll(cards => cards.slice(0, 5).map(card => card.querySelector('h3')?.textContent?.trim()));
+ const expectedTitles = challenges.slice().reverse().map(([, title]) => title);
+ if (JSON.stringify(newestTitles) !== JSON.stringify(expectedTitles)) throw new Error(`newest cards mismatch: ${JSON.stringify(newestTitles)}`);
+
+ const records = await page.evaluate(async ids => Promise.all(ids.map(id => fetch('/api/challenges/' + id).then(response => response.json()))), challenges.map(([id]) => id));
+ for (const record of records) {
+ if (record.prompt.length < 1200) throw new Error(`${record.title} prompt too short`);
+ if (record.runs.length !== 3 || record.runs.some(run => run.status !== 'done' || !run.thumb || typeof run.aiScore !== 'number')) {
+ throw new Error(`${record.title} is not complete/rendered/scored`);
+ }
+ }
+
+ const artifacts = [];
+ for (const [id, title] of challenges) {
+ for (const model of models) {
+ activeArtifact = `${id}/${model}`;
+ const errorStart = artifactErrors.length;
+ await page.goto(new URL(`artifact/${id}/${model}`, base).href, { waitUntil: 'domcontentloaded' });
+ await page.waitForTimeout(250);
+ const evidence = await page.evaluate(() => ({
+ bodyChars: (document.body.innerText || '').trim().length,
+ canvas: document.querySelectorAll('canvas').length,
+ svg: document.querySelectorAll('svg').length,
+ controls: document.querySelectorAll('button,input,select').length,
+ motionMarkers: (document.documentElement.outerHTML.match(/@keyframes|requestAnimationFrame|\.animate\s*\(/g) || []).length,
+ }));
+ if (evidence.bodyChars < 20 && evidence.canvas < 1 && evidence.svg < 1) throw new Error(`${title}/${model} rendered empty`);
+ artifacts.push({ id, model, clean: artifactErrors.length === errorStart, ...evidence });
+ activeArtifact = null;
+ }
+ }
+
+ for (const [id, title] of challenges) {
+ const candidates = artifacts.filter(artifact => artifact.id === id);
+ if (candidates.filter(artifact => artifact.clean).length < 2) throw new Error(`${title} has fewer than two clean implementations: ${JSON.stringify({ candidates, artifactErrors: artifactErrors.filter(item => item.artifact.startsWith(id + '/')) })}`);
+ }
+
+ await page.goto(base, { waitUntil: 'domcontentloaded' });
+ await page.screenshot({ path: 'verification/grateful-dead-poster-five-live.png', fullPage: true });
+ if (errors.length) throw new Error(`browser errors: ${errors.join(' | ')}`);
+ console.log(JSON.stringify({ pass: true, newestTitles, challenges: records.length, artifacts: artifacts.length, cleanArtifacts: artifacts.filter(item => item.clean).length, browserErrors: errors.length, artifactErrors, artifacts }));
+ await browser.close();
+})().catch(error => { console.error(error.stack || error); process.exit(1); });
← b3666bf auto-data-snapshot: 2026-09-01T16:44:49 (16 data files) — da
·
back to Model Arena
·
auto-data-snapshot: 2026-09-02T07:48:02 (4 data files) — dat 9b643ec →