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;
|
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 {
|
.skeleton-gif {
|
||||||
display: block;
|
display: block;
|
||||||
height: 40vh;
|
height: 40vh;
|
||||||
|
|
@ -703,8 +716,10 @@ html, body {
|
||||||
|
|
||||||
<!-- ===================== ROBOT ===================== -->
|
<!-- ===================== ROBOT ===================== -->
|
||||||
<div class="robot-wrap">
|
<div class="robot-wrap">
|
||||||
<img src="skeleton4.apng" class="skeleton-gif" alt="skeleton">
|
<div class="beat-wrap">
|
||||||
<div class="robot-shadow"></div>
|
<img src="skeleton4.apng" class="skeleton-gif" alt="skeleton">
|
||||||
|
<div class="robot-shadow"></div>
|
||||||
|
</div>
|
||||||
</div><!-- .robot-wrap -->
|
</div><!-- .robot-wrap -->
|
||||||
|
|
||||||
<!-- VU Meter -->
|
<!-- VU Meter -->
|
||||||
|
|
@ -886,12 +901,33 @@ for (let i = 0; i < VU_COUNT; i++) {
|
||||||
vuBars.push(bar);
|
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)
|
// Called with semicolon-separated CAVA values (0–100 per bar)
|
||||||
function updateBars(raw) {
|
function updateBars(raw) {
|
||||||
if (!isPlaying) return;
|
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) => {
|
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);
|
const h = Math.max(3, (v / 100) * MAX_BAR_H);
|
||||||
bar.style.height = h + 'px';
|
bar.style.height = h + 'px';
|
||||||
bar.style.opacity = 0.3 + 0.7 * (v / 100);
|
bar.style.opacity = 0.3 + 0.7 * (v / 100);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue