Skip to main content

ns-ui / loud

Magnetron Racetrack Sweep

A full-bleed background built on sputter target erosion: a magnetron's plasma racetrack sweeps back and forth across a target face, slowly grooving it wherever the ring has lingered, rendered as a persistent radial erosion field under a live, non-accumulated glow ring.

Usage

registry/loud/magnetron-racetrack-sweep/demo.tsx
"use client";

import { MagnetronRacetrackSweep } from "./component";

// The target already carries a faint groove from before mount (a short
// warmup), so the field never opens on a suspiciously pristine disc. Type
// sits on a token scrim since the disc spans most of the value range.
export default function MagnetronRacetrackSweepDemo() {
  return (
    <main className="h-screen w-full overflow-hidden bg-background">
      <MagnetronRacetrackSweep>
        <div className="pointer-events-none flex h-full w-full flex-col items-center justify-end gap-4 px-6 pb-14 text-center sm:pb-20">
          <div className="flex flex-col items-center gap-3 rounded-lg bg-background/70 px-7 py-5 backdrop-blur-md">
            <p className="font-mono text-[11px] uppercase tracking-[0.28em] text-ns-muted">
              ns-ui / magnetron-racetrack-sweep
            </p>
            <h1 className="text-2xl font-medium tracking-tight text-foreground sm:text-3xl">
              Worn by the sweep
            </h1>
            <p className="max-w-sm text-sm text-ns-muted">
              The racetrack sweeps in and out over 22s, slowly grooving the target wherever it lingers.
            </p>
          </div>
          <a
            href="#docs"
            className="pointer-events-auto mt-1 inline-flex w-fit items-center justify-center rounded-sm bg-ns-accent px-5 py-2.5 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"
          >
            Read the docs
          </a>
        </div>
      </MagnetronRacetrackSweep>
    </main>
  );
}

Component

registry/loud/magnetron-racetrack-sweep/component.tsx
"use client";

import { useEffect, useId, useRef } from "react";

// ---------------------------------------------------------------------------
// MagnetronRacetrackSweep — a full-bleed background eroded by sputter target
// wear: a magnetron's magnetic field confines plasma into an annular
// "racetrack" band on a planar sputter target's face. Production cathodes
// sweep that racetrack's radius back and forth (rotating/oscillating magnet
// assembly) so erosion doesn't dig one fixed groove and waste the rest of the
// target.
//
// EROSION IS RADIALLY SYMMETRIC, SO THE ACCUMULATION BUFFER IS 1D. The
// racetrack is a full annulus at every instant, never a spot — the whole
// erosion history is therefore a function of radius alone, never angle. That
// lets the "persistent, never-cleared accumulation buffer" (same non-clearing
// technique as rime-creep and edm-crater-field, monotonic here rather than
// decaying — real erosion never heals) live in a 256-texel 1D LUMINANCE
// texture instead of a full 2D grid: cheap to stamp, cheap to sample, and it
// is the mechanic's own physical symmetry that licenses the simplification,
// not a performance shortcut taken against the spec.
//
// The composite pass derives a surface normal from the 1D depth field's
// RADIAL gradient only (there is no tangential slope, by construction) and
// projects it back into 2D along the local radial direction — the target
// reads as a real machined groove, brighter/darker as it catches the
// achromatic studio's light bands, not a flat painted ring.
//
// The live glow ring is a SEPARATE, non-accumulated quantity: it is redrawn
// every frame from the sweep's current phase and added to luminance only
// (never colour — real sputtering plasma glows violet/pink, discarded on
// purpose per the monochrome constraint). A pointer brightens the ring
// locally by angle, never by hue, and never touches the sweep clock itself.
// ---------------------------------------------------------------------------

export interface MagnetronRacetrackSweepProps {
  /** Freezes the surface on a composed steady-state frame without unmounting. */
  paused?: boolean;
  /** Rendered in the DOM over the field — eyebrow, headline, CTA. */
  children?: React.ReactNode;
  className?: string;
  style?: React.CSSProperties;
}

const VERT_SRC = `
attribute vec2 a_pos;
void main() { gl_Position = vec4(a_pos, 0.0, 1.0); }
`;

const FRAG_SRC = `
precision highp float;

uniform vec2 u_size;       // css px
uniform float u_dpr;
uniform vec2 u_center;     // css px, target centre
uniform float u_targetR;   // css px, target radius
uniform sampler2D u_erosion; // 1D depth field, N x 1
uniform float u_erosionN;
uniform float u_ringR;         // current live sweep radius, normalized 0..1
uniform float u_ringHalfWidth; // normalized
uniform float u_pointerAngle;
uniform float u_pointerActive;
uniform float u_time;
uniform vec3 u_c0;
uniform vec3 u_c1;
uniform vec3 u_c2;
uniform vec3 u_c3;
uniform vec3 u_c4;
uniform vec3 u_bg;
uniform float u_bias;
uniform float u_contrast;

float sampleDepth(float r) {
  return texture2D(u_erosion, vec2(clamp(r, 0.0, 1.0), 0.5)).r;
}

float strip(float el, float at, float width) {
  float d = (el - at) / width;
  return exp(-d * d);
}

// simplified achromatic studio — a machined-metal read needs fewer bands
// than a mirror pool, same family as edm-crater-field's env().
float env(vec3 r) {
  float el = r.y;
  float az = atan(r.x, r.z);
  float L = 0.42;
  L += 0.20 * smoothstep(0.05, 0.62, el);
  L -= 0.18 * smoothstep(0.0, -0.55, el);
  L += 0.28 * strip(el, 0.14, 0.06);
  L += 0.14 * strip(el, 0.46, 0.09);
  L += 0.08 * exp(-pow(sin(az * 1.0 + u_time * 0.03) / 0.42, 2.0));
  return L;
}

vec3 ramp(float x) {
  vec3 c = mix(u_c0, u_c1, smoothstep(0.0, 0.30, x));
  c = mix(c, u_c2, smoothstep(0.28, 0.58, x));
  c = mix(c, u_c3, smoothstep(0.55, 0.85, x));
  c = mix(c, u_c4, smoothstep(0.82, 1.0, x));
  return c;
}

void main() {
  vec2 p = vec2(gl_FragCoord.x, u_size.y * u_dpr - gl_FragCoord.y) / u_dpr - u_center;
  float dist = length(p);
  float r = dist / u_targetR;

  float edge = smoothstep(1.02, 0.985, r);
  if (edge <= 0.0) {
    gl_FragColor = vec4(u_bg, 1.0);
    return;
  }

  vec2 rDir = dist > 0.0005 ? p / dist : vec2(0.0, 1.0);
  float texel = 1.0 / u_erosionN;
  float d0 = sampleDepth(r);
  float dPlus = sampleDepth(r + texel);
  float dMinus = sampleDepth(r - texel);
  float slope = (dPlus - dMinus) / (2.0 * texel);
  float relief = 2.2;
  vec3 n = normalize(vec3(-slope * relief * rDir, 1.0));
  vec2 vp = p / u_targetR;
  vec3 v = normalize(vec3(vp.x * 0.5, -vp.y * 0.5, 1.0));
  vec3 refl = reflect(-v, n);

  float L = env(refl);
  vec3 l1 = normalize(vec3(-0.28, 0.7, 0.6));
  float s1 = pow(max(dot(refl, l1), 0.0), 90.0);
  L += s1 * 0.6;

  // material removed reads as LOWER relative luminance than the fresh face,
  // in both themes — direction never flips, only bias/contrast do.
  float depthVis = clamp(d0 / 1.4, 0.0, 1.0);
  L -= depthVis * 0.34;

  // live glow ring — additive luminance only, never accumulated, never hue.
  float ringMask = 1.0 - smoothstep(u_ringHalfWidth * 0.7, u_ringHalfWidth * 1.15, abs(r - u_ringR));
  L += ringMask * 0.5;

  if (u_pointerActive > 0.5) {
    float ang = atan(p.x, -p.y);
    float diff = ang - u_pointerAngle;
    diff = mod(diff + 3.14159265, 6.2831853) - 3.14159265;
    float angularBoost = exp(-pow(diff / 0.35, 2.0));
    L += ringMask * angularBoost * 0.35;
  }

  float fres = pow(1.0 - max(dot(n, v), 0.0), 5.0);
  L += fres * 0.08;

  float Lc = clamp((L - 0.5) * u_contrast + 0.5 + u_bias, 0.0, 1.0);
  vec3 col = mix(u_bg, ramp(Lc), edge);
  gl_FragColor = vec4(col, 1.0);
}
`;

type RGB = [number, number, number];

function parseHex(raw: string): RGB | null {
  const m = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(raw.trim());
  if (!m) return null;
  let h = m[1];
  if (h.length === 3) h = h.split("").map((c) => c + c).join("");
  const n = parseInt(h, 16);
  return [((n >> 16) & 255) / 255, ((n >> 8) & 255) / 255, (n & 255) / 255];
}

function mixRGB(a: RGB, b: RGB, t: number): RGB {
  return [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t, a[2] + (b[2] - a[2]) * t];
}

function luminance([r, g, b]: RGB): number {
  return 0.2126 * r + 0.7152 * g + 0.0722 * b;
}

function compile(gl: WebGLRenderingContext, type: number, src: string): WebGLShader {
  const s = gl.createShader(type)!;
  gl.shaderSource(s, src);
  gl.compileShader(s);
  if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) {
    const info = gl.getShaderInfoLog(s);
    gl.deleteShader(s);
    throw new Error(`magnetron-racetrack-sweep: shader compile failed: ${info ?? ""}`);
  }
  return s;
}

// Minimal full-bleed fragment-shader host, same shape as weld-pool /
// edm-crater-field's GLSurface: one program, one fullscreen triangle pair,
// lazily-resolved uniform locations.
class GLSurface {
  gl: WebGLRenderingContext | null = null;
  private program: WebGLProgram | null = null;
  private vs: WebGLShader | null = null;
  private fs: WebGLShader | null = null;
  private buffer: WebGLBuffer | null = null;
  private locs = new Map<string, WebGLUniformLocation | null>();

  constructor(private canvas: HTMLCanvasElement, private frag: string) {}

  init(isRestore = false): boolean {
    const gl = this.canvas.getContext("webgl", {
      alpha: false,
      antialias: false,
      premultipliedAlpha: false,
      powerPreference: "high-performance",
    }) as WebGLRenderingContext | null;
    if (!gl) return false;
    this.gl = gl;
    try {
      this.vs = compile(gl, gl.VERTEX_SHADER, VERT_SRC);
      this.fs = compile(gl, gl.FRAGMENT_SHADER, this.frag);
      const program = gl.createProgram();
      if (!program) {
        this.abandon(isRestore);
        return false;
      }
      this.program = program;
      gl.attachShader(program, this.vs);
      gl.attachShader(program, this.fs);
      gl.linkProgram(program);
      if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
        this.abandon(isRestore);
        return false;
      }
    } catch {
      this.abandon(isRestore);
      return false;
    }
    gl.useProgram(this.program);
    this.buffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer);
    gl.bufferData(
      gl.ARRAY_BUFFER,
      new Float32Array([-1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, 1]),
      gl.STATIC_DRAW
    );
    const loc = gl.getAttribLocation(this.program!, "a_pos");
    gl.enableVertexAttribArray(loc);
    gl.vertexAttribPointer(loc, 2, gl.FLOAT, false, 0, 0);
    this.locs.clear();
    return true;
  }

  // A compile/link failure above happens on a context getContext() already
  // handed over, and the caller's `if (!surface.init()) return` never
  // registers the effect cleanup that hands a context back — so the release
  // belongs here, while this.gl still reaches it. The condition is the
  // inverse of that cleanup's `!canvas.isConnected`: the canvas is still
  // mounted on this path, and the context that must NOT be released is one
  // the browser has just restored, since onRestored re-enters init() and a
  // release there would feed its own loss/restore/fail cycle.
  private abandon(isRestore: boolean) {
    const lose = isRestore ? null : this.gl?.getExtension("WEBGL_lose_context");
    this.destroy();
    lose?.loseContext();
  }

  private loc(name: string): WebGLUniformLocation | null {
    if (!this.locs.has(name)) {
      this.locs.set(name, this.gl!.getUniformLocation(this.program!, name));
    }
    return this.locs.get(name) ?? null;
  }

  f(name: string, x: number) {
    this.gl?.uniform1f(this.loc(name), x);
  }
  i(name: string, x: number) {
    this.gl?.uniform1i(this.loc(name), x);
  }
  v2(name: string, x: number, y: number) {
    this.gl?.uniform2f(this.loc(name), x, y);
  }
  v3(name: string, c: RGB) {
    this.gl?.uniform3f(this.loc(name), c[0], c[1], c[2]);
  }

  draw(pixelW: number, pixelH: number) {
    const gl = this.gl;
    if (!gl || !this.program) return;
    gl.viewport(0, 0, pixelW, pixelH);
    gl.drawArrays(gl.TRIANGLES, 0, 6);
  }

  destroy() {
    const gl = this.gl;
    if (!gl) return;
    if (this.buffer) gl.deleteBuffer(this.buffer);
    if (this.program) gl.deleteProgram(this.program);
    if (this.vs) gl.deleteShader(this.vs);
    if (this.fs) gl.deleteShader(this.fs);
    this.buffer = null;
    this.program = null;
    this.vs = null;
    this.fs = null;
    this.locs.clear();
    this.gl = null;
  }
}

// ---------------------------------------------------------------------------
// Real numbers (docs/specs/r10/magnetron-racetrack-sweep.md):
//  - racetrack radius oscillates between 0.3 and 0.7 of target radius
//  - oscillation period: 22s
//  - racetrack ring width: 0.08 * targetRadius
//  - erosion accumulation rate: 0.002 depth-units/s wherever the ring
//    currently sits — monotonic, never decays
// ---------------------------------------------------------------------------
const RING_MID = 0.5; // (0.3 + 0.7) / 2
const RING_AMPLITUDE = 0.2; // (0.7 - 0.3) / 2
const SWEEP_PERIOD_S = 22;
const RING_WIDTH = 0.08;
const EROSION_RATE = 0.002; // depth-units/s within the band
const EROSION_BUCKETS = 256;
const WARMUP_STEP = 0.25;
const LIVE_WARMUP_S = 40; // "existing erosion groove faint" at t0
const STATIC_WARMUP_S = 480; // deep enough for the dual-band asymmetry to read at a glance
const STATIC_PHASE_S = SWEEP_PERIOD_S * 0.5; // sweep phase 0.5 -> "racetrack-mid"

function ringRAt(tSeconds: number): number {
  return RING_MID + RING_AMPLITUDE * Math.sin((2 * Math.PI * tSeconds) / SWEEP_PERIOD_S);
}

export function MagnetronRacetrackSweep({
  paused = false,
  children,
  className = "",
  style,
}: MagnetronRacetrackSweepProps) {
  const wrapRef = useRef<HTMLDivElement | null>(null);
  const canvasRef = useRef<HTMLCanvasElement | null>(null);
  const uid = useId();
  const pausedRef = useRef(paused);
  pausedRef.current = paused;

  useEffect(() => {
    const wrap = wrapRef.current;
    const canvas = canvasRef.current;
    if (!wrap || !canvas) return;

    const surface = new GLSurface(canvas, FRAG_SRC);
    let raf = 0;
    let running = false;
    let staticMode = false;
    let cssW = 0;
    let cssH = 0;
    let dpr = 1;
    let disposed = false;
    let lastMs = performance.now();

    // persistent, never-cleared radial erosion buffer — 1D because the
    // mechanic itself is radially symmetric (see header comment).
    const depthBuf = new Float32Array(EROSION_BUCKETS);
    const uploadBuf = new Uint8Array(EROSION_BUCKETS);
    let erosionTex: WebGLTexture | null = null;

    let simTime = 0;

    let hoverAngle = 0;
    let pointerActive = 0; // eased 0..1
    let pointerTarget = 0;

    const SCALES = [1, 0.7];
    const BUDGET_OVER = 24;
    let scaleIdx = 0;
    let frameEma = 16.7;
    let overMs = 0;
    let underMs = 0;
    let upWindow = 8000;

    let c0: RGB = [0.03, 0.03, 0.03];
    let c1: RGB = [0.18, 0.18, 0.18];
    let c2: RGB = [0.56, 0.56, 0.56];
    let c3: RGB = [0.93, 0.93, 0.93];
    let c4: RGB = [1, 1, 1];
    let bg: RGB = [0, 0, 0];
    let bias = 0;
    let contrast = 1.15;

    const readColors = () => {
      const cs = getComputedStyle(document.documentElement);
      const bgTok = parseHex(cs.getPropertyValue("--background")) ?? [1, 1, 1];
      const fg = parseHex(cs.getPropertyValue("--foreground")) ?? [0.09, 0.09, 0.09];
      const muted = parseHex(cs.getPropertyValue("--ns-muted")) ?? [0.55, 0.55, 0.55];
      const border = parseHex(cs.getPropertyValue("--border")) ?? [0.18, 0.18, 0.18];
      const black: RGB = [0, 0, 0];
      const white: RGB = [1, 1, 1];
      bg = bgTok;
      if (luminance(bgTok) < 0.5) {
        c0 = mixRGB(bgTok, black, 0.5);
        c1 = mixRGB(border, bgTok, 0.15);
        c2 = muted;
        c3 = fg;
        c4 = mixRGB(fg, white, 0.82);
        bias = -0.06;
        contrast = 1.18;
      } else {
        c0 = mixRGB(fg, black, 0.32);
        c1 = mixRGB(fg, muted, 0.55);
        c2 = mixRGB(muted, bgTok, 0.6);
        c3 = mixRGB(bgTok, muted, 0.16);
        c4 = bgTok;
        bias = 0.03;
        contrast = 1.2;
      }
    };
    readColors();

    // advance the erosion field by dt at absolute sim time t — pure function
    // of the sweep clock, no randomness, so warmup and the live loop use the
    // exact same function and a reduced-motion replay is deterministic.
    const simulate = (dt: number, t: number) => {
      const ringR = ringRAt(t);
      const half = RING_WIDTH / 2;
      const lo = ringR - half;
      const hi = ringR + half;
      const loIdx = Math.max(0, Math.floor(lo * EROSION_BUCKETS));
      const hiIdx = Math.min(EROSION_BUCKETS - 1, Math.ceil(hi * EROSION_BUCKETS));
      for (let i = loIdx; i <= hiIdx; i++) {
        depthBuf[i] += EROSION_RATE * dt;
      }
    };

    const warmup = (seconds: number) => {
      depthBuf.fill(0);
      let t = 0;
      while (t < seconds) {
        const step = Math.min(WARMUP_STEP, seconds - t);
        simulate(step, t);
        t += step;
      }
    };

    const uploadTexture = () => {
      const gl = surface.gl;
      if (!gl) return;
      for (let i = 0; i < EROSION_BUCKETS; i++) {
        const v = Math.max(0, Math.min(1.4, depthBuf[i]!)) / 1.4;
        uploadBuf[i] = Math.round(v * 255);
      }
      if (!erosionTex) {
        erosionTex = gl.createTexture();
        gl.bindTexture(gl.TEXTURE_2D, erosionTex);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
      } else {
        gl.bindTexture(gl.TEXTURE_2D, erosionTex);
      }
      gl.texImage2D(
        gl.TEXTURE_2D,
        0,
        gl.LUMINANCE,
        EROSION_BUCKETS,
        1,
        0,
        gl.LUMINANCE,
        gl.UNSIGNED_BYTE,
        uploadBuf
      );
      gl.activeTexture(gl.TEXTURE0);
      gl.bindTexture(gl.TEXTURE_2D, erosionTex);
    };

    const draw = (t: number) => {
      if (!surface.gl || cssW <= 0 || cssH <= 0) return;
      uploadTexture();
      const targetR = Math.min(cssW, cssH) * 0.42;
      surface.v2("u_size", cssW, cssH);
      surface.f("u_dpr", dpr);
      surface.v2("u_center", cssW / 2, cssH / 2);
      surface.f("u_targetR", targetR);
      surface.f("u_erosionN", EROSION_BUCKETS);
      surface.f("u_ringR", ringRAt(t));
      surface.f("u_ringHalfWidth", RING_WIDTH / 2);
      surface.f("u_pointerAngle", hoverAngle);
      surface.f("u_pointerActive", pointerActive);
      surface.f("u_time", t);
      surface.v3("u_c0", c0);
      surface.v3("u_c1", c1);
      surface.v3("u_c2", c2);
      surface.v3("u_c3", c3);
      surface.v3("u_c4", c4);
      surface.v3("u_bg", bg);
      surface.f("u_bias", bias);
      surface.f("u_contrast", contrast);
      surface.i("u_erosion", 0);
      surface.draw(canvas.width, canvas.height);
    };

    const loop = (nowMs: number) => {
      const rawMs = nowMs - lastMs;
      const dt = Math.min(0.05, Math.max(0, rawMs / 1000));
      lastMs = nowMs;
      simTime += dt;

      pointerActive += (pointerTarget - pointerActive) * (1 - Math.exp(-dt / 0.25));

      simulate(dt, simTime);
      draw(simTime);

      const clamped = Math.min(50, rawMs);
      frameEma += (clamped - frameEma) * (1 - Math.exp(-clamped / 120));
      if (frameEma > BUDGET_OVER) {
        overMs += clamped;
        underMs = 0;
      } else {
        underMs += clamped;
        overMs = 0;
      }
      const down = overMs > 900 && scaleIdx < SCALES.length - 1;
      const up = underMs > upWindow && scaleIdx > 0;
      if (down || up) {
        scaleIdx += down ? 1 : -1;
        if (down) upWindow = Math.min(64000, upWindow * 2);
        overMs = 0;
        underMs = 0;
        frameEma = 16.7;
        applyBacking();
      }
      raf = requestAnimationFrame(loop);
    };
    const wake = () => {
      if (running || disposed) return;
      running = true;
      lastMs = performance.now();
      raf = requestAnimationFrame(loop);
    };
    const sleep = () => {
      cancelAnimationFrame(raf);
      running = false;
    };

    const applyBacking = () => {
      if (cssW < 2 || cssH < 2) return;
      dpr = Math.min(window.devicePixelRatio || 1, 1.5) * SCALES[scaleIdx]!;
      const pw = Math.round(cssW * dpr);
      const ph = Math.round(cssH * dpr);
      if (canvas.width !== pw || canvas.height !== ph) {
        canvas.width = pw;
        canvas.height = ph;
      }
      canvas.style.width = `${cssW}px`;
      canvas.style.height = `${cssH}px`;
      draw(staticMode ? STATIC_PHASE_S : simTime);
    };

    const resize = () => {
      const rect = wrap.getBoundingClientRect();
      if (rect.width < 2 || rect.height < 2) return;
      cssW = rect.width;
      cssH = rect.height;
      scaleIdx = 0;
      overMs = 0;
      underMs = 0;
      upWindow = 8000;
      frameEma = 16.7;
      applyBacking();
    };

    let rectLeft = 0;
    let rectTop = 0;
    let rectDirty = true;
    const syncRect = () => {
      if (!rectDirty) return;
      const rect = wrap.getBoundingClientRect();
      rectLeft = rect.left;
      rectTop = rect.top;
      rectDirty = false;
    };
    const markRectDirty = () => {
      rectDirty = true;
    };

    // pointer only sets WHERE the ring reads brighter (angle), never the
    // sweep clock — the sweep is a fixed physical cycle, not pointer-driven.
    const onPointerMove = (e: PointerEvent) => {
      syncRect();
      const cx = rectLeft + cssW / 2;
      const cy = rectTop + cssH / 2;
      const dx = e.clientX - cx;
      const dy = e.clientY - cy;
      hoverAngle = Math.atan2(dx, -dy);
      pointerTarget = 1;
      if (staticMode) draw(STATIC_PHASE_S);
    };
    const onPointerLeave = () => {
      pointerTarget = 0;
    };

    if (!surface.init()) return; // no WebGL: children still render over the page bg
    const ro = new ResizeObserver(resize);
    ro.observe(wrap);
    resize();

    wrap.addEventListener("pointermove", onPointerMove);
    wrap.addEventListener("pointerleave", onPointerLeave);
    window.addEventListener("scroll", markRectDirty, { passive: true, capture: true });
    window.addEventListener("resize", markRectDirty, { passive: true });

    const mq = window.matchMedia("(prefers-reduced-motion: reduce)");
    let reduced = mq.matches;
    const applyMode = () => {
      if (reduced || pausedRef.current) {
        staticMode = true;
        sleep();
        warmup(STATIC_WARMUP_S);
        draw(STATIC_PHASE_S);
      } else {
        staticMode = false;
        simTime = LIVE_WARMUP_S;
        warmup(LIVE_WARMUP_S);
        wake();
      }
    };
    const onMq = () => {
      reduced = mq.matches;
      applyMode();
    };
    mq.addEventListener("change", onMq);

    let onScreen = true;
    const io = new IntersectionObserver(
      (entries) => {
        onScreen = entries.some((en) => en.isIntersecting);
        if (!onScreen) sleep();
        else if (!staticMode && !document.hidden) wake();
      },
      { threshold: 0 }
    );
    io.observe(wrap);

    const onVis = () => {
      if (document.hidden) sleep();
      else if (!staticMode && onScreen) wake();
    };
    document.addEventListener("visibilitychange", onVis);
    applyMode();

    let lastPolledPaused = pausedRef.current;
    let poll = 0;
    const tick = () => {
      if (pausedRef.current !== lastPolledPaused) {
        lastPolledPaused = pausedRef.current;
        applyMode();
      }
      poll = window.setTimeout(tick, 140);
    };
    tick();

    const themeObserver = new MutationObserver(() => {
      readColors();
      if (staticMode) draw(STATIC_PHASE_S);
    });
    themeObserver.observe(document.documentElement, {
      attributes: true,
      attributeFilter: ["class"],
    });

    const onLost = (e: Event) => {
      e.preventDefault();
      sleep();
    };
    const onRestored = () => {
      erosionTex = null;
      if (surface.init(true)) {
        resize();
        applyMode();
      }
    };
    canvas.addEventListener("webglcontextlost", onLost);
    canvas.addEventListener("webglcontextrestored", onRestored);

    return () => {
      disposed = true;
      ro.disconnect();
      io.disconnect();
      mq.removeEventListener("change", onMq);
      document.removeEventListener("visibilitychange", onVis);
      themeObserver.disconnect();
      canvas.removeEventListener("webglcontextlost", onLost);
      canvas.removeEventListener("webglcontextrestored", onRestored);
      wrap.removeEventListener("pointermove", onPointerMove);
      wrap.removeEventListener("pointerleave", onPointerLeave);
      window.removeEventListener("scroll", markRectDirty, { capture: true } as EventListenerOptions);
      window.removeEventListener("resize", markRectDirty);
      window.clearTimeout(poll);
      sleep();
      if (erosionTex && surface.gl) surface.gl.deleteTexture(erosionTex);
      erosionTex = null;
      // destroy() frees the GL objects but not the context holding them, and a
      // detached canvas keeps its context until GC gets round to it —
      // browsers cap live contexts per renderer process and evict the oldest
      // first, which kills tracks still on screen. Hand it back explicitly,
      // but only on a real unmount: this cleanup also runs when a dep changes,
      // and a canvas returns its one lost context to every later getContext(),
      // so releasing too eagerly would blank the rebuilt track.
      const lose = surface.gl?.getExtension("WEBGL_lose_context");
      surface.destroy();
      if (!canvas.isConnected) lose?.loseContext();
    };
  }, []);

  return (
    <div
      ref={wrapRef}
      data-magnetron-racetrack-sweep={uid}
      className={`relative isolate h-full w-full touch-none overflow-hidden bg-background ${className}`}
      style={style}
    >
      <canvas ref={canvasRef} aria-hidden="true" className="absolute inset-0 block h-full w-full" />
      {children ? <div className="relative z-[1] h-full w-full">{children}</div> : null}
    </div>
  );
}

MagnetronRacetrackSweep.displayName = "MagnetronRacetrackSweep";

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

Use when Pick magnetron-racetrack-sweep for a full-bleed background whose motion is a slow, physically justified sweep leaving a legible structural trace behind it — the erosion groove gives the field a persistent "memory" a generic looping gradient never accumulates. Pick edm-crater-field instead when the surface should read as a statistical steady-state (constant birth/decay, never accumulating toward one shape) rather than a slowly deepening groove; weld-pool when the background needs to carry rasterized headline type inside the same height field.

Install

npx shadcn add https://design.helpmarq.com/r/magnetron-racetrack-sweep.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 <MagnetronRacetrackSweep paused? children? className?> as a full-bleed WebGL background rendering sputter-target erosion under an oscillating magnetron racetrack. THE EROSION FIELD IS 1D: because the racetrack is a full annulus at every instant, its erosion history is a function of radius alone, never angle — the persistent, never-cleared accumulation buffer (same non-clearing technique as rime-creep/edm-crater-field, but MONOTONIC here, never decaying, matching real physical erosion) is therefore a 256-texel 1D LUMINANCE texture (EROSION_BUCKETS=256), not a full 2D grid. ringRAt(t) = 0.5 + 0.2*sin(2*pi*t/22) is the sweep's live normalized radius (oscillating between 0.3 and 0.7 of target radius, 22s period); every simulate(dt,t) call adds EROSION_RATE=0.002 depth-units/s to every bucket whose normalized radius falls within [ringRAt(t) - 0.04, ringRAt(t) + 0.04] (RING_WIDTH=0.08 total). Because the sinusoid dwells longest at its own turning points (0.3 and 0.7) and passes through the middle twice per cycle at full speed, the two turning-point bands erode measurably deeper than the middle band with ZERO extra logic — the dual-band asymmetry is the sweep's own kinematics, not a scripted bias. GROOVE SHADING: the composite fragment shader derives a surface normal from the 1D depth field's RADIAL gradient only (central difference at r +/- 1/256, no tangential term exists by construction) and projects it back into 2D along the local radial direction (rDir = p/length(p)), so the groove reads as a real machined cut catching a small achromatic multi-band studio (env(), same family as edm-crater-field's, fewer bands than a mirror pool needs) rather than a flat painted ring. Material removed subtracts directly from luminance (depthVis*0.34) — lower relative luminance than the fresh face, same direction in both themes, only bias/contrast differ per theme (dark: bias -0.06, contrast 1.18; light: bias 0.03, contrast 1.2). LIVE GLOW RING: a second, non-accumulated quantity — ringMask, a smoothstep band around the CURRENT ringRAt(t), added straight to luminance (+0.5) every frame, explicitly never colour (real sputtering plasma glows violet/pink; that hue is discarded on purpose per the monochrome constraint) and never composited into the persistent erosion texture. POINTER: pointermove computes the angle from the target centre to the pointer (never its radius) and eases pointerActive 0->1 over a 250ms exponential; the shader adds an angular gaussian boost (falloff 0.35 rad) on top of ringMask, brightening the glow ring locally by luminance only, never accent, and it must never alter ringRAt/simTime — the sweep is a fixed physical cycle, not pointer-driven. HOST: mirrors edm-crater-field's scaffolding — a GLSurface host class (fullscreen triangle pair, lazily-resolved uniform locations), a two-tier adaptive DPR ladder (SCALES=[1,0.7], steps down after a sustained >24ms EMA for 900ms, back up after a much longer clean stretch that doubles each time it trips), DPR capped at 1.5, a ResizeObserver on the wrapper (never window.resize), IntersectionObserver(threshold 0) + visibilitychange both gating the single rAF loop, webglcontextlost/restored rebuilding cleanly, and a paused prop polled every 140ms (props can't be read fresh inside a long-lived rAF closure without a ref). Tokens (--background, --foreground, --ns-muted, --border) are read via getComputedStyle(document.documentElement) before any paint and re-read on a MutationObserver watching documentElement's class. prefers-reduced-motion (or paused) sets a STATIC_WARMUP_S=480s deterministic warmup (the same simulate() function stepped in fixed 0.25s chunks from an all-zero buffer, no randomness anywhere in this component) then draws exactly one frame at STATIC_PHASE_S = 22*0.5 (sweep phase 0.5, ring at mid-radius, dual-band asymmetry already visible), named "racetrack-mid" — never a blank disc. The live (non-reduced) mode instead warms up only LIVE_WARMUP_S=40s before its first paint, so the groove starts faint rather than either pristine or already deep.

Props

PropTypeDefaultDescription
paused?booleanfalseFreezes the surface on a composed steady-state frame without unmounting.
children?React.ReactNode—Rendered in the DOM over the field — eyebrow, headline, CTA.
className?string——
style?React.CSSProperties——