diff --git a/index.html b/index.html
index 6d3d5d6..437077c 100644
--- a/index.html
+++ b/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,13 +404,13 @@ body {
if (container.children.length !== upper.length) {
container.innerHTML = '';
for (let i = 0; i < upper.length; i++) {
- container.appendChild(createFlap(upper[i]));
- }
- } else {
- for (let i = 0; i < upper.length; i++) {
- setFlapChar(container.children[i], upper[i]);
+ container.appendChild(createFlap(' '));
}
}
+
+ for (let i = 0; i < upper.length; i++) {
+ scheduleFlip(container.children[i], upper[i], i * STAGGER_MS);
+ }
}
// ─── Format Row ───────────────────────────────────────────────────