Baktiyar Ablaikhan

Streak tear

Source
arlan.me/vault ↗
Author
Arlan
Licence
MIT

Loading…

'gone', 'before', 'you', 'look' hold sharp, then swing toward you and tear into green and violet streaks; pointing holds the word at its largest.

Code

"use client";

import { CanvasStudy, clamp, easeIn, easeOut, makeCanvas, type Frame, type Painter } from "./stage";

export const BG = "#07070a";
const WORDS = ["gone", "before", "you", "look"];
const DUR = 2.1;
const PEAK = 1.78; // the word at its largest
const TINTS = { white: "#f5f3ee", green: "#36f59a", violet: "#9b5cff" };

type Img = { c: HTMLCanvasElement; w: number; h: number };

export function create(): Painter {
  let clock = 0;
  let key = "";
  const cache = new Map<string, Img>();

  const word = (s: string, tint: keyof typeof TINTS, fs: number, font: string, dpr: number): Img => {
    const k = `${s}|${tint}`;
    const hit = cache.get(k);
    if (hit) return hit;
    const probe = makeCanvas(1, 1).ctx;
    probe.font = `600 ${fs}px ${font}`;
    const w = Math.ceil(probe.measureText(s).width + fs * 0.2);
    const h = Math.ceil(fs * 1.3);
    const { c, ctx } = makeCanvas(w * dpr, h * dpr);
    ctx.scale(dpr, dpr);
    ctx.font = `600 ${fs}px ${font}`;
    ctx.textBaseline = "middle";
    ctx.fillStyle = TINTS[tint];
    ctx.fillText(s, fs * 0.1, h / 2);
    const img = { c, w, h };
    cache.set(k, img);
    return img;
  };

  return ({ ctx, w, h, t, dt, p, fonts, dpr, reduced }: Frame) => {
    if (reduced) clock = t;
    else {
      const local = clock % DUR;
      const next = local + dt;
      // pointing at it holds the word at its largest
      if (p.inside && local <= PEAK && next >= PEAK) clock += PEAK - local;
      else if (!(p.inside && Math.abs(local - PEAK) < 1e-6)) clock += dt;
    }
    ctx.fillStyle = BG;
    ctx.fillRect(0, 0, w, h);

    const fs = Math.min(h * 0.3, w * 0.16);
    const k = `${w}|${h}|${dpr}|${fonts.sans}`;
    if (k !== key) {
      key = k;
      cache.clear();
    }
    const n = Math.floor(clock / DUR);
    const local = clock - n * DUR;
    const s = WORDS[n % WORDS.length];

    let scale = 1;
    let stretch = 1;
    let lift = 0;
    let tear = 0;
    let turn = 0;
    if (local < 0.4) {
      // falls back out of the blur
      const e = easeOut(local / 0.4);
      stretch = 1 + (1 - e) * 16;
      scale = 1 + (1 - e) * 0.6;
      tear = 1 - e;
      lift = -(1 - e) * h * 0.1;
    } else if (local > 1.15) {
      const e = easeIn(clamp((local - 1.15) / (PEAK - 1.15)));
      scale = 1 + e * 1.3;
      stretch = 1 + e ** 1.6 * 24;
      lift = -e * h * 0.22;
      turn = -e * 0.05;
      tear = clamp((e - 0.35) / 0.65);
      if (local > PEAK) tear = 1; // last beat before the cut
    }

    const draw = (img: Img, dx: number, alpha: number) => {
      ctx.save();
      ctx.globalAlpha = alpha;
      ctx.translate(w / 2 + dx, h / 2 + lift);
      ctx.rotate(turn);
      ctx.scale(scale, scale * stretch);
      ctx.drawImage(img.c, -img.w / 2, -img.h / 2, img.w, img.h);
      ctx.restore();
    };

    const white = word(s, "white", fs, fonts.sans, dpr);
    if (tear > 0.01) {
      ctx.globalCompositeOperation = "lighter";
      const spread = fs * 0.12 * tear * scale;
      draw(word(s, "green", fs, fonts.sans, dpr), -spread, 0.9 * tear);
      draw(word(s, "violet", fs, fonts.sans, dpr), spread, 0.9 * tear);
      ctx.globalCompositeOperation = "source-over";
    }
    draw(white, 0, 1 - tear * 0.75);
  };
}

export default function StreakWords() {
  return (
    <CanvasStudy
      create={create}
      background={BG}
      still={0.9}
      label="'gone', 'before', 'you', 'look' hold sharp, then swing toward you and tear into green and violet streaks; pointing holds the word at its largest."
    />
  );
}
Prompt

Build "Streak tear", a text animation. What it does: 'gone', 'before', 'you', 'look' hold sharp, then swing toward you and tear into green and violet streaks; pointing holds the word at its largest. 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 dark 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).