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