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() {
|
(function() {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
// ─── Config ─────────────────────────────────────────────────────────
|
// ─── Constants ──────────────────────────────────────────────────────
|
||||||
// Column definitions - customize per use case
|
const CHARSET = ' ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789:.-/,&()+\u2713\u2717';
|
||||||
const COLS = [
|
const COLS = [
|
||||||
{ key: 'time', width: 7, header: 'TIME' },
|
{ key: 'time', width: 7, header: 'TIME' },
|
||||||
{ key: 'type', width: 10, header: 'TYPE' },
|
{ key: 'type', width: 10, header: 'TYPE' },
|
||||||
|
|
@ -210,14 +210,19 @@ body {
|
||||||
];
|
];
|
||||||
const TOTAL_CHARS = COLS.reduce((s, c) => s + c.width, 0);
|
const TOTAL_CHARS = COLS.reduce((s, c) => s + c.width, 0);
|
||||||
const MAX_ROWS = 20;
|
const MAX_ROWS = 20;
|
||||||
|
const FLIP_STEP_MS = 200;
|
||||||
|
const STAGGER_MS = 20;
|
||||||
const CHAR_WIDTH = 18;
|
const CHAR_WIDTH = 18;
|
||||||
|
|
||||||
|
// ─── State ──────────────────────────────────────────────────────────
|
||||||
|
const flapChars = new Map();
|
||||||
|
|
||||||
// ─── Flap Element Creation ─────────────────────────────────────────
|
// ─── Flap Element Creation ─────────────────────────────────────────
|
||||||
function createFlap(ch) {
|
function createFlap(ch) {
|
||||||
ch = ch || ' ';
|
ch = ch || ' ';
|
||||||
const el = document.createElement('div');
|
const el = document.createElement('div');
|
||||||
el.className = 'flap';
|
el.className = 'flap';
|
||||||
el.dataset.char = ch;
|
flapChars.set(el, ch);
|
||||||
|
|
||||||
const top = document.createElement('div');
|
const top = document.createElement('div');
|
||||||
top.className = 'flap-top';
|
top.className = 'flap-top';
|
||||||
|
|
@ -246,12 +251,115 @@ body {
|
||||||
else if (ch === '\u2717') flapEl.classList.add('flap-red');
|
else if (ch === '\u2717') flapEl.classList.add('flap-red');
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Set flap character (no animation yet) ─────────────────────────
|
// ─── Sound ─────────────────────────────────────────────────────────
|
||||||
function setFlapChar(flapEl, ch) {
|
const flipAudio = new Audio('/split.mp3');
|
||||||
flapEl.dataset.char = ch;
|
flipAudio.loop = true;
|
||||||
flapEl.querySelector('.flap-top .flap-char').textContent = ch;
|
flipAudio.volume = 0;
|
||||||
flapEl.querySelector('.flap-bottom .flap-char').textContent = ch;
|
let fadeInterval = null;
|
||||||
applyCharColor(flapEl, ch);
|
|
||||||
|
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 ───────────────────────────────────────────────────
|
// ─── Row helpers ───────────────────────────────────────────────────
|
||||||
|
|
@ -269,7 +377,7 @@ body {
|
||||||
const flaps = rowEl.children;
|
const flaps = rowEl.children;
|
||||||
const padded = text.padEnd(flaps.length);
|
const padded = text.padEnd(flaps.length);
|
||||||
for (let i = 0; i < flaps.length; i++) {
|
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) {
|
if (container.children.length !== upper.length) {
|
||||||
container.innerHTML = '';
|
container.innerHTML = '';
|
||||||
for (let i = 0; i < upper.length; i++) {
|
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++) {
|
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