Baktiyar Ablaikhan

Misregistered print

Source
arlan.me/vault ↗
Author
Arlan
Licence
MIT

Loading…

HOLD ON screen-printed on hot orange with an off-register outline and foil edges; the pointer tilts the sheet and pressing pulls it into register.

Code

"use client";

import { CanvasStudy, hash, makeCanvas, rng, type Frame, type Painter } from "./stage";

export const BG = "#ff5222";
const WHITE = "#fbf2e4";
const LINES = ["HOLD", "ON"];
const SHEET = 2.8; // seconds between sheets
const CONDENSE = 0.6; // horizontal squeeze for tall capitals

export function create(): Painter {
  let key = "";
  let grain: HTMLCanvasElement | null = null;
  let ink: HTMLCanvasElement | null = null;
  let fs = 0;
  let lineH = 0;
  let tiltX = 0;
  let tiltY = 0;
  let regX = 0;
  let regY = 0;

  const placeLines = (ctx: CanvasRenderingContext2D, w: number, h: number, draw: (s: string, x: number, y: number) => void) => {
    LINES.forEach((s, i) => {
      const y = h / 2 + (i - (LINES.length - 1) / 2) * lineH;
      ctx.save();
      ctx.translate(w / 2, y);
      ctx.scale(CONDENSE, 1);
      draw(s, -ctx.measureText(s).width / 2, 0);
      ctx.restore();
    });
  };

  const build = (w: number, h: number, dpr: number, font: string) => {
    const r = rng(7);
    // paper grain
    const g = makeCanvas(w * dpr, h * dpr);
    const img = g.ctx.createImageData(g.c.width, g.c.height);
    for (let i = 0; i < img.data.length; i += 4) {
      const v = r();
      img.data[i] = v > 0.5 ? 255 : 120;
      img.data[i + 1] = v > 0.5 ? 200 : 40;
      img.data[i + 2] = v > 0.5 ? 170 : 10;
      img.data[i + 3] = Math.floor(r() * 26);
    }
    g.ctx.putImageData(img, 0, 0);
    grain = g.c;

    // white ink with streaks and pinholes
    const k = makeCanvas(w * dpr, h * dpr);
    const c = k.ctx;
    c.scale(dpr, dpr);
    c.font = `700 ${fs}px ${font}`;
    c.textBaseline = "middle";
    c.fillStyle = WHITE;
    placeLines(c, w, h, (s, x, y) => c.fillText(s, x, y));
    c.globalCompositeOperation = "source-atop";
    for (let i = 0; i < w / 2; i++) {
      c.fillStyle = `rgba(150,120,95,${0.04 + r() * 0.1})`;
      c.fillRect(r() * w, 0, 0.6 + r() * 1.4, h);
    }
    c.globalCompositeOperation = "destination-out";
    const holes = Math.floor((w * h) / 900);
    for (let i = 0; i < holes; i++) {
      c.beginPath();
      c.arc(r() * w, r() * h, 0.4 + r() * 0.9, 0, Math.PI * 2);
      c.fill();
    }
    ink = k.c;
  };

  return ({ ctx, w, h, t, dt, p, fonts, dpr }: Frame) => {
    ctx.font = `700 100px ${fonts.sans}`;
    const wide = Math.max(...LINES.map((s) => ctx.measureText(s).width)) * CONDENSE;
    fs = Math.min(h * 0.5, (w * 0.5 * 100) / wide);
    lineH = fs * 0.86;
    const k = `${w}|${h}|${dpr}|${fonts.sans}|${typeof document !== "undefined" ? document.fonts?.status : ""}`;
    if (k !== key && w >= 1 && h >= 1) {
      key = k;
      build(w, h, dpr, fonts.sans);
    }
    ctx.font = `700 ${fs}px ${fonts.sans}`;
    ctx.textBaseline = "middle";

    // pointer tilt, smoothed
    const e = Math.min(1, dt * 6);
    const nx = p.inside ? (p.x / w - 0.5) * 2 : 0;
    const ny = p.inside ? (p.y / h - 0.5) * 2 : 0;
    tiltX += (nx - tiltX) * e;
    tiltY += (ny - tiltY) * e;

    // where this sheet's outline landed
    const sheet = Math.floor(t / SHEET);
    const ang = hash(sheet, 4) * Math.PI * 2;
    const mag = fs * (0.025 + hash(sheet, 5) * 0.03);
    const targetX = p.down ? 0 : Math.cos(ang) * mag + tiltX * fs * 0.035;
    const targetY = p.down ? 0 : Math.sin(ang) * mag + tiltY * fs * 0.025;
    const snap = Math.min(1, dt * (p.down ? 10 : 14));
    regX += (targetX - regX) * snap;
    regY += (targetY - regY) * snap;

    ctx.fillStyle = BG;
    ctx.fillRect(0, 0, w, h);
    if (grain) ctx.drawImage(grain, 0, 0, w, h);

    ctx.save();
    ctx.translate(w / 2, h / 2);
    ctx.transform(1, tiltX * 0.025, -tiltY * 0.02, 1, 0, 0);
    ctx.rotate(tiltX * 0.015);
    ctx.translate(-w / 2, -h / 2);

    // holographic foil showing at the letter edges
    const hue = 200 + tiltX * 80 + tiltY * 40 + Math.sin(t * 0.7) * 20;
    const foil = ctx.createLinearGradient(w * 0.3, h * 0.1, w * 0.7, h * 0.9);
    for (let i = 0; i <= 5; i++) foil.addColorStop(i / 5, `hsl(${(hue + i * 60) % 360} 85% 72%)`);
    ctx.strokeStyle = foil;
    ctx.lineJoin = "round";
    ctx.lineWidth = fs * 0.045;
    placeLines(ctx, w, h, (s, x, y) => ctx.strokeText(s, x + regX * 0.15, y + regY * 0.15));

    // white ink
    if (ink) ctx.drawImage(ink, 0, 0, w, h);

    // off-register outline with a soft shadow
    ctx.shadowColor = "rgba(60,10,0,0.35)";
    ctx.shadowBlur = fs * 0.03;
    ctx.shadowOffsetX = -tiltX * 3;
    ctx.shadowOffsetY = 2 - tiltY * 2;
    ctx.strokeStyle = "#141010";
    ctx.lineWidth = Math.max(1, fs * 0.012);
    placeLines(ctx, w, h, (s, x, y) => ctx.strokeText(s, x + regX / CONDENSE, y + regY));
    ctx.restore();
  };
}

export default function HoldOn() {
  return (
    <CanvasStudy
      create={create}
      background={BG}
      still={1}
      cursor="pointer"
      label="HOLD ON screen-printed in warm white on orange with an off-register black outline and foil edges; the pointer tilts it and pressing pulls the outline into register."
    />
  );
}
Prompt

Build "Misregistered print", a cursor animation. What it does: HOLD ON screen-printed on hot orange with an off-register outline and foil edges; the pointer tilts the sheet and pressing pulls it into register. 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).