[object Object]

← back to Scarlet Riverboat Masquerade

Sync objects to the music: beat detection drives pulsating lamps + fireworks bursts on the beat; click/tap anywhere launches a firework (Model Arena style). Additive, react-gated

6f9809ba7c0c52e4f3bf93c6c50ebaeb60900090 · 2026-09-01 18:27:44 -0700 · Steve Abrams

Files touched

Diff

commit 6f9809ba7c0c52e4f3bf93c6c50ebaeb60900090
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Tue Sep 1 18:27:44 2026 -0700

    Sync objects to the music: beat detection drives pulsating lamps + fireworks bursts on the beat; click/tap anywhere launches a firework (Model Arena style). Additive, react-gated
---
 index.html | 57 ++++++++++++++++++++++++++++++++++++++++++++++++++++++---
 1 file changed, 54 insertions(+), 3 deletions(-)

diff --git a/index.html b/index.html
index c2e78aa..4dea9ef 100644
--- a/index.html
+++ b/index.html
@@ -600,7 +600,7 @@ function drawBoat(){
     const nl = 6;
     for(let i=0;i<=nl;i++){
       const lx=-d.w + (2*d.w)*(i/nl);
-      const flick = S.reduced?1:(0.85+0.15*Math.sin(S.t*3+i+di));
+      const flick = S.reduced?1:(0.85+0.15*Math.sin(S.t*3+i+di)+(S.pulse||0)*0.7);  // lamps throb on the beat
       lantern(lx, d.y+d.h-4, 1, glow*flick, lan);
     }
   });
@@ -900,13 +900,47 @@ function drawRiver(){
   }
 }
 
+// ---------- Beat-triggered fireworks (only when React to Music is on) ----------
+let fireworks=[];
+function spawnFirework(px,py){
+  if(fireworks.length>600) return;
+  const W=S.W,H=S.H;
+  const x=(px!==undefined)?px:W*(0.30+Math.random()*0.40);
+  const y=(py!==undefined)?py:H*(0.14+Math.random()*0.20);
+  const color=pal('accent',0), warm=pal('lantern',0);
+  const n=20+((Math.random()*12)|0);
+  for(let i=0;i<n;i++){
+    const a=(i/n)*Math.PI*2 + Math.random()*0.2, sp=1.4+Math.random()*2.6;
+    fireworks.push({x,y,vx:Math.cos(a)*sp,vy:Math.sin(a)*sp,life:1,color: i%3?color:warm});
+  }
+}
+function drawFireworks(dt){
+  if(!fireworks.length) return;
+  ctx.save(); ctx.globalCompositeOperation='lighter';
+  for(let i=fireworks.length-1;i>=0;i--){
+    const p=fireworks[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){ fireworks.splice(i,1); continue; }
+    ctx.globalAlpha=Math.max(0,p.life);
+    ctx.fillStyle=p.color;
+    ctx.beginPath(); ctx.arc(p.x,p.y,2.2*p.life+0.6,0,7); ctx.fill();
+  }
+  ctx.restore(); ctx.globalAlpha=1;
+}
+let _seenBeat=0;
+
 // ---------- Main loop ----------
 let last=performance.now();
 function frame(now){
   const dt=Math.min(0.05,(now-last)/1000); last=now;
 
   // live audio energy (0..1) when "React to Music" is on — otherwise motion stays authored
-  S.amp = (window.SRM && window.SRM.react) ? (window.SRM.level||0) : 0;
+  const reacting = !!(window.SRM && window.SRM.react);
+  S.amp = reacting ? (window.SRM.level||0) : 0;
+  S.pulse = reacting ? (window.SRM.pulse||0) : 0;   // sharp per-beat throb for lamps
+  // fire a burst on each new detected beat
+  if(reacting && !S.reduced && window.SRM.beat>_seenBeat){ _seenBeat=window.SRM.beat; spawnFirework(); }
+  else if(!reacting){ _seenBeat=window.SRM?(window.SRM.beat||0):0; }   // let fireworks decay naturally
 
   if(S.playing){
     const spd = S.reduced ? 0.28 : 1.0;
@@ -945,6 +979,7 @@ function frame(now){
   drawRiver();
   drawAnimals();
   drawBoat();
+  drawFireworks(dt);
 
   requestAnimationFrame(frame);
 }
@@ -1069,6 +1104,14 @@ window.addEventListener('keydown',e=>{
   }
 });
 
+// click / tap anywhere on the scene (not on a control) to launch a firework
+document.getElementById('stage').addEventListener('pointerdown', e=>{
+  if(e.target.closest('button,input,select,a,.dock,.cams,.chapters,.musicroom,.museum,.label-toggle')) return;
+  const r=scene.getBoundingClientRect();
+  spawnFirework(e.clientX-r.left, e.clientY-r.top);
+  spawnFirework(e.clientX-r.left, e.clientY-r.top);  // a fuller double burst on a deliberate tap
+});
+
 // init
 resize();
 requestAnimationFrame(frame);
@@ -1197,12 +1240,20 @@ function ensureGraph(){
     return true;
   }catch(e){ return false; }
 }
+let _avg=0, _clock=0, _lastBeat=-99;
 function sampleLevel(){
   if(_analyser){
     _analyser.getByteFrequencyData(_freq);
     let sum=0, n=Math.max(1,Math.floor(_freq.length/6));  // low-end (bass) energy
     for(let i=0;i<n;i++) sum+=_freq[i];
-    window.SRM.level = window.SRM.level*0.6 + ((sum/n)/255)*0.4;  // smoothed 0..1
+    const raw=(sum/n)/255;
+    window.SRM.level = window.SRM.level*0.6 + raw*0.4;       // smoothed 0..1
+    // beat detection: a bass spike well above the running average, rate-limited
+    _avg = _avg*0.92 + raw*0.08; _clock++;
+    if(raw > _avg*1.15 && raw > 0.16 && (_clock-_lastBeat) > 6){
+      _lastBeat=_clock; window.SRM.beat=(window.SRM.beat||0)+1; window.SRM.pulse=1;
+    }
+    window.SRM.pulse = (window.SRM.pulse||0)*0.86;           // decaying throb (spikes to 1 on a beat)
   }
   requestAnimationFrame(sampleLevel);
 }

← 3e917c7 Add optional 'React to Music' mode: Web Audio AnalyserNode o  ·  back to Scarlet Riverboat Masquerade  ·  Add Photoshop-composited canyon-backdrop.png asset + two dan b8b147f →