Skip to main content

ns-ui / loud

Not Found Knockout

404 numerals rendered as literal absence: destination-out punches the glyphs through a token surface plane to reveal a deep grain void with drifting motes, defined by a bright stencil bevel rim and an inner wall-light glow so the digits read instantly in both themes, edges springing outward near the cursor.

Usage

registry/loud/not-found-knockout/demo.tsx
"use client";

import { Knockout404 } from "./component";

export default function Knockout404Demo() {
  return (
    <main className="flex min-h-screen flex-col bg-background">
      <header className="flex items-center justify-between border-b border-border px-6 py-4">
        <div className="flex items-baseline gap-4">
          <span className="font-mono text-sm font-semibold tracking-tight text-foreground">
            ns://ui
          </span>
          <span className="font-mono text-xs tracking-[0.25em] text-ns-muted">
            ns-ui / not-found-knockout
          </span>
        </div>
        <a
          href="#docs"
          className="rounded-sm px-2 py-1 font-mono text-xs text-ns-muted transition-colors hover:text-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ns-accent"
        >
          docs
        </a>
      </header>

      {/* the numerals are punched through the surface — move the cursor
          along their edges to nudge the stencil rim */}
      <Knockout404 className="min-h-0 flex-1" />

      <footer className="flex items-center justify-between border-t border-border px-6 py-3">
        <span className="font-mono text-xs text-ns-muted">
          ERR 404 / route unresolved
        </span>
        <span className="font-mono text-xs text-ns-muted">
          trace: surface plane carved / void exposed
        </span>
      </footer>
    </main>
  );
}

Component

registry/loud/not-found-knockout/component.tsx
"use client";

import { useEffect, useRef } from "react";

// ---------------------------------------------------------------------------
// Knockout404 — the 404 rendered as literal absence: every frame, a freshly
// refilled --surface plane is carved with destination-out so the numerals
// become holes revealing a deep grain void beneath (precomputed value-noise
// tile + 12 drifting motes), with an inner wall-light glow clipped to the
// holes so the cut reads as depth. A bright stencil bevel rims the cutout,
// rebuilt each frame from ~400 traced edge points whose spring-loaded
// normals bulge outward near the cursor (gaussian falloff, one small
// overshoot, forced-settle deadline). Because the frame starts from an
// explicit full clear and the carve always composites against a fresh fill,
// destination-out alpha can never accumulate or quantize — by construction.
// Canvas 2D, refs-only hot path, sleeps at cursor idle + settled springs.
// Copy and CTAs are DOM, offset-transformed from the container center.
// ---------------------------------------------------------------------------

type Vec3 = [number, number, number];

function parseColor(raw: string): Vec3 | null {
  const s = raw.trim();
  if (s.startsWith("#")) {
    const hex = s.slice(1);
    if (hex.length === 3) {
      const r = parseInt(hex.slice(0, 1) + hex.slice(0, 1), 16);
      const g = parseInt(hex.slice(1, 2) + hex.slice(1, 2), 16);
      const b = parseInt(hex.slice(2, 3) + hex.slice(2, 3), 16);
      return Number.isNaN(r + g + b) ? null : [r, g, b];
    }
    if (hex.length >= 6) {
      const r = parseInt(hex.slice(0, 2), 16);
      const g = parseInt(hex.slice(2, 4), 16);
      const b = parseInt(hex.slice(4, 6), 16);
      return Number.isNaN(r + g + b) ? null : [r, g, b];
    }
    return null;
  }
  const m = s.match(/rgba?\(\s*([\d.]+)[,\s]+([\d.]+)[,\s]+([\d.]+)/);
  return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : null;
}

function mix(a: Vec3, b: Vec3, t: number): Vec3 {
  return [
    Math.round(a[0] + (b[0] - a[0]) * t),
    Math.round(a[1] + (b[1] - a[1]) * t),
    Math.round(a[2] + (b[2] - a[2]) * t),
  ];
}

function css(c: Vec3): string {
  return `rgb(${c[0]},${c[1]},${c[2]})`;
}

// deterministic prng — grain field + mote seeding stay stable across themes
function mulberry32(a: number) {
  return () => {
    a |= 0;
    a = (a + 0x6d2b79f5) | 0;
    let t = Math.imul(a ^ (a >>> 15), 1 | a);
    t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
  };
}

// -- seamless 128px value-noise field, computed once per module ------------
const GRAIN = 128;
let noiseCache: Float32Array | null = null;
function noiseField(): Float32Array {
  if (noiseCache) return noiseCache;
  const f = new Float32Array(GRAIN * GRAIN);
  const rand = mulberry32(0x1a2b3c);
  const octave = (cells: number, weight: number) => {
    const lat = new Float32Array(cells * cells);
    for (let i = 0; i < lat.length; i++) lat[i] = rand();
    const sm = (t: number) => t * t * (3 - 2 * t);
    for (let y = 0; y < GRAIN; y++) {
      const gy = (y / GRAIN) * cells;
      const y0 = Math.floor(gy) % cells;
      const y1 = (y0 + 1) % cells;
      const ty = sm(gy - Math.floor(gy));
      for (let x = 0; x < GRAIN; x++) {
        const gx = (x / GRAIN) * cells;
        const x0 = Math.floor(gx) % cells;
        const x1 = (x0 + 1) % cells;
        const tx = sm(gx - Math.floor(gx));
        const a = lat[y0 * cells + x0] + (lat[y0 * cells + x1] - lat[y0 * cells + x0]) * tx;
        const b = lat[y1 * cells + x0] + (lat[y1 * cells + x1] - lat[y1 * cells + x0]) * tx;
        f[y * GRAIN + x] += weight * (a + (b - a) * ty);
      }
    }
  };
  octave(8, 0.5); // slow undulation
  octave(32, 0.3); // mid grain
  for (let i = 0; i < f.length; i++) f[i] += 0.2 * rand(); // fine speckle
  noiseCache = f;
  return f;
}

// -- Moore-neighbor contour tracing over a binary glyph mask ----------------
const DX = [1, 1, 0, -1, -1, -1, 0, 1];
const DY = [0, 1, 1, 1, 0, -1, -1, -1];

function traceLoops(mask: Uint8Array, gw: number, gh: number): number[][] {
  const visited = new Uint8Array(gw * gh);
  const loops: number[][] = [];
  const at = (x: number, y: number) =>
    x >= 0 && x < gw && y >= 0 && y < gh && mask[y * gw + x] === 1;
  for (let y = 0; y < gh; y++) {
    for (let x = 0; x < gw; x++) {
      const idx = y * gw + x;
      // a trace starts at an unvisited ink pixel with void to its left —
      // finds outer contours and inner counters (the 0's hole) alike
      if (mask[idx] !== 1 || visited[idx] || at(x - 1, y)) continue;
      const loop: number[] = [];
      let cx = x;
      let cy = y;
      let bdir = 4; // came from the west (the background we scanned across)
      const cap = gw * gh;
      for (let step = 0; step < cap; step++) {
        visited[cy * gw + cx] = 1;
        loop.push(cx, cy);
        let found = -1;
        for (let k = 1; k <= 8; k++) {
          const d = (bdir + k) % 8;
          if (at(cx + DX[d], cy + DY[d])) {
            found = d;
            break;
          }
        }
        if (found < 0) break; // isolated pixel
        cx += DX[found];
        cy += DY[found];
        bdir = (found + 6) % 8;
        if (cx === x && cy === y) break;
      }
      if (loop.length >= 16) loops.push(loop);
    }
  }
  return loops;
}

// resample traced loops to ~total arc-length-even points with outward
// normals; returns (bx,by,nx,ny,d,v) stride-6 floats + loop index ranges
function buildEdgePoints(
  mask: Uint8Array,
  gw: number,
  gh: number,
  scale: number,
  total: number
): { pts: Float32Array; ranges: number[] } {
  const raw = traceLoops(mask, gw, gh);
  const loops: { xs: Float32Array; ys: Float32Array; len: number; cum: Float32Array }[] = [];
  let totalLen = 0;
  for (const flat of raw) {
    const n = flat.length / 2;
    const xs = new Float32Array(n);
    const ys = new Float32Array(n);
    for (let i = 0; i < n; i++) {
      xs[i] = (flat[i * 2] + 0.5) * scale;
      ys[i] = (flat[i * 2 + 1] + 0.5) * scale;
    }
    const cum = new Float32Array(n + 1);
    for (let i = 0; i < n; i++) {
      const j = (i + 1) % n;
      cum[i + 1] = cum[i] + Math.hypot(xs[j] - xs[i], ys[j] - ys[i]);
    }
    const len = cum[n];
    if (len < 48) continue; // drop specks
    loops.push({ xs, ys, len, cum });
    totalLen += len;
  }
  if (!totalLen) return { pts: new Float32Array(0), ranges: [] };

  const out: number[] = [];
  const ranges: number[] = [];
  for (const loop of loops) {
    const n = Math.max(12, Math.round((loop.len / totalLen) * total));
    const step = loop.len / n;
    const m = loop.xs.length;
    const rx = new Float32Array(n);
    const ry = new Float32Array(n);
    let j = 0;
    for (let k = 0; k < n; k++) {
      const t = k * step;
      while (j < m - 1 && loop.cum[j + 1] < t) j++;
      const segLen = loop.cum[j + 1] - loop.cum[j];
      const u = segLen > 1e-6 ? (t - loop.cum[j]) / segLen : 0;
      const a = j;
      const b = (j + 1) % m;
      rx[k] = loop.xs[a] + (loop.xs[b] - loop.xs[a]) * u;
      ry[k] = loop.ys[a] + (loop.ys[b] - loop.ys[a]) * u;
    }
    // two closed-loop smoothing passes kill raster stairsteps
    for (let pass = 0; pass < 2; pass++) {
      const px = Float32Array.from(rx);
      const py = Float32Array.from(ry);
      for (let i = 0; i < n; i++) {
        const p = (i - 1 + n) % n;
        const q = (i + 1) % n;
        rx[i] = (px[p] + 2 * px[i] + px[q]) / 4;
        ry[i] = (py[p] + 2 * py[i] + py[q]) / 4;
      }
    }
    const base = out.length / 6;
    ranges.push(base, base + n);
    for (let i = 0; i < n; i++) {
      const p = (i - 1 + n) % n;
      const q = (i + 1) % n;
      const tx = rx[q] - rx[p];
      const ty = ry[q] - ry[p];
      const L = Math.hypot(tx, ty);
      let nx = L > 1e-6 ? ty / L : 1;
      let ny = L > 1e-6 ? -tx / L : 0;
      // orient outward — away from glyph ink, so displacement widens the hole
      const sx = Math.round((rx[i] + nx * 3) / scale - 0.5);
      const sy = Math.round((ry[i] + ny * 3) / scale - 0.5);
      if (sx >= 0 && sx < gw && sy >= 0 && sy < gh && mask[sy * gw + sx] === 1) {
        nx = -nx;
        ny = -ny;
      }
      out.push(rx[i], ry[i], nx, ny, 0, 0);
    }
  }
  return { pts: Float32Array.from(out), ranges };
}

// -- tuning -----------------------------------------------------------------
const EDGE_POINTS = 400;
const SIGMA = 120; // px — gaussian falloff of cursor influence
const INFLUENCE_R2 = (SIGMA * 3) * (SIGMA * 3);
const MAX_PUSH = 6; // px — outward displacement ceiling
const SPRING_K = 520; // s⁻² — settle < 400ms
const SPRING_C = 32.8; // ζ ≈ 0.72 → one ~3% overshoot
const SLEEP_MS = 500; // cursor idle before the loop may sleep
const INPUT_CUTOFF_MS = 600; // forced-settle deadline: influence dies here
const HARD_ZERO_MS = 1400; // absolute deadline — springs snapped to rest
const INTRO_MS = 2200; // ambient mote drift on mount before first sleep
const MOTES = 12;
const MOTE_SPEED = 4; // px/s
const MOTE_ALPHA = 0.3;
const GRAIN_ALPHA = 0.2;
const EPS_D = 0.05; // px
const EPS_V = 1; // px/s
const DT_MAX = 0.05;

export function Knockout404({
  glyph = "404",
  message = "This page drifted into the void.",
  primaryLabel = "Take me home",
  primaryHref = "/",
  secondaryLabel = "Contact support",
  secondaryHref = "#support",
  className = "",
}: {
  /** the carved numerals */
  glyph?: string;
  /** muted line floating in the punched negative space */
  message?: string;
  /** primary CTA text */
  primaryLabel?: string;
  /** primary CTA href */
  primaryHref?: string;
  /** secondary CTA text */
  secondaryLabel?: string;
  /** secondary CTA href */
  secondaryHref?: string;
  /** extra classes merged onto the rendered root element */
  className?: string;
}) {
  const rootRef = useRef<HTMLDivElement>(null);
  const canvasRef = useRef<HTMLCanvasElement>(null);
  const contentRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    const root = rootRef.current;
    const canvas = canvasRef.current;
    const content = contentRef.current;
    if (!root || !canvas || !content) return;
    const ctx = canvas.getContext("2d");
    if (!ctx) return;

    const reduced = window.matchMedia(
      "(prefers-reduced-motion: reduce)"
    ).matches;

    // offscreen layers: carved plane (dpr-sized), glyph mask (dpr-sized),
    // half-res sampler for edge tracing, 128px grain tile
    const plane = document.createElement("canvas");
    const glyphCv = document.createElement("canvas");
    const sampler = document.createElement("canvas");
    const grainCv = document.createElement("canvas");
    const glowCv = document.createElement("canvas");
    const pctx = plane.getContext("2d");
    const gctx = glyphCv.getContext("2d");
    const sctx = sampler.getContext("2d", { willReadFrequently: true });
    const grctx = grainCv.getContext("2d");
    const glctx = glowCv.getContext("2d");
    if (!pctx || !gctx || !sctx || !grctx || !glctx) return;

    const fontFamily = getComputedStyle(root).fontFamily || "sans-serif";

    // -- token inks: read at mount, re-derived live on theme flips ----------
    let surfaceCss = "#171717";
    let borderCss = "#2e2e2e";
    let mutedCss = "#8f8f8f";
    let voidCss = "#050505";
    let rimShadowCss = "#1c1c1c";
    let rimCatchCss = "#b5b5b5";
    let edgeCss = "#9a9a9a";
    let glowCss = "#b0b0b0";
    let mutedRgb: Vec3 = [143, 143, 143];
    const derive = () => {
      const cs = getComputedStyle(document.documentElement);
      const bg = parseColor(cs.getPropertyValue("--background")) ?? [10, 10, 10];
      const fg = parseColor(cs.getPropertyValue("--foreground")) ?? [237, 237, 237];
      const surface = parseColor(cs.getPropertyValue("--surface")) ?? [23, 23, 23];
      const border = parseColor(cs.getPropertyValue("--border")) ?? [46, 46, 46];
      mutedRgb = parseColor(cs.getPropertyValue("--ns-muted")) ?? [143, 143, 143];
      const lum = (0.2126 * bg[0] + 0.7152 * bg[1] + 0.0722 * bg[2]) / 255;
      // the void must read CLEARLY dimmer than the surface in BOTH themes:
      // dark crushes --background toward black; light pulls it a solid step
      // toward --foreground — the interior/surface gap is the legibility floor
      voidCss = css(lum < 0.5 ? mix(bg, [0, 0, 0], 0.6) : mix(bg, fg, 0.16));
      surfaceCss = css(surface);
      borderCss = css(border);
      mutedCss = css(mutedRgb);
      // stencil bevel, engraved in both themes: the tone mixed toward the
      // darker token is the top-left inner shadow, the lighter one the
      // bottom-right catch-light — a hole, never an emboss
      const towardFg = mix(border, fg, 0.8);
      const towardBg = mix(border, bg, 0.55);
      rimShadowCss = css(lum < 0.5 ? towardBg : towardFg);
      rimCatchCss = css(lum < 0.5 ? towardFg : towardBg);
      // crisp definition stroke + inner wall-light, both toward --foreground:
      // brighter than the surface in dark, darker than the surface in light
      edgeCss = css(mix(border, fg, 0.55));
      glowCss = css(mix(mutedRgb, fg, 0.45));
    };
    derive();

    // grain tile: recolor the cached noise field in --ns-muted per theme
    let grainPattern: CanvasPattern | null = null;
    const buildGrain = () => {
      const f = noiseField();
      grainCv.width = GRAIN;
      grainCv.height = GRAIN;
      const img = grctx.createImageData(GRAIN, GRAIN);
      const d = img.data;
      for (let i = 0; i < f.length; i++) {
        const v = Math.min(1, Math.max(0, f[i]));
        d[i * 4] = mutedRgb[0];
        d[i * 4 + 1] = mutedRgb[1];
        d[i * 4 + 2] = mutedRgb[2];
        d[i * 4 + 3] = Math.round(255 * GRAIN_ALPHA * (0.3 + 0.7 * v));
      }
      grctx.putImageData(img, 0, 0);
      grainPattern = ctx.createPattern(grainCv, "repeat");
    };
    buildGrain();

    // -- sizing: explicit style.width/height — inset does not size a canvas -
    let w = 0;
    let h = 0;
    let dpr = 1;
    let fs = 0; // glyph font size in css px
    let glyphW = 0;
    let sized = false;
    let ready = false; // fonts loaded + assets built

    const buildGlyph = () => {
      fs = Math.min(0.38 * w, 0.62 * h);
      gctx.setTransform(dpr, 0, 0, dpr, 0, 0);
      gctx.clearRect(0, 0, w, h);
      gctx.font = `600 ${fs}px ${fontFamily}`;
      gctx.textAlign = "center";
      gctx.textBaseline = "middle";
      gctx.fillStyle = "#fff"; // alpha mask — only coverage matters
      gctx.fillText(glyph, w / 2, h * 0.46);
      glyphW = gctx.measureText(glyph).width;
    };

    // -- edge points: glyph outline sampled once per resize -----------------
    let pts: Float32Array = new Float32Array(0);
    let ranges: number[] = [];
    const buildEdges = () => {
      const scale = 2; // half-res mask is plenty after smoothing
      const gw = Math.max(1, Math.ceil(w / scale));
      const gh = Math.max(1, Math.ceil(h / scale));
      sampler.width = gw;
      sampler.height = gh;
      sctx.font = `600 ${fs / scale}px ${fontFamily}`;
      sctx.textAlign = "center";
      sctx.textBaseline = "middle";
      sctx.fillStyle = "#fff";
      sctx.fillText(glyph, w / 2 / scale, (h * 0.46) / scale);
      const data = sctx.getImageData(0, 0, gw, gh).data;
      const mask = new Uint8Array(gw * gh);
      for (let i = 0; i < mask.length; i++) mask[i] = data[i * 4 + 3] > 127 ? 1 : 0;
      const res = buildEdgePoints(mask, gw, gh, scale, EDGE_POINTS);
      pts = res.pts;
      ranges = res.ranges;
    };

    // -- inner wall-light: soft glow hugging the cut edge, clipped to the
    // holes via destination-in against the glyph mask — static per
    // resize/theme, so draw() just blits it. This is what makes the digit
    // interiors read as carved depth instead of vanishing into the void.
    const buildGlow = () => {
      if (!ranges.length) return;
      glowCv.width = canvas.width;
      glowCv.height = canvas.height;
      glctx.setTransform(dpr, 0, 0, dpr, 0, 0);
      glctx.clearRect(0, 0, w, h);
      glctx.strokeStyle = glowCss;
      glctx.lineJoin = "round";
      glctx.beginPath();
      for (let r = 0; r < ranges.length; r += 2) {
        const s = ranges[r];
        const e = ranges[r + 1];
        for (let i = s; i < e; i++) {
          const o = i * 6;
          if (i === s) glctx.moveTo(pts[o], pts[o + 1]);
          else glctx.lineTo(pts[o], pts[o + 1]);
        }
        glctx.closePath();
      }
      const blur = Math.max(14, fs * 0.07);
      glctx.shadowColor = glowCss;
      glctx.lineWidth = 3;
      glctx.shadowBlur = blur;
      glctx.globalAlpha = 0.85;
      glctx.stroke();
      glctx.shadowBlur = blur * 2.4;
      glctx.globalAlpha = 0.5;
      glctx.stroke();
      glctx.shadowBlur = 0;
      glctx.globalAlpha = 1;
      glctx.globalCompositeOperation = "destination-in";
      glctx.setTransform(1, 0, 0, 1, 0, 0);
      glctx.drawImage(glyphCv, 0, 0);
      glctx.globalCompositeOperation = "source-over";
    };

    // -- motes: 12 slow sparks wrapped inside the glyph's bounding box ------
    const moteX = new Float32Array(MOTES);
    const moteY = new Float32Array(MOTES);
    const moteVX = new Float32Array(MOTES);
    const moteVY = new Float32Array(MOTES);
    const moteS = new Float32Array(MOTES);
    let bx0 = 0;
    let bx1 = 0;
    let by0 = 0;
    let by1 = 0;
    const initMotes = () => {
      const pad = 32;
      bx0 = Math.max(0, w / 2 - glyphW / 2 - pad);
      bx1 = Math.min(w, w / 2 + glyphW / 2 + pad);
      by0 = Math.max(0, h * 0.46 - fs * 0.38 - pad);
      by1 = Math.min(h, h * 0.46 + fs * 0.38 + pad);
      const rand = mulberry32(0x404);
      for (let i = 0; i < MOTES; i++) {
        moteX[i] = bx0 + rand() * (bx1 - bx0);
        moteY[i] = by0 + rand() * (by1 - by0);
        const a = rand() * Math.PI * 2;
        moteVX[i] = Math.cos(a) * MOTE_SPEED;
        moteVY[i] = Math.sin(a) * MOTE_SPEED;
        moteS[i] = i % 3 === 0 ? 2 : 1;
      }
    };

    const placeContent = () => {
      // copy/CTAs hang below the numerals: OFFSET transform from container
      // center — never absolute canvas coords
      content.style.transform = `translate(-50%, ${Math.round(fs * 0.38 + 28)}px)`;
    };

    const resize = () => {
      const rect = root.getBoundingClientRect();
      if (rect.width < 2 || rect.height < 2) {
        sized = false; // zero-size guard — loop no-ops until real
        return;
      }
      w = rect.width;
      h = rect.height;
      dpr = Math.min(2, window.devicePixelRatio || 1);
      canvas.style.width = `${w}px`; // replaced element: size explicitly
      canvas.style.height = `${h}px`;
      canvas.width = Math.max(1, Math.round(w * dpr));
      canvas.height = Math.max(1, Math.round(h * dpr));
      ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
      plane.width = canvas.width;
      plane.height = canvas.height;
      glyphCv.width = canvas.width;
      glyphCv.height = canvas.height;
      sized = true;
      if (ready) {
        buildGlyph();
        buildEdges();
        buildGlow();
        initMotes();
        placeContent();
      }
    };
    resize();

    const strokeRim = (
      ox: number,
      oy: number,
      color: string,
      alpha: number,
      width = 1
    ) => {
      if (!ranges.length) return;
      ctx.save();
      ctx.translate(ox, oy);
      ctx.strokeStyle = color;
      ctx.globalAlpha = alpha;
      ctx.lineWidth = width;
      ctx.lineJoin = "round";
      ctx.beginPath();
      for (let r = 0; r < ranges.length; r += 2) {
        const s = ranges[r];
        const e = ranges[r + 1];
        for (let i = s; i < e; i++) {
          const o = i * 6;
          const x = pts[o] + pts[o + 2] * pts[o + 4];
          const y = pts[o + 1] + pts[o + 3] * pts[o + 4];
          if (i === s) ctx.moveTo(x, y);
          else ctx.lineTo(x, y);
        }
        ctx.closePath();
      }
      ctx.stroke();
      ctx.restore();
    };

    const draw = () => {
      if (!sized || !ready) return;
      // explicit full clear — nothing survives from the previous frame
      ctx.clearRect(0, 0, w, h);
      // (a) void backdrop: dim base, tiled grain, drifting motes
      ctx.fillStyle = voidCss;
      ctx.fillRect(0, 0, w, h);
      if (grainPattern) {
        ctx.save();
        ctx.setTransform(1, 0, 0, 1, 0, 0); // device px → crisp grain
        ctx.fillStyle = grainPattern;
        ctx.fillRect(0, 0, canvas.width, canvas.height);
        ctx.restore();
      }
      ctx.globalAlpha = MOTE_ALPHA;
      ctx.fillStyle = mutedCss;
      for (let i = 0; i < MOTES; i++) {
        const s = moteS[i];
        ctx.fillRect(moteX[i] - s / 2, moteY[i] - s / 2, s, s);
      }
      ctx.globalAlpha = 1;
      // (b)+(c) surface plane refilled fresh, then carved: destination-out
      // only ever composites against this frame's fill — no accumulation
      pctx.setTransform(dpr, 0, 0, dpr, 0, 0);
      pctx.clearRect(0, 0, w, h);
      pctx.fillStyle = surfaceCss;
      pctx.fillRect(0, 0, w, h);
      pctx.globalCompositeOperation = "destination-out";
      pctx.drawImage(glyphCv, 0, 0, w, h);
      pctx.globalCompositeOperation = "source-over";
      ctx.drawImage(plane, 0, 0, w, h);
      // (c2) inner wall-light blit — the holes catch light along their edges
      if (glowCv.width > 0) ctx.drawImage(glowCv, 0, 0, w, h);
      // (d) stencil bevel rim, restroked from the displaced point list —
      // bright catch + 1.5px definition stroke keep the digits unmistakable
      strokeRim(-0.75, -0.75, rimShadowCss, 0.95);
      strokeRim(0.75, 0.75, rimCatchCss, 1);
      strokeRim(0, 0, edgeCss, 1, 1.5);
    };

    // -- hot-path state: locals only, never React state ---------------------
    let raf = 0;
    let last = 0;
    let visible = true;
    let px = 0;
    let py = 0;
    let pointerActive = false;
    let lastMove = -1e9;
    let disposed = false;
    const mountedAt = performance.now();

    const loop = (now: number) => {
      raf = 0;
      if (!visible || document.hidden || !sized || !ready) {
        last = 0;
        return; // observers/visibilitychange wake us
      }
      const dt = last === 0 ? 1 / 60 : Math.min(DT_MAX, (now - last) / 1000);
      last = now;
      const idle = now - lastMove;

      // edge springs — cursor influence dies at the forced-settle deadline,
      // and a hard deadline snaps everything to rest so sleep is guaranteed
      let settled = true;
      const n = pts.length / 6;
      if (idle > HARD_ZERO_MS) {
        for (let i = 0; i < n; i++) {
          pts[i * 6 + 4] = 0;
          pts[i * 6 + 5] = 0;
        }
      } else {
        const influence = pointerActive && idle < INPUT_CUTOFF_MS;
        for (let i = 0; i < n; i++) {
          const o = i * 6;
          let target = 0;
          if (influence) {
            const dx = px - pts[o];
            const dy = py - pts[o + 1];
            const d2 = dx * dx + dy * dy;
            if (d2 < INFLUENCE_R2) {
              target = MAX_PUSH * Math.exp(-d2 / (2 * SIGMA * SIGMA));
            }
          }
          let d = pts[o + 4];
          let v = pts[o + 5];
          const a = SPRING_K * (target - d) - SPRING_C * v;
          v += a * dt;
          d += v * dt;
          pts[o + 4] = d;
          pts[o + 5] = v;
          if (Math.abs(d - target) > EPS_D || Math.abs(v) > EPS_V || target > EPS_D) {
            settled = false;
          }
        }
      }

      // motes: 4px/s ambient drift on wrapped paths inside the glyph bbox
      const bw = bx1 - bx0;
      const bh = by1 - by0;
      if (bw > 1 && bh > 1) {
        for (let i = 0; i < MOTES; i++) {
          let x = moteX[i] + moteVX[i] * dt;
          let y = moteY[i] + moteVY[i] * dt;
          if (x < bx0) x += bw;
          else if (x > bx1) x -= bw;
          if (y < by0) y += bh;
          else if (y > by1) y -= bh;
          moteX[i] = x;
          moteY[i] = y;
        }
      }

      draw();

      // sleep: cursor idle past threshold, springs under epsilon, intro done
      if (idle > SLEEP_MS && settled && now - mountedAt > INTRO_MS) {
        last = 0;
        return; // motes freeze with the loop — redraw resumes on wake
      }
      raf = requestAnimationFrame(loop);
    };

    const wake = () => {
      if (reduced || raf || disposed) return;
      last = 0;
      raf = requestAnimationFrame(loop);
    };

    const onMove = (e: PointerEvent) => {
      const rect = root.getBoundingClientRect();
      px = e.clientX - rect.left;
      py = e.clientY - rect.top;
      pointerActive = true;
      lastMove = performance.now();
      wake();
    };
    const onLeave = () => {
      pointerActive = false;
      wake(); // springs animate home, then the loop sleeps
    };

    // -- observers ----------------------------------------------------------
    const ro = new ResizeObserver(() => {
      resize();
      draw();
      wake();
    });
    ro.observe(root);
    const io = new IntersectionObserver((entries) => {
      visible = entries[0]?.isIntersecting ?? true;
      if (visible) wake();
    });
    io.observe(root);
    // live theme re-derive: regenerate inks, grain tile AND glyph canvas
    const mo = new MutationObserver(() => {
      derive();
      buildGrain();
      if (sized && ready) {
        buildGlyph();
        buildGlow();
      }
      draw();
      wake();
    });
    mo.observe(document.documentElement, {
      attributes: true,
      attributeFilter: ["class"],
    });
    const onVis = () => {
      if (!document.hidden) wake();
    };
    document.addEventListener("visibilitychange", onVis);

    if (!reduced) {
      root.addEventListener("pointermove", onMove);
      root.addEventListener("pointerdown", onMove);
      root.addEventListener("pointerleave", onLeave);
    }

    // rasterize only once Geist is loaded — a fallback-font carve is wrong.
    // reduced motion: this single draw IS the static carved frame.
    document.fonts.ready.then(() => {
      if (disposed) return;
      ready = true;
      if (sized) {
        buildGlyph();
        buildEdges();
        buildGlow();
        initMotes();
        placeContent();
      }
      draw();
      wake();
    });

    return () => {
      disposed = true;
      cancelAnimationFrame(raf);
      raf = 0;
      ro.disconnect();
      io.disconnect();
      mo.disconnect();
      document.removeEventListener("visibilitychange", onVis);
      root.removeEventListener("pointermove", onMove);
      root.removeEventListener("pointerdown", onMove);
      root.removeEventListener("pointerleave", onLeave);
    };
  }, [glyph]);

  return (
    <div
      ref={rootRef}
      className={`relative isolate min-h-[480px] w-full overflow-hidden bg-background font-sans ${className}`}
    >
      <canvas
        ref={canvasRef}
        aria-hidden
        className="pointer-events-none absolute left-0 top-0"
      />
      <h1 className="sr-only">{glyph} — page not found</h1>
      {/* copy + CTAs float in the punched negative space — DOM, offset from center */}
      <div
        ref={contentRef}
        className="absolute left-1/2 top-[46%] flex w-full max-w-md flex-col items-center gap-6 px-6 text-center"
        style={{ transform: "translate(-50%, calc(13vw + 28px))" }}
      >
        <p className="font-mono text-sm text-ns-muted">{message}</p>
        <div className="flex flex-wrap items-center justify-center gap-3">
          <a
            href={primaryHref}
            className="inline-flex items-center justify-center rounded-sm bg-ns-accent px-4 py-2 text-sm font-medium text-white transition-colors duration-150 hover:bg-ns-accent-hover focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ns-accent"
          >
            {primaryLabel}
          </a>
          <a
            href={secondaryHref}
            className="inline-flex items-center justify-center rounded-sm border border-border px-4 py-2 text-sm text-ns-muted transition-colors duration-150 hover:border-foreground/40 hover:text-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ns-accent"
          >
            {secondaryLabel}
          </a>
        </div>
      </div>
    </div>
  );
}

The usage example imports ./component because it sits next to the component file in this repo. Installed, that file lands at components/ui/not-found-knockout.tsx, so point the import there.

Use when a 404 page where the numerals should read as literal absence punched through the surface into a grain void, their edges springing outward near the cursor; pick not-found-postmark instead for a 404 that reads as a returned letter rather than a hole, or empty-state-dashed when the region is empty inside a working page rather than a missing route.

Install

npx shadcn add https://design.helpmarq.com/r/not-found-knockout.json

Ask AI

Point an assistant at this component's docs (llms-full.txt) with one click.

Claude, ChatGPT, Grok, and Perplexity open with the prompt already in. Gemini copies it to your clipboard first. Paste it in once the chat opens.

Build spec

Build a full 404 page where the numerals are literal absence: a single full-viewport Canvas 2D layer redrawn with an EXPLICIT FULL CLEAR every frame in this order — (a) void backdrop: a 128px seamless value-noise grain tile precomputed once per theme (recolored in --ns-muted, alpha 0.2, tiled at device resolution) over a deep token base (--background crushed 60% toward black in dark, background mixed 16% toward foreground in light so the void reads CLEARLY dimmer than the surface in BOTH themes — the interior/surface gap is the legibility floor), plus 12 drifting motes (1-2px squares, --ns-muted alpha 0.3, 4px/s on wrapped paths inside the glyph bounding box); (b) surface plane: an offscreen plane canvas refilled solid --surface; (c) carve: destination-out drawImage of an offscreen glyph canvas ('404', Geist Sans 600, font-size min(38vw, 62vh), rasterized after document.fonts.ready, centered at 46% height) so the numerals become holes showing layer (a) — because the plane is refilled fresh each frame, destination-out alpha can never accumulate or quantize; (c2) inner wall-light: a static offscreen glow layer — the undisplaced outline stroked in muted-mixed-45%-toward-foreground with layered shadowBlur (max(14px, 7% of font-size) and 2.4x that) then clipped to the holes via destination-in against the glyph mask, rebuilt per resize/theme — so the cut walls catch light and the digit interiors read as carved depth; (d) rim: the glyph outline restroked at 1.5px in border-mixed-55%-toward-foreground as the crisp definition stroke, plus two ±0.75px offset strokes in border-mixed-80%-toward-foreground (catch-light) and border-mixed-55%-toward-background (inner shadow) tones for an engraved stencil bevel bright enough that 404 reads at a glance. EDGE INTERACTION: threshold a half-res alpha mask of the glyph, Moore-neighbor trace its contours (outer loops and the 0's counter), arc-length resample to ~400 points with two smoothing passes and outward normals (oriented away from ink via mask probe); cursor proximity with gaussian falloff sigma 120px displaces points outward up to 6px along their normals; each point returns on a spring (k=520 s^-2, zeta~0.72, one ~3% overshoot, settle < 400ms) with a forced-settle deadline: cursor influence dies at 600ms idle and a hard deadline at 1400ms snaps displacement and velocity to zero; the displaced rim is restroked from the point list every frame. INTERACTION MODEL: pointermove/pointerdown nudge edges; DOM copy ('This page drifted into the void.') and two CTAs — 'Take me home' (accent primary, rounded-sm, hover accent-hover) and 'Contact support' (ghost bordered, hover border-foreground/40) — float below the punched area, positioned by an OFFSET transform from the container center (never absolute canvas coords), both keyboard-focusable with token-relative accent focus rings; an sr-only h1 keeps semantics. HOUSEKEEPING: all inks parsed from getComputedStyle tokens at mount with a MutationObserver on documentElement class that re-derives colors AND regenerates the grain tile + glyph canvas per theme; canvas sized via explicit style.width/style.height plus a dpr-clamped (2) backing store and setTransform; rAF is direct-DOM with zero React state on the hot path and sleeps when cursor idle > 500ms and all edge springs are under epsilon (motes freeze on sleep, redraw resumes on wake) after a 2.2s ambient intro; IntersectionObserver and document.hidden pause the loop; zero-size containers guarded; every listener, observer, and rAF torn down. REDUCED MOTION: one static carved frame, motes frozen, no edge displacement, no pointer listeners. Demo composes the complete page: top strip with mono logotype and one nav link, carved 404 centered, muted copy line, the two CTAs, and a hairline footer with mono status text 'ERR 404 / route unresolved'.

Props

PropTypeDefaultDescription
glyph?string"404"the carved numerals
message?string"This page drifted into the void."muted line floating in the punched negative space
primaryLabel?string"Take me home"primary CTA text
primaryHref?string"/"primary CTA href
secondaryLabel?string"Contact support"secondary CTA text
secondaryHref?string"#support"secondary CTA href
className?string—extra classes merged onto the rendered root element