Baktiyar Ablaikhan

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).