[object Object]

← back to Playa Ignition

Playa Ignition: single-file animated Burning Man gallery — neon Man effigy (ignitable, with flames+embers), fire-poi dancers, art car, neon installations, desert starfield, drifting dust, dancing bears, all pulsing to an authored BPM beat

3af8b73ab068646204b1e5411f6a88029c3e3fff · 2026-09-01 22:35:20 -0700 · Steve Abrams

Files touched

Diff

commit 3af8b73ab068646204b1e5411f6a88029c3e3fff
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Tue Sep 1 22:35:20 2026 -0700

    Playa Ignition: single-file animated Burning Man gallery — neon Man effigy (ignitable, with flames+embers), fire-poi dancers, art car, neon installations, desert starfield, drifting dust, dancing bears, all pulsing to an authored BPM beat
---
 .gitignore |   5 +
 README.md  |  18 +++
 index.html | 470 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 493 insertions(+)

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..f8dc3e8
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,5 @@
+node_modules/
+.env*
+*.log
+.DS_Store
+.playwright-mcp/
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..e5d880b
--- /dev/null
+++ b/README.md
@@ -0,0 +1,18 @@
+# Playa Ignition — A Burning Man Gallery
+
+A single-file animated generative gallery: a night on the playa. The neon **Man**
+effigy stands over the desert, fire-poi dancers spin flame trails, an art car
+drifts by, dust blows, and a colorful troupe dances beneath a vast starfield —
+all pulsing to an **authored beat** (set the BPM; there is no audio).
+
+## Run
+Open `index.html` in any modern browser. No build, no dependencies.
+
+## Steer it
+- **🔥 Ignite the Man** (key `B`) — set the effigy alight; flames climb, embers rise, then it settles
+- **Beat (BPM)** drives the neon flashes, fire flares, and dancers' hops
+- **Neon · Crowd · Dust · Dancing Bears** sliders
+- Cameras: **The Playa · The Man · Overhead** (`1 2 3`)
+- **Click** anywhere sparks a flare · `Space` pause · `T` trails · `M` reduced motion · `C` controls · `L` museum label
+
+Original visual language — dust, light and fire. No borrowed logos, emblems, or art.
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..b05ef42
--- /dev/null
+++ b/index.html
@@ -0,0 +1,470 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="UTF-8">
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+<meta name="description" content="Playa Ignition — an animated generative gallery of a Burning Man night: the neon Man effigy, fire-poi dancers, art cars and a desert starfield, all pulsing to an authored techno beat. Ignite the Man.">
+<meta name="color-scheme" content="dark">
+<title>Playa Ignition — A Burning Man Gallery</title>
+<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'><rect width='16' height='16' fill='%230a0a1e'/><path d='M8 2l3 6-3 6-3-6z' fill='%23ff7a2a'/></svg>">
+<style>
+  :root{
+    --ink:#eae4f2; --ink-dim:#a99fc0; --neon:#ff2fd0; --neon2:#2fe6ff; --gold:#ffcf5a; --fire:#ff6a1a;
+    --panel:rgba(14,10,26,0.72); --panel-edge:rgba(120,90,220,0.32); --focus:#8fe0ff;
+    --serif:"Iowan Old Style","Palatino Linotype",Palatino,Georgia,serif;
+    --sans:"Avenir Next","Segoe UI",system-ui,-apple-system,sans-serif;
+  }
+  *{box-sizing:border-box}
+  html,body{margin:0;height:100%;background:#05050f;color:var(--ink);font-family:var(--sans);overflow:hidden}
+  #stage{position:fixed;inset:0}
+  canvas{position:absolute;inset:0;width:100%;height:100%}
+  .grain{position:fixed;inset:0;pointer-events:none;z-index:5;opacity:.08;mix-blend-mode:overlay;
+    background-image:url("data:image/svg+xml,%3Csvg viewBox='0 0 220 220' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.9' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E")}
+
+  .skip{position:fixed;top:-60px;left:12px;z-index:20;background:var(--neon2);color:#04121a;padding:10px 16px;border-radius:8px;font-weight:600;text-decoration:none;transition:top .2s}
+  .skip:focus{top:12px}
+
+  .titleplate{position:fixed;top:22px;left:26px;z-index:6;max-width:min(52vw,560px);pointer-events:none;text-shadow:0 2px 18px rgba(0,0,0,.75)}
+  .titleplate .kicker{letter-spacing:.42em;text-transform:uppercase;font-size:11px;color:var(--neon2);opacity:.95;margin:0 0 6px 2px}
+  .titleplate h1{font-family:var(--serif);font-weight:600;font-style:italic;font-size:clamp(26px,4.4vw,52px);line-height:.98;margin:0;color:var(--ink)}
+  .titleplate .sub{font-family:var(--serif);font-size:14px;color:var(--ink-dim);margin-top:8px;font-style:italic}
+
+  .chrome-toggle{position:fixed;top:22px;right:26px;z-index:12}
+  #stage.hide-chrome .dock,#stage.hide-chrome .cams,#stage.hide-chrome .label-toggle,#stage.hide-chrome .museum{display:none!important}
+
+  .dock{position:fixed;left:50%;bottom:20px;transform:translateX(-50%);z-index:7;display:flex;gap:18px;align-items:stretch;flex-wrap:wrap;justify-content:center;max-width:min(96vw,1120px);
+    background:var(--panel);border:1px solid var(--panel-edge);border-radius:16px;padding:14px 20px;backdrop-filter:blur(9px);box-shadow:0 12px 40px rgba(0,0,0,.5)}
+  .group{display:flex;gap:12px;align-items:center}
+  .group + .group{border-left:1px solid rgba(120,90,220,.2);padding-left:18px}
+  .field{display:flex;flex-direction:column;gap:5px;min-width:92px}
+  .field label{font-size:10px;letter-spacing:.14em;text-transform:uppercase;color:var(--ink-dim)}
+  .field .val{font-family:var(--serif);font-style:italic;color:var(--neon2);font-size:12px}
+  input[type=range]{-webkit-appearance:none;appearance:none;height:4px;border-radius:4px;background:linear-gradient(90deg,var(--neon),var(--neon2));outline:none;width:100%}
+  input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;width:16px;height:16px;border-radius:50%;background:var(--ink);border:2px solid var(--neon2);cursor:pointer;box-shadow:0 0 8px rgba(47,230,255,.6)}
+  input[type=range]::-moz-range-thumb{width:16px;height:16px;border-radius:50%;background:var(--ink);border:2px solid var(--neon2);cursor:pointer}
+  .btn{font-family:var(--sans);font-size:12px;letter-spacing:.05em;background:rgba(30,18,54,.7);color:var(--ink);border:1px solid var(--panel-edge);border-radius:10px;padding:9px 13px;cursor:pointer;transition:background .2s,color .2s;display:inline-flex;align-items:center;gap:7px;white-space:nowrap}
+  .btn:hover{background:rgba(60,30,110,.9);color:var(--neon2)}
+  .btn[aria-pressed="true"]{background:var(--neon2);color:#04121a;border-color:var(--neon2);font-weight:600}
+  .btn.burn{border-color:var(--fire);color:var(--gold)}
+  .btn.burn:hover{background:rgba(120,40,10,.9);color:#fff}
+  .toggle{display:flex;flex-direction:column;gap:6px}
+  .cams{position:fixed;left:26px;bottom:20px;z-index:7;display:flex;gap:8px;flex-direction:column}
+  .cams .btn{justify-content:flex-start;min-width:118px}
+
+  .label-toggle{position:fixed;right:26px;bottom:20px;z-index:7}
+  .museum{position:fixed;right:26px;bottom:66px;z-index:8;width:min(88vw,368px);max-height:74vh;overflow:auto;
+    background:linear-gradient(180deg,rgba(18,12,34,.96),rgba(9,6,18,.96));border:1px solid var(--panel-edge);border-radius:14px;padding:20px 22px;
+    box-shadow:0 18px 50px rgba(0,0,0,.6);font-size:13px;line-height:1.55;color:var(--ink-dim)}
+  .museum[hidden]{display:none}
+  .museum h2{font-family:var(--serif);font-style:italic;color:var(--neon2);font-size:18px;margin:0 0 4px}
+  .museum .meta{font-size:10px;letter-spacing:.18em;text-transform:uppercase;color:var(--ink-dim);margin-bottom:12px;opacity:.8}
+  .museum p{margin:0 0 10px}
+  .museum strong{color:var(--ink)}
+  .museum .keys{margin:14px 0 0;padding:12px 0 0;border-top:1px solid rgba(120,90,220,.2);font-size:11px;line-height:1.7}
+  .museum .keys b{display:inline-block;min-width:74px;color:var(--neon2);font-family:var(--serif);font-style:italic}
+
+  :focus-visible{outline:3px solid var(--focus);outline-offset:2px;border-radius:6px}
+  .sr{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}
+
+  @media(max-width:820px){
+    .titleplate{max-width:64vw}
+    .cams{flex-direction:row;left:12px;bottom:100px}.cams .btn{min-width:auto;padding:7px 10px}
+    .label-toggle{bottom:100px;right:12px}.museum{bottom:148px;right:12px}
+    .dock{gap:10px;padding:10px 12px}.group + .group{padding-left:10px}.field{min-width:74px}
+  }
+</style>
+</head>
+<body>
+<a href="#dock" class="skip">Skip to controls</a>
+<main id="stage" class="hide-chrome" role="application" aria-label="Playa Ignition — an animated Burning Man gallery you can steer">
+  <h1 class="sr">Playa Ignition — an animated generative Burning Man gallery</h1>
+  <canvas id="sky" aria-hidden="true"></canvas>
+  <canvas id="scene" aria-hidden="true"></canvas>
+  <div class="grain" aria-hidden="true"></div>
+
+  <div class="titleplate">
+    <p class="kicker">A Burning Man Gallery</p>
+    <h1 aria-hidden="true">Playa<br>Ignition</h1>
+    <p class="sub" id="phaseLabel">Deep Playa · after dark</p>
+  </div>
+
+  <button class="btn chrome-toggle" id="chromeBtn" aria-pressed="false">☰ Controls</button>
+
+  <div class="cams" role="group" aria-label="Camera view">
+    <button class="btn" data-cam="wide" aria-pressed="true">◈ The Playa</button>
+    <button class="btn" data-cam="man" aria-pressed="false">◈ The Man</button>
+    <button class="btn" data-cam="over" aria-pressed="false">◈ Overhead</button>
+  </div>
+
+  <div class="dock" id="dock" role="group" aria-label="Visual controls">
+    <div class="group">
+      <div class="toggle">
+        <button class="btn" id="playBtn" aria-pressed="true">❚❚ Pause</button>
+        <button class="btn" id="reduceBtn" aria-pressed="false">◐ Reduced Motion</button>
+      </div>
+      <div class="toggle">
+        <button class="btn burn" id="burnBtn">🔥 Ignite the Man</button>
+        <button class="btn" id="trailBtn" aria-pressed="true">✧ Light Trails</button>
+      </div>
+    </div>
+    <div class="group">
+      <div class="field"><label for="bpm">Beat <span class="val" id="bpmV">124 BPM</span></label><input type="range" id="bpm" min="60" max="180" value="124"></div>
+      <div class="field"><label for="neon">Neon <span class="val" id="neonV">Bright</span></label><input type="range" id="neon" min="0" max="100" value="75"></div>
+      <div class="field"><label for="crowd">Crowd <span class="val" id="crowdV">Full</span></label><input type="range" id="crowd" min="0" max="100" value="80"></div>
+      <div class="field"><label for="dust">Dust <span class="val" id="dustV">Drifting</span></label><input type="range" id="dust" min="0" max="100" value="55"></div>
+      <div class="field"><label for="bears">Dancing Bears <span class="val" id="bearsV">3</span></label><input type="range" id="bears" min="0" max="10" value="3" step="1"></div>
+    </div>
+  </div>
+
+  <button class="btn label-toggle" id="labelBtn" aria-expanded="false" aria-controls="museum">🏷 Museum Label</button>
+  <aside class="museum" id="museum" hidden aria-label="Museum label">
+    <h2>Playa Ignition</h2>
+    <div class="meta">Generative Canvas · Authored Visual Rhythm · Single File</div>
+    <p>A night on the open playa. The <strong>Man</strong> stands over the desert in cycling neon; fire-poi dancers trace glowing arcs, an art car drifts across the dust, and a colorful troupe dances beneath a vast desert starfield.</p>
+    <p><strong>The rhythm is authored, not measured.</strong> A built-in beat — set it in BPM — drives the neon flashes, the fire flares, and the dancers' hops. There is no audio; the pulse is written into the drawing loop itself, an original visual language of dust, light and fire.</p>
+    <p>Press <strong>Ignite the Man</strong> to set the effigy alight — flames climb, embers rise, the crowd surges — then the playa settles and the Man stands again. No borrowed logos, emblems, or art: an original homage to the spirit of the burn.</p>
+    <div class="keys">
+      <div><b>Space</b> pause / play</div>
+      <div><b>B</b> ignite the Man</div>
+      <div><b>M</b> reduced motion</div>
+      <div><b>T</b> light trails</div>
+      <div><b>C</b> show / hide controls</div>
+      <div><b>1 2 3</b> playa · man · overhead</div>
+      <div><b>Click</b> spark a flare</div>
+      <div><b>L</b> this museum label</div>
+    </div>
+  </aside>
+  <p class="sr" aria-live="polite" id="live">The playa at night. Set the beat and ignite the Man.</p>
+</main>
+
+<script>
+(() => {
+"use strict";
+const sky=document.getElementById('sky'), scene=document.getElementById('scene');
+const sctx=sky.getContext('2d'), ctx=scene.getContext('2d');
+const live=document.getElementById('live');
+
+const S={ t:0, playing:true, reduced:false, trails:true,
+  bpm:124, neon:0.75, crowd:0.8, dust:0.55, bears:3,
+  cam:'wide', burn:0, pulse:0, beatCount:0, _beatPhase:0,
+  W:0,H:0,DPR:1 };
+if(window.matchMedia && matchMedia('(prefers-reduced-motion: reduce)').matches) S.reduced=true;
+
+function clamp(v,a,b){return Math.max(a,Math.min(b,v));}
+function announce(m){ live.textContent=m; }
+// cutout helper (flat fill + bold outline)
+const OUTLINE='rgba(6,4,14,0.92)';
+function cut(fill,w){ ctx.fillStyle=fill; ctx.strokeStyle=OUTLINE; ctx.lineWidth=w||2.4; ctx.lineJoin='round'; ctx.lineCap='round'; }
+function fo(){ ctx.fill(); ctx.stroke(); }
+
+// ---------- resize + starfield ----------
+let stars=[];
+function resize(){
+  S.DPR=Math.min(2,window.devicePixelRatio||1); S.W=innerWidth; S.H=innerHeight;
+  [sky,scene].forEach(c=>{ c.width=Math.round(S.W*S.DPR); c.height=Math.round(S.H*S.DPR); c.getContext('2d').setTransform(S.DPR,0,0,S.DPR,0,0); });
+  stars=[]; const n=Math.round(S.W*S.H/2600);
+  for(let i=0;i<n;i++) stars.push({x:Math.random()*S.W, y:Math.random()*S.H*0.68, r:Math.random()*1.4+0.2, tw:Math.random()*6.28});
+  skyDirty=true;
+}
+addEventListener('resize',resize);
+
+// ---------- sky ----------
+let skyDirty=true;
+function drawSky(){
+  const W=S.W,H=S.H;
+  const g=sctx.createLinearGradient(0,0,0,H);
+  g.addColorStop(0,'#07061a'); g.addColorStop(0.45,'#160f2e'); g.addColorStop(0.72,'#2a1836'); g.addColorStop(0.86,'#43303a'); g.addColorStop(1,'#5a4436');
+  sctx.clearRect(0,0,W,H); sctx.fillStyle=g; sctx.fillRect(0,0,W,H);
+  // milky way band
+  sctx.save(); sctx.globalAlpha=0.10; sctx.fillStyle='#9fb0ff';
+  sctx.beginPath(); sctx.ellipse(W*0.62,H*0.22,W*0.5,H*0.09,-0.5,0,7); sctx.fill(); sctx.restore();
+  // stars
+  for(const s of stars){ const tw=0.5+0.5*Math.abs(Math.sin(s.tw)); sctx.globalAlpha=tw; sctx.fillStyle='#eef0ff'; sctx.beginPath(); sctx.arc(s.x,s.y,s.r,0,7); sctx.fill(); }
+  sctx.globalAlpha=1;
+  // moon
+  const mx=W*0.8,my=H*0.16,mr=40;
+  const gl=sctx.createRadialGradient(mx,my,4,mx,my,mr*3); gl.addColorStop(0,'rgba(230,235,255,0.5)'); gl.addColorStop(1,'rgba(0,0,0,0)');
+  sctx.fillStyle=gl; sctx.beginPath(); sctx.arc(mx,my,mr*3,0,7); sctx.fill();
+  sctx.fillStyle='#e8ecff'; sctx.beginPath(); sctx.arc(mx,my,mr,0,7); sctx.fill();
+  skyDirty=false;
+}
+
+// ---------- ground / playa ----------
+function drawPlaya(){
+  const W=S.W,H=S.H, top=H*0.72;
+  const g=ctx.createLinearGradient(0,top,0,H);
+  g.addColorStop(0,'#4a3a34'); g.addColorStop(0.4,'#6a5040'); g.addColorStop(1,'#8a6a4a');
+  ctx.fillStyle=g; ctx.fillRect(0,top,W,H-top);
+  // dust haze at the horizon
+  const hz=ctx.createLinearGradient(0,top-40,0,top+50); hz.addColorStop(0,'rgba(180,150,110,0)'); hz.addColorStop(1,'rgba(180,150,110,0.28)');
+  ctx.fillStyle=hz; ctx.fillRect(0,top-40,W,90);
+  // playa cracks
+  ctx.strokeStyle='rgba(30,20,14,0.25)'; ctx.lineWidth=1;
+  for(let i=0;i<6;i++){ const y=top+20+i*((H-top)/6); ctx.beginPath(); for(let x=0;x<=W;x+=60){ ctx.lineTo(x, y+Math.sin(x*0.02+i)*3);} ctx.stroke(); }
+}
+
+// distant neon art on the horizon
+function drawInstallations(){
+  const W=S.W,H=S.H, hy=H*0.71, glow=S.neon*(0.7+(S.pulse||0)*0.6);
+  const cols=['#ff2fd0','#2fe6ff','#ffcf5a','#7affa0'];
+  const items=[[0.12,'ring'],[0.28,'tri'],[0.5,'dome'],[0.68,'ring'],[0.9,'tri']];
+  ctx.save(); ctx.globalCompositeOperation='lighter';
+  items.forEach((it,i)=>{
+    const x=W*it[0], c=cols[i%cols.length], sc=Math.min(W,H)/26;
+    ctx.strokeStyle=c; ctx.shadowColor=c; ctx.shadowBlur=14*glow; ctx.globalAlpha=0.35+0.4*glow; ctx.lineWidth=2;
+    if(it[1]==='ring'){ ctx.beginPath(); ctx.arc(x,hy-sc*0.6,sc*0.6,0,7); ctx.stroke(); }
+    else if(it[1]==='tri'){ ctx.beginPath(); ctx.moveTo(x,hy-sc*1.3); ctx.lineTo(x-sc*0.7,hy); ctx.lineTo(x+sc*0.7,hy); ctx.closePath(); ctx.stroke(); }
+    else { ctx.beginPath(); for(let a=0;a<=6;a++){ const an=Math.PI*a/6; ctx.lineTo(x+Math.cos(an)*sc*0.9, hy-Math.sin(an)*sc*0.9);} ctx.stroke();
+      for(let a=0;a<6;a++){ const an=Math.PI*a/6; ctx.beginPath(); ctx.moveTo(x,hy); ctx.lineTo(x+Math.cos(an)*sc*0.9,hy-Math.sin(an)*sc*0.9); ctx.stroke(); } }
+  });
+  ctx.restore(); ctx.shadowBlur=0; ctx.globalAlpha=1;
+}
+
+// ---------- The Man ----------
+let embers=[];
+function manTransform(){
+  const W=S.W,H=S.H; let cx=W*0.5, base=H*0.78, scale=Math.min(W,H)/620;
+  if(S.cam==='man'){ scale=Math.min(W,H)/330; base=H*0.84; }
+  if(S.cam==='over'){ base=H*0.6; scale=Math.min(W,H)/560; }
+  return {cx,base,scale};
+}
+function drawTheMan(){
+  const {cx,base,scale}=manTransform();
+  const burn=S.burn, glow=S.neon*(0.7+(S.pulse||0)*0.7);
+  const neonCol=`hsl(${(S.t*40)%360},90%,62%)`;
+  ctx.save(); ctx.translate(cx,base); ctx.scale(scale,scale);
+
+  // stepped base (pyramid)
+  cut('rgba(40,28,22,0.98)',3);
+  for(let i=0;i<3;i++){ const w=150-i*36, h=18; ctx.beginPath(); ctx.rect(-w,-i*h-h,w*2,h); fo(); }
+
+  // the figure (drawn on top of base, origin at base top ~ y=-54)
+  ctx.save(); ctx.translate(0,-54);
+  const sway=S.reduced?0:Math.sin(S.t*0.6)*0.02;
+  ctx.rotate(sway);
+  // body geometry as glowing neon truss
+  ctx.strokeStyle = burn>0.05 ? '#ffd24a' : neonCol;
+  ctx.shadowColor = ctx.strokeStyle; ctx.shadowBlur = 18*glow*(1+burn); ctx.lineWidth=6; ctx.lineCap='round';
+  ctx.globalAlpha=0.9;
+  // legs
+  ctx.beginPath(); ctx.moveTo(0,-40); ctx.lineTo(-34,0); ctx.moveTo(0,-40); ctx.lineTo(34,0); ctx.stroke();
+  // body trapezoid
+  ctx.beginPath(); ctx.moveTo(-22,-40); ctx.lineTo(22,-40); ctx.lineTo(16,-104); ctx.lineTo(-16,-104); ctx.closePath(); ctx.stroke();
+  // arms raised in a V
+  ctx.beginPath(); ctx.moveTo(-16,-100); ctx.lineTo(-52,-150); ctx.moveTo(16,-100); ctx.lineTo(52,-150); ctx.stroke();
+  // head
+  ctx.beginPath(); ctx.arc(0,-124,14,0,7); ctx.stroke();
+  ctx.shadowBlur=0; ctx.globalAlpha=1;
+
+  // burning: flames engulf the figure
+  if(burn>0.02){
+    ctx.globalCompositeOperation='lighter';
+    for(let i=0;i<26;i++){
+      const fx=(Math.random()-0.5)*70, fyBase=-Math.random()*150*burn;
+      const fl=(0.5+Math.random()*0.7)*burn;
+      const g=ctx.createRadialGradient(fx,fyBase,0,fx,fyBase,20*fl);
+      g.addColorStop(0,`rgba(255,${180+Math.random()*60|0},80,${0.5*fl})`); g.addColorStop(1,'rgba(255,60,0,0)');
+      ctx.fillStyle=g; ctx.beginPath(); ctx.arc(fx,fyBase,20*fl,0,7); ctx.fill();
+    }
+    ctx.globalCompositeOperation='source-over';
+  }
+  ctx.restore();
+  ctx.restore();
+
+  // spawn embers from the burn (in world space)
+  if(burn>0.3 && !S.reduced && Math.random()<0.6){
+    embers.push({x:cx+(Math.random()-0.5)*80*scale, y:base-120*scale, vx:(Math.random()-0.5)*20, vy:-(40+Math.random()*60), life:1});
+  }
+}
+
+// ---------- fire-poi dancers ----------
+function fireDancer(x,y,s,ph){
+  const t=S.t, spin=t*4+ph, glow=0.6+(S.pulse||0)*0.6;
+  ctx.save(); ctx.translate(x,y); ctx.scale(s,s);
+  // body silhouette
+  cut('rgba(24,16,30,0.96)',2);
+  ctx.beginPath(); ctx.ellipse(0,-14,4,8,0,0,7); fo();          // torso
+  ctx.beginPath(); ctx.arc(0,-24,3.4,0,7); fo();                // head
+  ctx.beginPath(); ctx.moveTo(-2,-4); ctx.lineTo(-3-Math.sin(t*2)*2,10); ctx.moveTo(2,-4); ctx.lineTo(3+Math.sin(t*2)*2,10); ctx.strokeStyle=OUTLINE; ctx.lineWidth=2.4; ctx.stroke();
+  // two poi on trails
+  ctx.globalCompositeOperation='lighter';
+  for(const dir of [1,-1]){
+    const r=16;
+    ctx.strokeStyle=`rgba(255,150,40,${0.28*glow})`; ctx.lineWidth=3; ctx.shadowColor='#ff8a2a'; ctx.shadowBlur=10*glow;
+    ctx.beginPath(); for(let a=0;a<=20;a++){ const an=spin*dir - a*0.28; ctx.lineTo(dir*8+Math.cos(an)*r, -16+Math.sin(an)*r); } ctx.stroke();
+    const hx=dir*8+Math.cos(spin*dir)*r, hy=-16+Math.sin(spin*dir)*r;
+    const g=ctx.createRadialGradient(hx,hy,0,hx,hy,7); g.addColorStop(0,'rgba(255,230,120,0.95)'); g.addColorStop(1,'rgba(255,90,0,0)');
+    ctx.fillStyle=g; ctx.beginPath(); ctx.arc(hx,hy,7,0,7); ctx.fill();
+  }
+  ctx.shadowBlur=0; ctx.globalCompositeOperation='source-over';
+  ctx.restore();
+}
+
+// ---------- art car ----------
+function drawArtCar(){
+  if(S.reduced) return;
+  const W=S.W,H=S.H, y=H*0.75, x=((S.t*26)%(W+400))-200;
+  const glow=S.neon;
+  ctx.save(); ctx.translate(x,y);
+  cut('rgba(20,14,26,0.98)',3);
+  ctx.beginPath(); ctx.moveTo(-70,0); ctx.lineTo(-56,-30); ctx.lineTo(40,-30); ctx.lineTo(56,-8); ctx.lineTo(70,0); ctx.closePath(); fo();  // body
+  ctx.beginPath(); ctx.rect(-30,-52,54,24); fo();                                                                                          // cabin
+  // neon underglow + trim
+  ctx.globalCompositeOperation='lighter';
+  ctx.strokeStyle=`rgba(47,230,255,${0.6*glow})`; ctx.shadowColor='#2fe6ff'; ctx.shadowBlur=14*glow; ctx.lineWidth=2.4;
+  ctx.beginPath(); ctx.moveTo(-72,4); ctx.lineTo(72,4); ctx.stroke();
+  ctx.strokeStyle=`rgba(255,47,208,${0.6*glow})`; ctx.shadowColor='#ff2fd0';
+  ctx.beginPath(); ctx.rect(-30,-52,54,24); ctx.stroke();
+  ctx.shadowBlur=0; ctx.globalCompositeOperation='source-over';
+  // wheels
+  cut('rgba(10,8,14,0.98)',2);
+  ctx.beginPath(); ctx.arc(-44,4,10,0,7); fo(); ctx.beginPath(); ctx.arc(40,4,10,0,7); fo();
+  ctx.restore();
+}
+
+// ---------- dancing bears (colorful, beat-synced) ----------
+const BEAR_COLORS=['rgba(255,79,140,0.98)','rgba(255,180,60,0.98)','rgba(80,220,200,0.98)','rgba(150,120,255,0.98)','rgba(255,120,70,0.98)','rgba(120,220,120,0.98)','rgba(255,140,210,0.98)','rgba(90,180,255,0.98)','rgba(230,210,70,0.98)','rgba(180,110,255,0.98)'];
+function bear(x,y,s,ph,FUR){
+  const beat=S.pulse||0, e=1+beat*1.0;
+  const sway=Math.sin(S.t*2.8+ph)*0.30*e;
+  const bob=Math.abs(Math.sin(S.t*5.4+ph))*3 + beat*13;
+  const step=Math.sin(S.t*2.8+ph);
+  const armL=Math.sin(S.t*3.6+ph), armR=Math.sin(S.t*3.6+ph+Math.PI);
+  const t=S.t;
+  ctx.save(); ctx.translate(x,y-bob); ctx.scale(s*1.2,s*1.2); ctx.rotate(sway);
+  function limb(px,py,ang,len,w,foot){ ctx.save(); ctx.translate(px,py); ctx.rotate(ang);
+    ctx.lineCap='round'; ctx.strokeStyle=OUTLINE; ctx.lineWidth=w+2.4; ctx.beginPath(); ctx.moveTo(0,0); ctx.lineTo(0,len); ctx.stroke();
+    ctx.strokeStyle=FUR; ctx.lineWidth=w; ctx.beginPath(); ctx.moveTo(0,0); ctx.lineTo(0,len); ctx.stroke();
+    if(foot){ cut(FUR,1.8); ctx.beginPath(); ctx.ellipse(0,len,3.2,2,0,0,7); fo(); } ctx.restore(); }
+  limb(-3.5,-3,0.12+step*0.7,12,6.5,true); limb(3.5,-3,0.12-step*0.7,12,6.5,true);
+  cut(FUR,2.6); ctx.beginPath(); ctx.ellipse(0,-12,8.5,11,0,0,7); fo();
+  ctx.fillStyle='rgba(255,255,255,0.22)'; ctx.beginPath(); ctx.ellipse(0,-10,4.5,6,0,0,7); ctx.fill();
+  limb(-7,-18,2.3+armL*0.7+beat*0.45,11,5,true); limb(7,-18,-2.3+armR*0.7-beat*0.45,11,5,true);
+  cut(FUR,2.4); ctx.beginPath(); ctx.arc(0,-30,7,0,7); fo();
+  ctx.beginPath(); ctx.arc(-6,-36,3,0,7); fo(); ctx.beginPath(); ctx.arc(6,-36,3,0,7); fo();
+  cut('rgba(210,170,140,0.98)',1.5); ctx.beginPath(); ctx.ellipse(0,-27,3.6,2.7,0,0,7); fo();
+  ctx.fillStyle=OUTLINE; ctx.beginPath(); ctx.arc(0,-29,1.1,0,7); ctx.fill();
+  ctx.restore();
+}
+
+// ---------- dust ----------
+let dustP=[];
+function drawDust(){
+  const W=S.W,H=S.H, want=Math.round(S.dust*90);
+  if(!S.reduced && S.playing) while(dustP.length<want){ dustP.push({x:Math.random()*W,y:H*0.72+Math.random()*(H*0.28),vx:8+Math.random()*22,r:0.6+Math.random()*1.8,a:0.1+Math.random()*0.25}); }
+  ctx.save(); ctx.fillStyle='#d8c0a0';
+  for(let i=dustP.length-1;i>=0;i--){ const p=dustP[i]; if(!S.reduced&&S.playing){ p.x+=p.vx*0.016*(1+(S.pulse||0)); if(p.x>W+10)p.x=-10; }
+    if(i>=want){ dustP.splice(i,1); continue; } ctx.globalAlpha=p.a; ctx.beginPath(); ctx.arc(p.x,p.y,p.r,0,7); ctx.fill(); }
+  ctx.restore(); ctx.globalAlpha=1;
+}
+
+// ---------- click flares ----------
+let flares=[];
+function spawnFlare(x,y){ if(flares.length>500) return; const n=18+((Math.random()*12)|0); const hue=(Math.random()*360)|0;
+  for(let i=0;i<n;i++){ const a=(i/n)*6.28+Math.random()*0.2, sp=1.4+Math.random()*2.6; flares.push({x,y,vx:Math.cos(a)*sp,vy:Math.sin(a)*sp,life:1,hue}); } }
+function drawFlares(dt){ if(!flares.length) return; ctx.save(); ctx.globalCompositeOperation='lighter';
+  for(let i=flares.length-1;i>=0;i--){ const p=flares[i]; p.x+=p.vx; p.y+=p.vy; p.vy+=0.03; p.vx*=0.98; p.vy*=0.98; p.life-=dt*0.85;
+    if(p.life<=0){ flares.splice(i,1); continue; } ctx.globalAlpha=Math.max(0,p.life); ctx.fillStyle=`hsl(${p.hue},90%,60%)`; ctx.beginPath(); ctx.arc(p.x,p.y,2.2*p.life+0.6,0,7); ctx.fill(); }
+  ctx.restore(); ctx.globalAlpha=1; }
+
+function drawEmbers(dt){ if(!embers.length) return; ctx.save(); ctx.globalCompositeOperation='lighter';
+  for(let i=embers.length-1;i>=0;i--){ const p=embers[i]; p.x+=p.vx*dt; p.y+=p.vy*dt; p.vy+=6*dt; p.life-=dt*0.5;
+    if(p.life<=0){ embers.splice(i,1); continue; } ctx.globalAlpha=Math.max(0,p.life); ctx.fillStyle='#ffb347'; ctx.beginPath(); ctx.arc(p.x,p.y,2*p.life+0.5,0,7); ctx.fill(); }
+  ctx.restore(); ctx.globalAlpha=1; }
+
+// ---------- populate the crowd ----------
+function drawCrowd(){
+  const W=S.W,H=S.H, gy=H*0.79, d=S.crowd;
+  // fire-poi dancers ring around the Man
+  const nP=Math.round(2+d*5);
+  for(let i=0;i<nP;i++){ const a=(i/nP)*Math.PI*2 + S.t*0.1; const rx=Math.cos(a)*W*0.22, rz=Math.sin(a); fireDancer(W*0.5+rx, gy+rz*20, 0.9+rz*0.15, i*1.7); }
+}
+
+// ---------- main loop ----------
+let last=performance.now();
+function frame(now){
+  const dt=Math.min(0.05,(now-last)/1000); last=now;
+  if(S.playing){ S.t += dt*1.0; }
+  // authored beat clock -> pulse
+  if(S.playing && !S.reduced){
+    S._beatPhase += dt*(S.bpm/60);
+    if(S._beatPhase>=1){ S._beatPhase-=1; S.pulse=1; S.beatCount++; }
+    S.pulse *= Math.pow(0.06, dt*4);       // decay per beat
+  } else S.pulse=0;
+  // burn decay
+  if(S.burn>0){ S.burn=Math.max(0,S.burn-dt*0.18); }
+
+  if(skyDirty) drawSky();
+  const W=S.W,H=S.H;
+  if(S.trails && !S.reduced && S.playing){ ctx.fillStyle='rgba(5,4,14,0.18)'; ctx.fillRect(0,0,W,H); }
+  else ctx.clearRect(0,0,W,H);
+
+  drawPlaya();
+  drawInstallations();
+  drawDust();
+  drawArtCar();
+  drawTheMan();
+  drawCrowd();
+  // dancing bears troupe across the playa foreground
+  const N=Math.max(0,Math.min(10,Math.round(S.bears)));
+  if(N>0){ const margin=W*0.06, span=W-2*margin, size=Math.max(0.9,Math.min(2.0,13/(N+2.5)));
+    for(let i=0;i<N;i++){ const x=(N===1)?W*0.16:margin+span*(i/(N-1)); bear(x, H*0.86+((i*53)%9-4), size, i*1.3, BEAR_COLORS[i%BEAR_COLORS.length]); } }
+  drawEmbers(dt);
+  drawFlares(dt);
+  requestAnimationFrame(frame);
+}
+
+// ---------- UI ----------
+const neonWords=['Dark','Dim','Warm','Bright','Blazing'], crowdWords=['Empty','Sparse','Lively','Full','Teeming'], dustWords=['Still','Faint','Drifting','Gusting','Whiteout'];
+function word(a,v){return a[Math.min(a.length-1,Math.floor(v*a.length))];}
+function slider(id,valId,arr,set,fmt){ const el=document.getElementById(id), v=document.getElementById(valId);
+  el.addEventListener('input',e=>{ const val=+e.target.value; set(val); v.textContent=fmt?fmt(val):word(arr,val/100); }); }
+document.getElementById('bpm').addEventListener('input',e=>{ S.bpm=+e.target.value; document.getElementById('bpmV').textContent=S.bpm+' BPM'; });
+slider('neon','neonV',neonWords,v=>S.neon=v/100);
+slider('crowd','crowdV',crowdWords,v=>S.crowd=v/100);
+slider('dust','dustV',dustWords,v=>S.dust=v/100);
+document.getElementById('bears').addEventListener('input',e=>{ S.bears=+e.target.value; document.getElementById('bearsV').textContent=String(S.bears); });
+
+const playBtn=document.getElementById('playBtn');
+playBtn.addEventListener('click',()=>{ S.playing=!S.playing; playBtn.setAttribute('aria-pressed',S.playing); playBtn.textContent=S.playing?'❚❚ Pause':'► Play'; announce(S.playing?'Playing.':'Paused.'); });
+const reduceBtn=document.getElementById('reduceBtn');
+reduceBtn.addEventListener('click',()=>{ S.reduced=!S.reduced; reduceBtn.setAttribute('aria-pressed',S.reduced); if(S.reduced) ctx.clearRect(0,0,S.W,S.H); announce(S.reduced?'Reduced motion on.':'Reduced motion off.'); });
+if(S.reduced) reduceBtn.setAttribute('aria-pressed','true');
+const trailBtn=document.getElementById('trailBtn');
+trailBtn.addEventListener('click',()=>{ S.trails=!S.trails; trailBtn.setAttribute('aria-pressed',S.trails); if(!S.trails) ctx.clearRect(0,0,S.W,S.H); announce(S.trails?'Light trails on.':'Light trails off.'); });
+const burnBtn=document.getElementById('burnBtn');
+function ignite(){ S.burn=1; announce('The Man is alight — the burn begins.'); }
+burnBtn.addEventListener('click',ignite);
+
+document.querySelectorAll('[data-cam]').forEach(b=>{ b.addEventListener('click',()=>{ S.cam=b.dataset.cam; document.querySelectorAll('[data-cam]').forEach(x=>x.setAttribute('aria-pressed',x===b)); ctx.clearRect(0,0,S.W,S.H); const nm={wide:'The playa',man:'The Man',over:'Overhead'}; announce(nm[S.cam]+' view.'); }); });
+
+const labelBtn=document.getElementById('labelBtn'), museum=document.getElementById('museum');
+labelBtn.addEventListener('click',()=>{ const open=museum.hidden; museum.hidden=!open; labelBtn.setAttribute('aria-expanded',String(open)); });
+
+const stageEl=document.getElementById('stage'), chromeBtn=document.getElementById('chromeBtn');
+function toggleChrome(){ const hidden=stageEl.classList.toggle('hide-chrome'); chromeBtn.setAttribute('aria-pressed',String(!hidden)); chromeBtn.textContent=hidden?'☰ Controls':'✕ Close'; announce(hidden?'Controls hidden.':'Controls shown.'); }
+chromeBtn.addEventListener('click',toggleChrome);
+
+stageEl.addEventListener('pointerdown',e=>{ if(e.target.closest('button,input,select,a,.dock,.cams,.museum,.label-toggle')) return; const r=scene.getBoundingClientRect(); spawnFlare(e.clientX-r.left,e.clientY-r.top); });
+
+addEventListener('keydown',e=>{
+  if(e.target.matches('input,button')&&(e.key===' '||e.key==='Enter')) return;
+  switch(e.key.toLowerCase()){
+    case ' ': e.preventDefault(); playBtn.click(); break;
+    case 'b': ignite(); break;
+    case 'm': reduceBtn.click(); break;
+    case 't': trailBtn.click(); break;
+    case 'c': toggleChrome(); break;
+    case 'l': labelBtn.click(); break;
+    case '1': document.querySelector('[data-cam="wide"]').click(); break;
+    case '2': document.querySelector('[data-cam="man"]').click(); break;
+    case '3': document.querySelector('[data-cam="over"]').click(); break;
+  }
+});
+
+resize();
+requestAnimationFrame(frame);
+})();
+</script>
+</body>
+</html>

(oldest)  ·  back to Playa Ignition  ·  Add deploy config for playa.agentabrams.com (Kamatera + Clou 7444435 →