diff --git a/plugin/contents/ui/dancer.html b/plugin/contents/ui/dancer.html index 987f54a..3ba1740 100644 --- a/plugin/contents/ui/dancer.html +++ b/plugin/contents/ui/dancer.html @@ -217,6 +217,19 @@ html, body { transition: opacity 0.8s; } +.beat-wrap { + display: block; + transform-origin: center bottom; +} +@keyframes beatBounce { + 0% { transform: scale(1.08) translateY(-10px); filter: brightness(1.4); } + 55% { transform: scale(0.96) translateY(4px); filter: brightness(1); } + 100% { transform: scale(1) translateY(0); filter: brightness(1); } +} +.beat-wrap.beat { + animation: beatBounce 0.35s cubic-bezier(0.22, 0.61, 0.36, 1) forwards; +} + .skeleton-gif { display: block; height: 40vh; @@ -703,8 +716,10 @@ html, body {
- skeleton -
+
+ skeleton +
+
@@ -886,12 +901,33 @@ for (let i = 0; i < VU_COUNT; i++) { vuBars.push(bar); } +// Beat detection +const BASS_BARS = 4; +const BEAT_THRESH = 1.5; +const BEAT_COOLDOWN = 280; +let bassSmooth = 0, bassEma = 0, lastBeat = 0; +const beatWrap = document.querySelector('.beat-wrap'); + +function detectBeat(vals) { + const bass = vals.slice(0, BASS_BARS).reduce((s, v) => s + v, 0) / BASS_BARS; + bassSmooth = bassSmooth * 0.6 + bass * 0.4; // fast smooth + bassEma = bassEma * 0.96 + bass * 0.04; // slow background + const now = performance.now(); + if (bassSmooth > bassEma * BEAT_THRESH && bassSmooth > 8 && now - lastBeat > BEAT_COOLDOWN) { + lastBeat = now; + beatWrap.classList.remove('beat'); + void beatWrap.offsetWidth; // force reflow to restart animation + beatWrap.classList.add('beat'); + } +} + // Called with semicolon-separated CAVA values (0–100 per bar) function updateBars(raw) { if (!isPlaying) return; - const vals = raw.split(';'); + const vals = raw.split(';').map(v => Math.min(100, parseInt(v, 10) || 0)); + detectBeat(vals); vuBars.forEach((bar, i) => { - const v = Math.min(100, parseInt(vals[i] || 0, 10)); + const v = vals[i] || 0; const h = Math.max(3, (v / 100) * MAX_BAR_H); bar.style.height = h + 'px'; bar.style.opacity = 0.3 + 0.7 * (v / 100);