Skip to main content

ns-ui

Delta Frame Macroblock

A quiet full-width band that visualizes a video codec's own P-frame change-detection decision: a grid of macroblocks sits mostly static, and every 220ms tick a small, capped subset is flagged changed with a brief outline flash and a slightly shifted tone while every other block is explicitly left untouched, exactly the sparse per-block change map a real codec decodes.

Usage

registry/core/delta-frame-macroblock/demo.tsx
"use client";

import { DeltaFrameMacroblock } from "./component";

export default function DeltaFrameMacroblockDemo() {
  return (
    <div className="flex min-h-screen flex-col items-center justify-center gap-0 bg-background px-6">
      <div className="w-full max-w-3xl border-b border-border py-16">
        <p className="font-mono text-xs tracking-widest text-ns-muted">CHANGELOG</p>
        <h2 className="mt-3 text-2xl font-semibold text-foreground">
          Every release, only what moved
        </h2>
      </div>
      <div className="h-40 w-full max-w-3xl">
        <DeltaFrameMacroblock />
      </div>
      <div className="w-full max-w-3xl border-t border-border py-16">
        <p className="max-w-lg text-sm leading-relaxed text-ns-muted">
          ns-ui / delta-frame-macroblock — a quiet band sitting between sections. A
          small, capped subset of macroblocks is flagged as changed every 220ms
          tick; every other block is explicitly left untouched, same as a
          decoder skipping an unflagged P-frame block.
        </p>
      </div>
    </div>
  );
}

Component

registry/core/delta-frame-macroblock/component.tsx
"use client";

import { useEffect, useRef } from "react";

// ---------------------------------------------------------------------------
// DeltaFrameMacroblock — a quiet full-width band that visualizes a video
// codec's own P-frame change-detection decision, not the pixels it redraws.
//
// After an MPEG/H.26x-class codec sends one full (I-)frame, every frame after
// that only encodes the macroblocks that actually changed; each block carries
// a "redraw" or "skip, unchanged" flag, and a decoder leaves every skipped
// block's pixels exactly as they were in the frame buffer. Scrub a real
// codec's per-block change map and you see a sparse, shifting set of flagged
// blocks against a mostly static field — the compression artifact visible as
// blockiness in low-bitrate video, made deliberate here instead of a defect.
//
// A uniform grid of macroblocks is sized off the container's SMALLER
// dimension: block = clamp(round(minDim / 16), 12, 24) px, then cols/rows are
// rounded so the grid fills the container exactly (a 320px band at 16px
// blocks holds a 20-block-wide row). Each block carries a fixed per-block
// seed tone (drawn once, never reseeded) plus a bounded, reflecting
// toneOffset that only moves the tick a block is flagged — that is the
// "slightly shifted tone" of a motion-compensated redraw, and reflecting the
// offset at its amplitude ceiling keeps the drift visible forever without
// ever saturating into salt-and-pepper noise (an unbounded random walk would
// eventually finish, which fails the always-different-at-rest rule).
//
// A fixed 220ms tick — the visualized P-frame interval, deliberately slower
// than a real ~30fps codec tick per the round 9 decoupling rule, so each
// delta event stays an individually followable discrete flash rather than a
// strobe — flags a SMALL, capped number of blocks as "changed" (2-4 blocks
// regardless of grid size, not the raw 6-10% the source ratio would suggest:
// at typical card-scale grids of 150-400 blocks, 6-10% is 10-30 simultaneous
// flags, and against a 340ms outline lifetime that overlaps into a
// continuous shimmer rather than discrete countable events — the spec's own
// kill criteria pre-authorizes making the timing more aggressive rather than
// shipping that). A flagged block's outline appears instantly, holds at full
// --foreground-weight stroke for 140ms, then fades over the next 200ms back
// to unoutlined — 340ms total, comfortably inside the 220ms-spaced ticks so
// at most two flash generations ever overlap.
//
// The whole board is redrawn every animation frame from typed-array state
// (tone + toneOffset + per-block last-flagged time) rather than painted into
// a persistent buffer, so a theme flip or resize repaints every block
// identically instead of leaving stale pixels from the old theme behind.
// ---------------------------------------------------------------------------

const MIN_BLOCK = 12;
const MAX_BLOCK = 24;
const TICK_MS = 220; // visualized P-frame interval
const HOLD_MS = 140; // outline holds at full strength
const FADE_MS = 200; // then fades back to unoutlined
const FLASH_MS = HOLD_MS + FADE_MS;
const FLAG_FRACTION = 0.08; // codec's typical sparse P-frame update ratio
const FLAG_MIN = 2;
const FLAG_MAX = 4; // capped for legibility — see header note
const TONE_AMP = 0.05; // reflecting bound on toneOffset
const TONE_STEP = 0.018; // per-flag nudge, before reflection
const BASE_ALPHA = 0.05; // narrow luminance band floor
const BASE_NOISE = 0.045; // per-block fixed-seed spread, dark theme
const LIGHT_NOISE_MULT = 1.6; // widened on a light background — see below
const WARMUP_TICKS = 16; // pre-drifts the field so mount isn't a bare I-frame
const RM_WARMUP_TICKS = 24;

function mulberry32(seed: number) {
  let a = seed >>> 0;
  return () => {
    a = (a + 0x6d2b79f5) >>> 0;
    let t = a;
    t = Math.imul(t ^ (t >>> 15), t | 1);
    t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
  };
}

function reflect(v: number, amp: number): number {
  let x = v;
  if (x > amp) x = 2 * amp - x;
  else if (x < -amp) x = -2 * amp - x;
  if (x > amp) x = amp;
  else if (x < -amp) x = -amp;
  return x;
}

/** parses a getComputedStyle "rgb(r, g, b)" / "rgba(r, g, b, a)" string */
function parseRgb(s: string): [number, number, number] {
  const m = s.match(/rgba?\(\s*([\d.]+)[,\s]+([\d.]+)[,\s]+([\d.]+)/);
  if (!m) return [128, 128, 128];
  return [Number(m[1]), Number(m[2]), Number(m[3])];
}

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

export interface DeltaFrameMacroblockProps {
  /** extra classes merged onto the rendered canvas element */
  className?: string;
}

export function DeltaFrameMacroblock({ className = "" }: DeltaFrameMacroblockProps) {
  const canvasRef = useRef<HTMLCanvasElement>(null);

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

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

    let disposed = false;
    let tokensRead = false;
    let fg: [number, number, number] = [128, 128, 128];
    let bg: [number, number, number] = [255, 255, 255];
    let lightMult = 1;

    let width = 0;
    let height = 0;
    let sized = false;
    let cols = 0;
    let rows = 0;
    let blockW = 0;
    let blockH = 0;
    let count = 0;

    let baseTone = new Float32Array(0);
    let toneOffset = new Float32Array(0);
    let flagTime = new Float32Array(0);
    let rng = mulberry32(0x0dfa11e);

    let simNow = 0;
    let acc = 0;
    let last = 0;
    let raf = 0;
    let visible = true;

    const readTokens = () => {
      const cs = getComputedStyle(document.documentElement);
      fg = parseRgb(cs.getPropertyValue("--foreground") || getComputedStyle(canvas).color);
      bg = parseRgb(cs.getPropertyValue("--background") || "rgb(255,255,255)");
      // light theme washes out subtle per-block variance faster than dark —
      // widen the fixed-seed noise amplitude, still purely a multiplier on
      // token-derived alpha, never a new colour.
      lightMult = relativeLuminance(bg) > 0.5 ? LIGHT_NOISE_MULT : 1;
      tokensRead = true;
    };

    const layout = () => {
      const rect = canvas.getBoundingClientRect();
      if (rect.width < 2 || rect.height < 2) {
        sized = false;
        return;
      }
      width = rect.width;
      height = rect.height;
      const dpr = Math.min(window.devicePixelRatio || 1, 2);
      canvas.width = Math.max(1, Math.round(width * dpr));
      canvas.height = Math.max(1, Math.round(height * dpr));
      ctx.setTransform(dpr, 0, 0, dpr, 0, 0);

      const minDim = Math.min(width, height);
      const block = Math.min(MAX_BLOCK, Math.max(MIN_BLOCK, Math.round(minDim / 16)));
      cols = Math.max(1, Math.round(width / block));
      rows = Math.max(1, Math.round(height / block));
      blockW = width / cols;
      blockH = height / rows;
      count = cols * rows;

      rng = mulberry32(0x0dfa11e);
      baseTone = new Float32Array(count);
      toneOffset = new Float32Array(count);
      flagTime = new Float32Array(count).fill(-Infinity);
      for (let i = 0; i < count; i++) baseTone[i] = rng() * 2 - 1;

      simNow = 0;
      acc = 0;
      sized = true;
    };

    /** one tick: flags a small capped subset of blocks as "changed" */
    const tick = (t: number) => {
      const n = Math.min(count, Math.max(FLAG_MIN, Math.min(FLAG_MAX, Math.round(count * FLAG_FRACTION))));
      const chosen = new Set<number>();
      let guard = 0;
      while (chosen.size < n && guard < n * 20) {
        guard++;
        chosen.add(Math.floor(rng() * count));
      }
      for (const i of chosen) {
        flagTime[i] = t;
        const step = (rng() - 0.5) * 2 * TONE_STEP;
        toneOffset[i] = reflect((toneOffset[i] ?? 0) + step, TONE_AMP);
      }
    };

    const draw = (t: number) => {
      if (!sized) return;
      if (!tokensRead) readTokens();

      ctx.clearRect(0, 0, width, height);
      ctx.fillStyle = `rgb(${bg[0]}, ${bg[1]}, ${bg[2]})`;
      ctx.fillRect(0, 0, width, height);

      const fgStyle = `rgb(${fg[0]}, ${fg[1]}, ${fg[2]})`;
      ctx.fillStyle = fgStyle;
      for (let r = 0; r < rows; r++) {
        for (let c = 0; c < cols; c++) {
          const i = r * cols + c;
          const noise = (baseTone[i] ?? 0) * BASE_NOISE * lightMult;
          const drift = toneOffset[i] ?? 0;
          let alpha = BASE_ALPHA + noise + drift;
          if (alpha < 0.015) alpha = 0.015;
          else if (alpha > 0.22) alpha = 0.22;
          ctx.globalAlpha = alpha;
          ctx.fillRect(c * blockW, r * blockH, blockW, blockH);
        }
      }

      ctx.strokeStyle = fgStyle;
      ctx.lineWidth = 1;
      for (let r = 0; r < rows; r++) {
        for (let c = 0; c < cols; c++) {
          const i = r * cols + c;
          const age = t - (flagTime[i] ?? -Infinity);
          if (age < 0 || age > FLASH_MS) continue;
          const strength = age <= HOLD_MS ? 1 : 1 - (age - HOLD_MS) / FADE_MS;
          ctx.globalAlpha = strength;
          ctx.strokeRect(c * blockW + 0.5, r * blockH + 0.5, blockW - 1, blockH - 1);
        }
      }
      ctx.globalAlpha = 1;
    };

    const warm = (ticks: number) => {
      let t = 0;
      for (let k = 0; k < ticks; k++) {
        t += TICK_MS;
        tick(t);
      }
      simNow = t;
    };

    const loop = (now: number) => {
      raf = 0;
      if (!visible || !sized) return;
      if (last === 0) last = now;
      const dt = Math.min(100, now - last);
      last = now;
      simNow += dt;
      acc += dt;
      while (acc >= TICK_MS) {
        acc -= TICK_MS;
        tick(simNow - acc);
      }
      draw(simNow);
      raf = requestAnimationFrame(loop);
    };

    const start = () => {
      layout();
      if (!sized) return;
      readTokens();
      if (reduced) {
        warm(RM_WARMUP_TICKS);
        // freeze on the freshly-flagged frame itself — 0ms into the 140ms
        // hold, before any fade softens which blocks are flagged.
        draw(simNow);
        return;
      }
      warm(WARMUP_TICKS);
      last = 0;
      if (!raf) raf = requestAnimationFrame(loop);
    };

    let resizeTimer = 0;
    const onResize = () => {
      window.clearTimeout(resizeTimer);
      resizeTimer = window.setTimeout(() => {
        if (disposed) return;
        cancelAnimationFrame(raf);
        raf = 0;
        start();
      }, 120);
    };
    const ro = new ResizeObserver(onResize);
    ro.observe(canvas);

    const mo = new MutationObserver(() => {
      tokensRead = false;
      if (reduced) draw(simNow);
    });
    mo.observe(document.documentElement, { attributes: true, attributeFilter: ["class"] });

    const io = new IntersectionObserver((entries) => {
      visible = entries[0]?.isIntersecting ?? true;
      if (visible && sized && !reduced && !raf) {
        last = 0;
        raf = requestAnimationFrame(loop);
      }
    });
    io.observe(canvas);

    document.fonts.ready.then(() => {
      if (!disposed) onResize();
    });

    start();

    return () => {
      disposed = true;
      cancelAnimationFrame(raf);
      raf = 0;
      window.clearTimeout(resizeTimer);
      ro.disconnect();
      mo.disconnect();
      io.disconnect();
    };
  }, []);

  return (
    <canvas
      ref={canvasRef}
      aria-hidden
      className={`block h-full w-full text-foreground ${className}`}
    />
  );
}

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

Use when Pick delta-frame-macroblock for a quiet band/divider whose subject IS a change-detection decision — a mostly static block grid where a small, individually-countable subset of blocks flashes as 'changed' each tick and everything else is provably left alone, sourced from real MPEG/H.26x P-frame skip-flag encoding. background-ascii-dither is the better pick when the field should read as a continuous glyph-mapped luminance surface driven by an image or flowing noise rather than discrete per-cell change events; background-truchet-weave when the surface should read as one continuous interlaced line weave rather than a static cell grid with sparse redraws.

Install

npx shadcn add https://design.helpmarq.com/r/delta-frame-macroblock.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 <DeltaFrameMacroblock className?> as a full-bleed <canvas> (block h-full w-full) drawn with Canvas 2D on a direct-DOM rAF loop, no React state on the hot path. GEOMETRY: block = clamp(round(minDim / 16), 12, 24) px where minDim = min(container width, height); cols = round(width / block), rows = round(height / block), then blockW = width / cols and blockH = height / rows so the grid fills the container exactly with no partial edge blocks (a 320px band at 16px blocks holds a 20-block-wide row). PER-BLOCK STATE, held in three Float32Arrays sized cols*rows and mutated in place: baseTone (a fixed per-block seed value in [-1, 1] from a mulberry32 PRNG re-seeded on every layout, drawn once and never reseeded during the session), toneOffset (starts at 0, only ever moved the tick a block is flagged, bounded to +-0.05 with reflection at the bound rather than clamping — an unbounded random walk would eventually saturate the narrow luminance band into salt-and-pepper noise, which both breaks light theme and eventually stops changing, failing the always-different-at-rest rule), and flagTime (the sim-clock timestamp, in ms, a block was last flagged; -Infinity until first flagged). TICK, every 220ms (accumulator-driven off real elapsed time so speed never depends on frame rate, not a raw per-frame animation of a real codec tick rate per the round 9 decoupling rule): pick a small capped subset of distinct block indices — n = clamp(round(count * 0.08), 2, 4), i.e. the spec's 6-10% sparse-update ratio capped to an absolute 2-4 blocks regardless of grid size, because at typical card-scale grids (150-400 blocks) the raw percentage produces 10-30 simultaneous flags which, against a 340ms outline lifetime, overlaps into continuous shimmer rather than discrete countable events (this is a deliberate, spec-authorized departure toward more aggressive/legible timing, not a defect) — set flagTime[i] = current sim time and nudge toneOffset[i] by a random +-0.018 step (reflected) for each chosen block. RENDER, every animation frame, full redraw from the typed arrays (never a persistent buffer, so a theme flip or resize repaints every block identically instead of leaving stale-theme pixels behind): clear to --background, then for every block fill with --foreground at alpha = clamp(0.05 base + baseTone*0.045*lightMult + toneOffset, 0.015, 0.22) — a narrow, low-contrast luminance band, value-only, no hue, where lightMult is 1.6 in light theme and 1 in dark (derived from the resolved --background token's relative luminance, not from string-matching a theme class, so it stays token-driven with zero colour literals). Then, for every block whose (simNow - flagTime) is within 0-340ms, stroke its rect in --foreground at 1px: full alpha (1.0) for the first 140ms (the hold), then linearly fading to 0 over the following 200ms (the fade) — 340ms total, chosen so at most roughly one and a half tick-generations of outlines ever overlap on screen at once, keeping each flash an individually followable event rather than a continuous glow. TOKENS: --foreground and --background are read via getComputedStyle(document.documentElement).getPropertyValue and re-derived on a MutationObserver watching documentElement's class attribute; a tokensRead guard means draw() never paints before the first successful read, checked explicitly on the rAF start, the ResizeObserver-triggered relayout, and the IntersectionObserver resume path. Canvas is DPR-clamped to 2 and resized via ResizeObserver (120ms debounce) which fully relays out the grid, reseeds baseTone, and resets toneOffset/flagTime — a resize is treated as a fresh mount, not a stretch of stale state. WARM START (both paths run a synchronous tick loop before any paint, so mount never shows a bare freshly-reset I-frame): normal path runs 16 synchronous 220ms ticks before starting the rAF loop, so the very first painted frame already shows a natural scatter of blocks at different points in their fade lifecycle (t0 = a scatter of freshly-flagged blocks mid-fade from a recent tick, static base grid otherwise unchanged since mount) plus pre-drifted toneOffset variance from earlier warm-up ticks. prefers-reduced-motion: reduce runs 24 synchronous ticks, draws exactly one frame at that exact final sim time (0ms into the last tick's 140ms hold, full-strength outlines, before any fade softens which blocks are flagged versus static — the most legible single frame of the mechanic), and binds no rAF and no further ticks. Pauses on IntersectionObserver exit, resumes with a reset last-frame timestamp on re-entry. No interaction: the flag pattern is never linked to pointer position — the mechanic is the codec's own change-detection decision, not a hover-responsive grid, and a pointer-linked flag pattern would also risk mixing accent into what must stay a value-only highlight. Props: className only. Zero dependencies, canvas + CSS token reads only, every colour --foreground/--background, no --ns-accent, no --border used as a fill or stroke.

Props

PropTypeDefaultDescription
className?string—extra classes merged onto the rendered canvas element