feat(drum_highway_3d): hit FX — sparks, timing colors, lane flashes, kick pulse, open hi-hat (D2) (#697)

- Pooled spark bursts (PORTED highway_3d, pool 160) at the struck lane,
  timing-colored via _timingHex/_classifyTiming (±50ms window, inner 40%
  = on-time); streak-scaled counts (streakFx)
- Lane flashes resurrected as pooled additive gauss-tex quads at the hit
  line (timing-colored; red for wrong hits); kick = full-width quad
- Kick pulse: camera dip + amber floor wash, exp decay, hitFx-scaled
- Approach highlight: lane stripes brighten toward their next note
- Open hi-hat: hh_open renders a warm ring around the gem (closes TODO);
  orthogonal to accent/ghost/flam variants
- Settings: Hit sparks / Timing colours / Streak feedback toggles +
  Hit feedback intensity slider (drum_h3d_bg_*, live-applying)
- All FX resources pooled/shared, disposed in BOTH teardown paths
- Tests: _classifyTiming boundaries + FX defaults (10 total)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Byron Gamatos
2026-07-02 01:07:50 +02:00
committed by GitHub
co-authored by Claude Fable 5
parent beefd7bc09
commit 56419dc789
5 changed files with 443 additions and 19 deletions
+54 -5
View File
@@ -54,6 +54,46 @@
Soft light-bleed around the hit line and bright notes. Applies
live; turn off to reclaim GPU headroom on weak machines.
</p>
<label for="drumh3d-fx-sparks" class="flex items-center gap-2 text-xs text-gray-300 cursor-pointer mt-3">
<input type="checkbox" id="drumh3d-fx-sparks" checked
onchange="window.drumH3dSetFx && window.drumH3dSetFx('sparks', this.checked)">
Hit sparks
</label>
<p class="text-xs text-gray-500 mt-1">
A small particle burst on every scored pad hit.
</p>
<label for="drumh3d-fx-timing" class="flex items-center gap-2 text-xs text-gray-300 cursor-pointer mt-3">
<input type="checkbox" id="drumh3d-fx-timing" checked
onchange="window.drumH3dSetFx && window.drumH3dSetFx('timingFx', this.checked)">
Timing colours
</label>
<p class="text-xs text-gray-500 mt-1">
Tint hit feedback by timing — on-time green, early cyan, late
amber. Off = always green.
</p>
<label for="drumh3d-fx-streak" class="flex items-center gap-2 text-xs text-gray-300 cursor-pointer mt-3">
<input type="checkbox" id="drumh3d-fx-streak" checked
onchange="window.drumH3dSetFx && window.drumH3dSetFx('streakFx', this.checked)">
Streak feedback
</label>
<p class="text-xs text-gray-500 mt-1">
Spark bursts grow with your combo.
</p>
<label for="drumh3d-fx-hitfx" class="text-xs font-medium text-gray-400 mb-1 mt-3 block">
Hit feedback intensity <span id="drumh3d-fx-hitfx-val" class="text-gray-500 font-mono">0.70</span>
</label>
<input type="range" id="drumh3d-fx-hitfx"
min="0" max="1" step="0.05" value="0.7"
oninput="window.drumH3dSetFx && window.drumH3dSetFx('hitFx', this.value); document.getElementById('drumh3d-fx-hitfx-val').textContent = parseFloat(this.value).toFixed(2)"
class="w-full">
<p class="text-xs text-gray-500 mt-1">
Drives the lane flashes, approach glow and the kick camera pulse.
0 turns them off.
</p>
</div>
<!-- MIDI input -->
@@ -440,11 +480,20 @@
// FX toggles (drum_h3d_bg_* — guitar-parity graphics controls).
// Only explicit values override; absent/corrupt keys keep the
// default (ON), matching screen.js readFxSettings.
const storedBloom = localStorage.getItem('drum_h3d_bg_bloom');
if (storedBloom === '1' || storedBloom === 'true') {
document.getElementById('drumh3d-fx-bloom').checked = true;
} else if (storedBloom === '0' || storedBloom === 'false') {
document.getElementById('drumh3d-fx-bloom').checked = false;
const hydrateFxBool = (key, elId) => {
const raw = localStorage.getItem('drum_h3d_bg_' + key);
if (raw === '1' || raw === 'true') document.getElementById(elId).checked = true;
else if (raw === '0' || raw === 'false') document.getElementById(elId).checked = false;
};
hydrateFxBool('bloom', 'drumh3d-fx-bloom');
hydrateFxBool('sparks', 'drumh3d-fx-sparks');
hydrateFxBool('timingFx', 'drumh3d-fx-timing');
hydrateFxBool('streakFx', 'drumh3d-fx-streak');
const storedHitFx = parseFloat(localStorage.getItem('drum_h3d_bg_hitFx'));
if (Number.isFinite(storedHitFx)) {
const v = Math.min(1, Math.max(0, storedHitFx));
document.getElementById('drumh3d-fx-hitfx').value = String(v);
document.getElementById('drumh3d-fx-hitfx-val').textContent = v.toFixed(2);
}
} catch (e) {
console.warn('[Drum-Hwy3D settings] hydration failed:', e);