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.
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.jsonSource
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}`}
/>
);
}
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
| Prop | Type | Default | Description |
|---|---|---|---|
| className? | string | — | extra classes merged onto the rendered canvas element |