Add split-flap animation engine with staggered character stepping and sound
This commit is contained in:
parent
93d7f53ab4
commit
a2b6ab0546
1 changed files with 123 additions and 15 deletions
136
index.html
136
index.html
|
|
@ -196,8 +196,8 @@ body {
|
|||
(function() {
|
||||
'use strict';
|
||||
|
||||
// ─── Config ─────────────────────────────────────────────────────────
|
||||
// Column definitions - customize per use case
|
||||
// ─── Constants ──────────────────────────────────────────────────────
|
||||
const CHARSET = ' ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789:.-/,&()+\u2713\u2717';
|
||||
const COLS = [
|
||||
{ key: 'time', width: 7, header: 'TIME' },
|
||||
{ key: 'type', width: 10, header: 'TYPE' },
|
||||
|
|
@ -210,14 +210,19 @@ body {
|
|||
];
|
||||
const TOTAL_CHARS = COLS.reduce((s, c) => s + c.width, 0);
|
||||
const MAX_ROWS = 20;
|
||||
const FLIP_STEP_MS = 200;
|
||||
const STAGGER_MS = 20;
|
||||
const CHAR_WIDTH = 18;
|
||||
|
||||
// ─── State ──────────────────────────────────────────────────────────
|
||||
const flapChars = new Map();
|
||||
|
||||
// ─── Flap Element Creation ─────────────────────────────────────────
|
||||
function createFlap(ch) {
|
||||
ch = ch || ' ';
|
||||
const el = document.createElement('div');
|
||||
el.className = 'flap';
|
||||
el.dataset.char = ch;
|
||||
flapChars.set(el, ch);
|
||||
|
||||
const top = document.createElement('div');
|
||||
top.className = 'flap-top';
|
||||
|
|
@ -246,12 +251,115 @@ body {
|
|||
else if (ch === '\u2717') flapEl.classList.add('flap-red');
|
||||
}
|
||||
|
||||
// ─── Set flap character (no animation yet) ─────────────────────────
|
||||
function setFlapChar(flapEl, ch) {
|
||||
flapEl.dataset.char = ch;
|
||||
flapEl.querySelector('.flap-top .flap-char').textContent = ch;
|
||||
flapEl.querySelector('.flap-bottom .flap-char').textContent = ch;
|
||||
applyCharColor(flapEl, ch);
|
||||
// ─── Sound ─────────────────────────────────────────────────────────
|
||||
const flipAudio = new Audio('/split.mp3');
|
||||
flipAudio.loop = true;
|
||||
flipAudio.volume = 0;
|
||||
let fadeInterval = null;
|
||||
|
||||
function startFlipSound() {
|
||||
clearInterval(fadeInterval);
|
||||
flipAudio.currentTime = 3;
|
||||
flipAudio.volume = 0;
|
||||
flipAudio.play().catch(() => {});
|
||||
const target = 0.5;
|
||||
const step = target / 8;
|
||||
fadeInterval = setInterval(() => {
|
||||
flipAudio.volume = Math.min(flipAudio.volume + step, target);
|
||||
if (flipAudio.volume >= target) clearInterval(fadeInterval);
|
||||
}, 10);
|
||||
}
|
||||
|
||||
function stopFlipSound() {
|
||||
clearInterval(fadeInterval);
|
||||
const step = flipAudio.volume / 12;
|
||||
fadeInterval = setInterval(() => {
|
||||
flipAudio.volume = Math.max(flipAudio.volume - step, 0);
|
||||
if (flipAudio.volume <= 0) {
|
||||
clearInterval(fadeInterval);
|
||||
flipAudio.pause();
|
||||
}
|
||||
}, 10);
|
||||
}
|
||||
|
||||
// ─── Batch Flip Animation Engine ────────────────────────────────────
|
||||
const activeFlips = [];
|
||||
let animRunning = false;
|
||||
|
||||
function buildSteps(oldChar, newChar) {
|
||||
const oldIdx = CHARSET.indexOf(oldChar);
|
||||
const newIdx = CHARSET.indexOf(newChar);
|
||||
if (oldIdx === -1 || newIdx === -1) return [newChar];
|
||||
|
||||
const steps = [];
|
||||
if (newIdx >= oldIdx) {
|
||||
for (let i = oldIdx + 1; i <= newIdx; i++) steps.push(CHARSET[i]);
|
||||
} else {
|
||||
for (let i = oldIdx + 1; i < CHARSET.length; i++) steps.push(CHARSET[i]);
|
||||
for (let i = 0; i <= newIdx; i++) steps.push(CHARSET[i]);
|
||||
}
|
||||
if (steps.length === 0) return [newChar];
|
||||
|
||||
const maxSteps = 12;
|
||||
if (steps.length <= maxSteps) return steps;
|
||||
const sampled = [];
|
||||
for (let i = 0; i < maxSteps - 1; i++) {
|
||||
sampled.push(steps[Math.floor(i * (steps.length - 1) / (maxSteps - 1))]);
|
||||
}
|
||||
sampled.push(steps[steps.length - 1]);
|
||||
return sampled;
|
||||
}
|
||||
|
||||
function scheduleFlip(flapEl, newChar, delay) {
|
||||
const oldChar = flapChars.get(flapEl) || ' ';
|
||||
if (oldChar === newChar) return;
|
||||
|
||||
const topSpan = flapEl.querySelector('.flap-top .flap-char');
|
||||
const bottomSpan = flapEl.querySelector('.flap-bottom .flap-char');
|
||||
const steps = buildSteps(oldChar, newChar);
|
||||
const startTime = performance.now() + delay;
|
||||
|
||||
activeFlips.push({
|
||||
flapEl, topSpan, bottomSpan, steps,
|
||||
stepIdx: 0,
|
||||
nextTime: startTime
|
||||
});
|
||||
|
||||
if (!animRunning) {
|
||||
animRunning = true;
|
||||
startFlipSound();
|
||||
requestAnimationFrame(animLoop);
|
||||
}
|
||||
}
|
||||
|
||||
function animLoop(timestamp) {
|
||||
let stillActive = false;
|
||||
|
||||
for (let i = 0; i < activeFlips.length; i++) {
|
||||
const flip = activeFlips[i];
|
||||
if (flip.stepIdx >= flip.steps.length) continue;
|
||||
|
||||
stillActive = true;
|
||||
if (timestamp >= flip.nextTime) {
|
||||
const ch = flip.steps[flip.stepIdx];
|
||||
flip.topSpan.textContent = ch;
|
||||
flip.bottomSpan.textContent = ch;
|
||||
flapChars.set(flip.flapEl, ch);
|
||||
flip.stepIdx++;
|
||||
flip.nextTime = timestamp + FLIP_STEP_MS;
|
||||
if (flip.stepIdx >= flip.steps.length) {
|
||||
applyCharColor(flip.flapEl, ch);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (stillActive) {
|
||||
requestAnimationFrame(animLoop);
|
||||
} else {
|
||||
activeFlips.length = 0;
|
||||
animRunning = false;
|
||||
stopFlipSound();
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Row helpers ───────────────────────────────────────────────────
|
||||
|
|
@ -269,7 +377,7 @@ body {
|
|||
const flaps = rowEl.children;
|
||||
const padded = text.padEnd(flaps.length);
|
||||
for (let i = 0; i < flaps.length; i++) {
|
||||
setFlapChar(flaps[i], padded[i]);
|
||||
scheduleFlip(flaps[i], padded[i], i * STAGGER_MS);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -296,12 +404,12 @@ body {
|
|||
if (container.children.length !== upper.length) {
|
||||
container.innerHTML = '';
|
||||
for (let i = 0; i < upper.length; i++) {
|
||||
container.appendChild(createFlap(upper[i]));
|
||||
container.appendChild(createFlap(' '));
|
||||
}
|
||||
} else {
|
||||
}
|
||||
|
||||
for (let i = 0; i < upper.length; i++) {
|
||||
setFlapChar(container.children[i], upper[i]);
|
||||
}
|
||||
scheduleFlip(container.children[i], upper[i], i * STAGGER_MS);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue