feat(drum_highway_3d): audio-reactive ambience + score FX overlay (D4) (#708)

- BG_STYLES port (off/particles/lights/geometric) into a renderOrder -1
  group; _bgGetAnalyser/_bgReadBands (stems-first, one-shot #audio
  fallback, permanent-failure latch, 5ms bands cache); Ambience
  intensity slider + Audio-reactive toggle; remounts on style/intensity/
  palette change and across kit-change scene rebuilds
- Score-FX overlay canvas (guitar drawScoreFx adapted to internal
  scoring): +1 pops at the struck lane, ring pulse every 10-combo,
  milestone bursts at 25/50/100, red wash on 3+ streak break; pooled,
  cleared when idle, removed in teardown
- butterchurn/image/video deliberately out of scope
- Tests: style id validation + FX defaults (15 total)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Byron Gamatos
2026-07-02 09:01:56 +02:00
committed by GitHub
co-authored by Claude Fable 5
parent 4214ef365e
commit d409d55615
5 changed files with 621 additions and 0 deletions
+44
View File
@@ -101,6 +101,42 @@
Lane stripe and accent-ring strength — lower for a calmer deck.
</p>
<label for="drumh3d-fx-bgstyle" class="text-xs font-medium text-gray-400 mb-1 mt-3 block">Background ambience</label>
<select id="drumh3d-fx-bgstyle"
onchange="window.drumH3dSetBgStyle && window.drumH3dSetBgStyle(this.value)"
class="w-full bg-dark-700 border border-gray-800 rounded-lg px-3 py-2 text-xs text-gray-300 outline-none">
<option value="off">Off</option>
<option value="particles" selected>Particles</option>
<option value="lights">Stage lights</option>
<option value="geometric">Geometric</option>
</select>
<label for="drumh3d-fx-bgintensity" class="text-xs font-medium text-gray-400 mb-1 mt-2 block">
Ambience intensity <span id="drumh3d-fx-bgintensity-val" class="text-gray-500 font-mono">0.50</span>
</label>
<input type="range" id="drumh3d-fx-bgintensity"
min="0" max="1" step="0.05" value="0.5"
oninput="window.drumH3dSetFx && window.drumH3dSetFx('bgIntensity', this.value); document.getElementById('drumh3d-fx-bgintensity-val').textContent = parseFloat(this.value).toFixed(2)"
class="w-full">
<label for="drumh3d-fx-bgreactive" class="flex items-center gap-2 text-xs text-gray-300 cursor-pointer mt-2">
<input type="checkbox" id="drumh3d-fx-bgreactive" checked
onchange="window.drumH3dSetFx && window.drumH3dSetFx('bgReactive', this.checked)">
Audio-reactive ambience
</label>
<p class="text-xs text-gray-500 mt-1">
The backdrop pulses with the mix (stems analyser when a sloppak
is loaded). Off = it animates on time only.
</p>
<label for="drumh3d-fx-scorefx" class="flex items-center gap-2 text-xs text-gray-300 cursor-pointer mt-3">
<input type="checkbox" id="drumh3d-fx-scorefx" checked
onchange="window.drumH3dSetFx && window.drumH3dSetFx('scoreFx', this.checked)">
Score effects
</label>
<p class="text-xs text-gray-500 mt-1">
+1 pops on hits, a ring pulse every 10-combo, milestone bursts at
25/50/100, and a brief red flicker when a streak breaks.
</p>
<label for="drumh3d-fx-bloom" class="flex items-center gap-2 text-xs text-gray-300 cursor-pointer mt-3">
<input type="checkbox" id="drumh3d-fx-bloom" checked
onchange="window.drumH3dSetFx && window.drumH3dSetFx('bloom', this.checked)">
@@ -546,6 +582,8 @@
hydrateFxBool('timingFx', 'drumh3d-fx-timing');
hydrateFxBool('streakFx', 'drumh3d-fx-streak');
hydrateFxBool('cinematic', 'drumh3d-fx-cinematic');
hydrateFxBool('bgReactive', 'drumh3d-fx-bgreactive');
hydrateFxBool('scoreFx', 'drumh3d-fx-scorefx');
const hydrateFxRange = (key, elId, valId) => {
const n = parseFloat(localStorage.getItem('drum_h3d_bg_' + key));
if (!Number.isFinite(n)) return;
@@ -556,6 +594,12 @@
hydrateFxRange('hitFx', 'drumh3d-fx-hitfx', 'drumh3d-fx-hitfx-val');
hydrateFxRange('glow', 'drumh3d-fx-glow', 'drumh3d-fx-glow-val');
hydrateFxRange('vibrancy', 'drumh3d-fx-vibrancy', 'drumh3d-fx-vibrancy-val');
hydrateFxRange('bgIntensity', 'drumh3d-fx-bgintensity', 'drumh3d-fx-bgintensity-val');
const storedStyle = localStorage.getItem('drum_h3d_bg_style');
const styleSel = document.getElementById('drumh3d-fx-bgstyle');
if (storedStyle && Array.from(styleSel.options).some(o => o.value === storedStyle)) {
styleSel.value = storedStyle;
}
const storedTheme = localStorage.getItem('drum_h3d_bg_theme');
const themeSel = document.getElementById('drumh3d-fx-theme');
if (storedTheme && Array.from(themeSel.options).some(o => o.value === storedTheme)) {