← 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 →