Flipbook drop
- Source
- arlan.me/vault ↗
- Author
- Arlan
- Licence
- MIT
Loading…
At ten frames a second the letters of 'Motion' fall, stretch and squash with a lagging hard shadow, then inflate and pop into a ring of dashes.
Code
"use client";
import { CanvasStudy, easeIn, easeOut, prog, type Frame, type Painter } from "./stage";
export const BG = "#ffffff";
const WORD = "Motion";
const CYCLE = 4.8;
const FPS = 10;
// [letters, shadow]
const PAIRS: [string, string][] = [
["#ec3445", "#1b1b1b"],
["#2f3be8", "#ff7a3c"],
["#1b1b1b", "#ffcf2e"],
["#ff4fa0", "#2f3be8"],
["#14a66e", "#1b1b1b"],
["#ff7a3c", "#6a2be8"],
];
/** Fall, stretch, squash and settle for one letter; returns [dy, sx, sy] (dy in fractions of h). */
function drop(local: number): [number, number, number] {
const fall = prog(local, 0, 0.32);
if (fall < 1) {
const k = easeIn(fall);
return [-(1 - k) * 0.85, 0.82, 1.32];
}
const s = local - 0.32;
if (s < 0.45) {
const damp = Math.exp(-s * 9) * Math.cos(s * 22);
return [0, 1 + 0.32 * damp, 1 - 0.3 * damp];
}
return [0, 1, 1];
}
export function create(): Painter {
return ({ ctx, w, h, t: raw, fonts }: Frame) => {
const t = Math.floor(raw * FPS) / FPS; // flipbook stepping
ctx.fillStyle = BG;
ctx.fillRect(0, 0, w, h);
const cycle = Math.floor(t / CYCLE);
const u = t - cycle * CYCLE;
const [fill, shade] = PAIRS[cycle % PAIRS.length];
const next = PAIRS[(cycle + 1) % PAIRS.length][0];
ctx.font = `800 100px ${fonts.sans}`;
const tight = 0.84;
const raw100 = [...WORD].reduce((a, c) => a + ctx.measureText(c).width * tight, 0);
const fs = Math.min(h * 0.42, (w * 0.62 * 100) / raw100);
ctx.font = `800 ${fs}px ${fonts.sans}`;
ctx.textBaseline = "alphabetic";
const widths = [...WORD].map((c) => ctx.measureText(c).width);
const total = widths.reduce((a, b) => a + b * tight, 0);
const base = h / 2 + fs * 0.36;
const sh = { x: -fs * 0.055, y: fs * 0.04 };
const inflate = easeIn(prog(u, 2.2, 1.35)) ** 1.6;
const popAt = 3.55;
const scale = 1 + inflate * 0.7;
if (u < popAt) {
ctx.save();
ctx.translate(w / 2, h / 2);
ctx.scale(scale, scale);
ctx.translate(-w / 2, -h / 2);
for (const layer of [0, 1]) {
let x = (w - total) / 2;
[...WORD].forEach((c, i) => {
const start = 0.1 + i * 0.2 + (layer === 0 ? 0.12 : 0); // shadows arrive a beat behind
const local = u - start;
const cw = widths[i];
if (local >= 0) {
const [dy, sx, sy] = drop(local);
ctx.save();
ctx.translate(x + cw / 2 + (layer === 0 ? sh.x : 0), base + dy * h + (layer === 0 ? sh.y : 0));
ctx.scale(sx, sy);
ctx.fillStyle = layer === 0 ? shade : fill;
ctx.fillText(c, -cw / 2, 0);
ctx.restore();
}
x += cw * tight;
});
}
ctx.restore();
} else {
// pop: a ring of small dashes in the next colour flies outward
const k = prog(u, popAt, 0.55);
if (k < 1) {
const R0 = (total * scale) / 2;
const R = R0 * 0.6 + easeOut(k) * w * 0.45;
const len = fs * 0.22 * (1 - k * 0.7);
ctx.strokeStyle = next;
ctx.lineCap = "round";
ctx.lineWidth = Math.max(2, fs * 0.05);
const N = 26;
for (let i = 0; i < N; i++) {
const a = (i / N) * Math.PI * 2;
const cx = Math.cos(a);
const cy = Math.sin(a) * 0.62;
ctx.beginPath();
ctx.moveTo(w / 2 + cx * R, h / 2 + cy * R);
ctx.lineTo(w / 2 + cx * (R + len), h / 2 + cy * (R + len));
ctx.stroke();
}
}
}
};
}
export default function FlipbookMotion() {
return (
<CanvasStudy
create={create}
background={BG}
still={2}
label="Chunky letters of 'Motion' with a hard drop shadow fall in at ten frames a second, squash, inflate and pop into a ring of dashes."
/>
);
}
Prompt
Build "Flipbook drop", a text animation. What it does: At ten frames a second the letters of 'Motion' fall, stretch and squash with a lagging hard shadow, then inflate and pop into a ring of dashes. Write it as a single self-contained React component in TypeScript that fills its parent (w-full h-full), styled with Tailwind CSS. Technique: an HTML canvas, a requestAnimationFrame loop, pointer tracking. It sits on a light background, in a 16:10 frame. Respect prefers-reduced-motion with a still state, pause work when the element is off screen, and keep it accessible. Original: https://www.arlan.me/vault (Arlan, MIT).