Hop poster
- Source
- arlan.me/vault ↗
- Author
- Arlan
- Licence
- MIT
Loading…
Four lines on highlight bars hop in from the edges, hold and scatter, leaving flat blocks of colour that flood each new palette.
Code
"use client";
import { CanvasStudy, hash, prog, type Frame, type Painter } from "./stage";
export const BG = "#3f3fff";
const PERIOD = 4.8; // a new pass starts every PERIOD seconds; passes overlap
const CUT = 0.95; // the ground switches to the new pass this long after it starts
const HOPS_IN = 6;
const HOPS_OUT = 5;
type Pass = { bg: string; bar: string; ink: string; lines: string[] };
const PASSES: Pass[] = [
{ bg: "#3f3fff", bar: "#ffe14a", ink: "#111111", lines: ["MOVE", "WITH THE", "SQUARE", "GRID"] },
{ bg: "#ff5a36", bar: "#141414", ink: "#ffffff", lines: ["SAY IT", "OUT", "LOUD AND", "FLAT"] },
{ bg: "#141414", bar: "#7cf29c", ink: "#141414", lines: ["TYPE", "IN SHORT", "HOPS", "ONLY"] },
{ bg: "#f2ede3", bar: "#ff3fa4", ink: "#ffffff", lines: ["NEW", "WORDS", "NEW", "COLOURS"] },
{ bg: "#00a86b", bar: "#f2ede3", ink: "#00a86b", lines: ["READ", "IT", "THEN", "LEAVE"] },
];
type Rect = { x: number; y: number; w: number; h: number };
type LineState = { rect: Rect; trail: Rect[]; visible: boolean };
function lineState(k: number, j: number, local: number, w: number, h: number, barW: number): LineState {
const bh = h * 0.15;
const gap = h * 0.028;
const top = h / 2 - (4 * bh + 3 * gap) / 2;
const fx = w * (0.07 + hash(j, k) * 0.3);
const fy = top + j * (bh + gap);
const edge = Math.floor(hash(j + 3, k) * 4); // 0 left, 1 right, 2 top, 3 bottom
const from: [number, number] = edge === 0 ? [-barW, fy] : edge === 1 ? [w, fy] : edge === 2 ? [fx, -bh] : [fx, h];
const out = Math.floor(hash(j + 9, k) * 4);
const to: [number, number] = out === 0 ? [-barW, fy] : out === 1 ? [w, fy] : out === 2 ? [fx, -bh] : [fx, h];
const trail: Rect[] = [];
const rect = (x: number, y: number): Rect => ({ x, y, w: barW, h: bh });
const kin = prog(local, j * 0.16, 1.0);
const hopIn = Math.min(HOPS_IN, Math.floor(kin * HOPS_IN + 1e-6));
for (let i = 1; i <= hopIn; i++) {
const f = (i - 1) / HOPS_IN;
if (i > 1) trail.push(rect(from[0] + (fx - from[0]) * f, from[1] + (fy - from[1]) * f));
}
let x = from[0] + (fx - from[0]) * (hopIn / HOPS_IN);
let y = from[1] + (fy - from[1]) * (hopIn / HOPS_IN);
const kout = prog(local, 2.7 + j * 0.12, 0.9);
const hopOut = Math.min(HOPS_OUT, Math.floor(kout * HOPS_OUT + 1e-6));
for (let i = 1; i <= hopOut; i++) {
const f = (i - 1) / HOPS_OUT;
trail.push(rect(fx + (to[0] - fx) * f, fy + (to[1] - fy) * f));
}
if (hopOut > 0) {
x = fx + (to[0] - fx) * (hopOut / HOPS_OUT);
y = fy + (to[1] - fy) * (hopOut / HOPS_OUT);
}
return { rect: rect(x, y), trail, visible: hopIn > 0 && hopOut < HOPS_OUT };
}
export function create(): Painter {
return ({ ctx, w, h, t, fonts }: Frame) => {
const base = Math.max(0, Math.floor((t - CUT) / PERIOD));
const passes = [base, base + 1].filter((k) => t >= k * PERIOD);
ctx.fillStyle = PASSES[base % PASSES.length].bg;
ctx.fillRect(0, 0, w, h);
const fs = h * 0.105;
ctx.font = `800 ${fs}px ${fonts.sans}`;
ctx.textBaseline = "middle";
for (const k of passes) {
const pass = PASSES[k % PASSES.length];
const local = t - k * PERIOD;
const states = pass.lines.map((s, j) => lineState(k, j, local, w, h, ctx.measureText(s).width + fs * 0.7));
// flat blocks where the lines just were
ctx.fillStyle = pass.bar;
states.forEach((st) => st.trail.forEach((r) => ctx.fillRect(Math.round(r.x), Math.round(r.y), Math.ceil(r.w), Math.ceil(r.h))));
states.forEach((st, j) => {
if (!st.visible) return;
const r = st.rect;
ctx.fillStyle = pass.bar;
ctx.fillRect(Math.round(r.x), Math.round(r.y), Math.ceil(r.w), Math.ceil(r.h));
ctx.fillStyle = pass.ink;
ctx.fillText(pass.lines[j], Math.round(r.x + fs * 0.35), Math.round(r.y + r.h / 2 + fs * 0.04));
});
}
};
}
export default function HopPoster() {
return (
<CanvasStudy
create={create}
background={BG}
still={2}
label="A kinetic poster: four lines on highlight bars hop in from the edges, hold, and scatter, leaving flat blocks of colour that flood each new pass."
/>
);
}
Prompt
Build "Hop poster", a text animation. What it does: Four lines on highlight bars hop in from the edges, hold and scatter, leaving flat blocks of colour that flood each new palette. 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).