Add beat-synced bounce effect to character
Detects bass beats from CAVA data using a fast/slow EMA ratio. On each beat, applies a CSS scale+translateY animation to .beat-wrap without interfering with the APNG playback or positioning. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
d0f72716c2
commit
85726b8c41
1 changed files with 40 additions and 4 deletions
|
|
@ -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 {
|
|||
|
||||
<!-- ===================== ROBOT ===================== -->
|
||||
<div class="robot-wrap">
|
||||
<div class="beat-wrap">
|
||||
<img src="skeleton4.apng" class="skeleton-gif" alt="skeleton">
|
||||
<div class="robot-shadow"></div>
|
||||
</div>
|
||||
</div><!-- .robot-wrap -->
|
||||
|
||||
<!-- VU Meter -->
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue