Skip to main content

ns-ui

Divider Teletext Mosaic

A full-width section divider rendered as a real teletext / videotex ALPHAMOSAIC graphics band: each character cell is a 6-bit code across a 2-wide x 3-tall sub-cell 'sextant' grid, the actual broadcast alphamosaic addressing (distinct from this registry's 2x2 quadrant block dividers and 2x4 braille-dot components), painted row by row on a fixed cadence the way a real teletext page arrives off the broadcast signal.

Usage

registry/core/divider-teletext-mosaic/demo.tsx
"use client";

import { DividerTeletextMosaic } from "./component";

export default function DividerTeletextMosaicDemo() {
  return (
    <div className="flex min-h-screen w-full flex-col items-center justify-center gap-10 bg-background px-8 py-10">
      <div className="w-full max-w-3xl">
        <h2 className="mb-3 font-mono text-sm text-foreground">Section one</h2>
        <p className="mb-8 max-w-prose font-mono text-xs text-ns-muted">
          A page break rendered as a teletext alphamosaic band — a 6-bit,
          2x3 sextant grid per cell, painted row by row the way a real
          teletext page arrives off the broadcast signal.
        </p>
        <DividerTeletextMosaic />
      </div>
      <div className="w-full max-w-3xl">
        <h2 className="mt-8 font-mono text-sm text-foreground">Section two</h2>
      </div>
    </div>
  );
}

Component

registry/core/divider-teletext-mosaic/component.tsx
"use client";

import { useEffect, useRef } from "react";

// ---------------------------------------------------------------------------
// DividerTeletextMosaic — a full-width section divider rendered as a
// teletext / videotex ALPHAMOSAIC graphics band.
//
// Real broadcast teletext (World System Teletext / CEEFAX-era) never had a
// per-pixel framebuffer: each character cell is a 6-bit code selecting one
// of 64 patterns across a fixed 2-wide x 3-tall sub-cell grid ("sextant"
// addressing — top-left, top-right, mid-left, mid-right, bottom-left,
// bottom-right). That is a genuinely different sub-cell grid from anything
// else in this registry: block-element dividers quantize 2x2 quadrants,
// braille components address a 2x4 dot grid — this is 2x3, the real
// alphamosaic grid a teletext decoder actually drew, contiguous (no gaps
// between sub-cells, matching the "contiguous mosaic" mode broadcasters used
// for photo-like graphics, as opposed to "separated mosaic" which insets a
// gap per block).
//
// Sub-cells are rendered as filled canvas rects, not the Unicode sextant
// block range (U+1FB00-U+1FB3B, "Symbols for Legacy Computing") — this
// repo's mono stack (GeistMono / GeistMono Fallback / ui-monospace) does not
// carry that range, and shipping unverified glyph coverage risks silent
// tofu. Drawing the six sub-cells directly is also what a real teletext
// decoder chip did: it never asked a font for a glyph, it lit sub-cell
// segments off the 6-bit code.
//
// ALIVE AT REST: a real teletext page doesn't arrive all at once — rows
// paint in one at a time as they're received off the broadcast VBI signal.
// This band reproduces that: a write cursor sweeps top-to-bottom on a fixed
// cadence, re-sampling a slow generative field into a fresh 6-bit pattern
// per cell each time a row's turn comes, then holds that content until the
// next pass. A short sync pause follows a full pass before the page
// "refreshes" again, mirroring the real per-page transmission cycle.
// ---------------------------------------------------------------------------

const ROWS = 6; // character rows in the band
const ROW_INTERVAL_MS = 420; // time between successive row writes ("as received")
const PAUSE_MS = 600; // sync pause between a completed pass and the next
const FLASH_MS = 220; // dwell of the just-written row's highlight hairline
const DITHER_STRENGTH = 0.32; // ordered-dither spread around the 0.5 threshold

// 2x3 ordered-dither bias table, one entry per sub-cell index
// (subRow*2 + subCol), scattered rather than linear so the threshold band
// doesn't read as a simple diagonal — the same discipline real videotex
// picture-mosaic dithering used to fake tone out of a binary grid.
const DITHER_ORDER = [0, 3, 4, 1, 2, 5];
const DITHER_BIAS = DITHER_ORDER.map((k) => (k + 0.5) / 6 - 0.5);

function fieldValue(sx: number, sy: number, t: number) {
  // three non-commensurate traveling components summed and normalized —
  // enough structure to read as a low-res broadcast picture once
  // sextant-quantized, and slow enough that a 420ms row-write cadence
  // samples visibly different content on every pass.
  const f =
    Math.sin(sx * 0.35 + t * 0.9) +
    0.6 * Math.sin(sy * 0.6 - sx * 0.12 + t * 0.53) +
    0.4 * Math.sin((sx + sy) * 0.22 + t * 0.31);
  const v = (f + 2) / 4; // amplitude sum is +/-2 -> 0..1
  return Math.pow(Math.min(1, Math.max(0, v)), 1.2);
}

function subCellPattern(colGlobal: number, row: number, t: number) {
  // returns a 6-bit mask, bit i = sub-cell i (0=top-left ... 5=bottom-right)
  let bits = 0;
  for (let subRow = 0; subRow < 3; subRow++) {
    for (let subCol = 0; subCol < 2; subCol++) {
      const idx = subRow * 2 + subCol;
      const sx = colGlobal * 2 + subCol;
      const sy = row * 3 + subRow;
      const v = fieldValue(sx, sy, t);
      const threshold = 0.5 + DITHER_BIAS[idx] * DITHER_STRENGTH;
      if (v > threshold) bits |= 1 << idx;
    }
  }
  return bits;
}

export interface DividerTeletextMosaicProps {
  /** cell size in px; row height. Cell width is derived from monospace metrics. Default 14. */
  cellSize?: number;
  /** extra classes merged onto the rendered root element */
  className?: string;
}

export function DividerTeletextMosaic({
  cellSize = 14,
  className = "",
}: DividerTeletextMosaicProps) {
  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 fg = "currentColor";
    let cellW = cellSize * 0.6;
    const cellH = cellSize;
    let cols = 0;
    let sized = false;
    let disposed = false;

    // per-cell state: pattern (6-bit) and the timestamp (ms, component-local
    // clock) it was last written, for the write-flash hairline
    let patterns: Uint8Array[] = [];
    let writeAt: Float32Array[] = [];

    const readTokens = () => {
      fg = getComputedStyle(canvas).color;
    };

    const allocRows = () => {
      patterns = Array.from({ length: ROWS }, () => new Uint8Array(cols));
      writeAt = Array.from({ length: ROWS }, () => new Float32Array(cols).fill(-Infinity));
    };

    // globalT: seconds, drives the generative field. Never resets — content
    // keeps evolving pass over pass so no two page loads look identical.
    let globalT = 0;
    // step machine: stepIndex 0..ROWS-1 writes that row, stepIndex===ROWS is
    // the sync pause; stepAcc accumulates ms toward the current step's
    // duration, same accumulator discipline as the sibling VU divider.
    let stepIndex = 0;
    let stepAcc = 0;

    const stepDuration = (i: number) => (i < ROWS ? ROW_INTERVAL_MS : PAUSE_MS);

    const writeRow = (row: number) => {
      if (!sized) return;
      for (let c = 0; c < cols; c++) {
        patterns[row][c] = subCellPattern(c, row, globalT);
        writeAt[row][c] = globalT;
      }
    };

    // full pass at increasing globalT, used both for the initial warm start
    // (a page never opens as a blank cell grid) and after a resize (a new
    // column count leaves nothing painted otherwise)
    const warmPass = () => {
      for (let r = 0; r < ROWS; r++) {
        globalT += ROW_INTERVAL_MS / 1000;
        writeRow(r);
      }
      stepIndex = 0;
      stepAcc = 0;
    };

    const resize = () => {
      const { width } = canvas.getBoundingClientRect();
      const height = ROWS * cellH;
      if (width < 2) {
        sized = false;
        return;
      }
      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 fontFamily = getComputedStyle(canvas).fontFamily;
      const off = document.createElement("canvas").getContext("2d");
      if (off) {
        off.font = `${cellSize}px ${fontFamily}`;
        cellW = Math.max(6, off.measureText("MMMMMMMMMM").width / 10);
      }

      cols = Math.max(8, Math.floor(width / cellW));
      sized = true;
      allocRows();
      warmPass();
    };

    const draw = () => {
      if (!sized) return;
      ctx.clearRect(0, 0, cols * cellW + cellW, ROWS * cellH);
      ctx.fillStyle = fg;

      const subW = cellW / 2;
      const subH = cellH / 3;

      for (let r = 0; r < ROWS; r++) {
        const rowPatterns = patterns[r];
        const rowWriteAt = writeAt[r];
        for (let c = 0; c < cols; c++) {
          const bits = rowPatterns[c];
          if (bits === 0) continue;
          const x = c * cellW;
          const y = r * cellH;
          for (let subRow = 0; subRow < 3; subRow++) {
            for (let subCol = 0; subCol < 2; subCol++) {
              const idx = subRow * 2 + subCol;
              if (!(bits & (1 << idx))) continue;
              ctx.fillRect(
                Math.round(x + subCol * subW),
                Math.round(y + subRow * subH),
                Math.ceil(subW) + 1,
                Math.ceil(subH) + 1
              );
            }
          }
          if (!reduced) {
            const age = (globalT - rowWriteAt[c]) * 1000;
            if (age >= 0 && age < FLASH_MS) {
              const alpha = 1 - age / FLASH_MS;
              ctx.globalAlpha = alpha * 0.6;
              ctx.fillRect(Math.round(x), Math.round(y), Math.ceil(cellW) + 1, 1);
              ctx.globalAlpha = 1;
            }
          }
        }
      }
    };

    // -- loop ----------------------------------------------------------------
    let raf = 0;
    let last = 0;

    const loop = (now: number) => {
      const dtMs = last ? Math.min(250, now - last) : 1000 / 60;
      last = now;
      globalT += dtMs / 1000;
      stepAcc += dtMs;

      let guard = 0;
      while (stepAcc >= stepDuration(stepIndex) && guard < ROWS + 1) {
        stepAcc -= stepDuration(stepIndex);
        if (stepIndex < ROWS) writeRow(stepIndex);
        stepIndex = (stepIndex + 1) % (ROWS + 1);
        guard++;
      }

      draw();
      if (!document.hidden) raf = requestAnimationFrame(loop);
    };

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

    let resizeTimer: ReturnType<typeof setTimeout> | null = null;
    const onResize = () => {
      if (resizeTimer) clearTimeout(resizeTimer);
      resizeTimer = setTimeout(() => {
        resizeTimer = null;
        resize();
        draw();
      }, 150);
    };
    window.addEventListener("resize", onResize);

    const onVis = () => {
      cancelAnimationFrame(raf);
      if (!document.hidden && !reduced && sized) {
        last = 0;
        raf = requestAnimationFrame(loop);
      }
    };
    document.addEventListener("visibilitychange", onVis);

    document.fonts.ready.then(() => {
      if (disposed) return;
      readTokens();
      resize();

      if (reduced) {
        // freeze on a deliberately non-t0 frame: run the row-write sweep
        // forward through one full extra pass past the warm start, so
        // every row has been freshly rewritten at least twice and the
        // mosaic is at its most legible full-page state — never the sync
        // pause (which, while not empty, is the least eventful frame) and
        // never t0's cold warm-fill.
        for (let r = 0; r < ROWS; r++) {
          globalT += ROW_INTERVAL_MS / 1000;
          writeRow(r);
        }
        draw();
        return;
      }

      draw();
      raf = requestAnimationFrame(loop);
    });

    return () => {
      disposed = true;
      cancelAnimationFrame(raf);
      if (resizeTimer) clearTimeout(resizeTimer);
      mo.disconnect();
      window.removeEventListener("resize", onResize);
      document.removeEventListener("visibilitychange", onVis);
    };
  }, [cellSize]);

  return (
    <div
      role="separator"
      aria-orientation="horizontal"
      className={`ns-dtm w-full font-mono ${className}`}
    >
      <canvas
        ref={canvasRef}
        aria-hidden="true"
        className="block w-full text-foreground"
        style={{ height: ROWS * cellSize }}
      />
    </div>
  );
}

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

Use when Pick divider-teletext-mosaic when the surface is a section divider that should read as a genuine broadcast teletext page painting in — a 2x3 sextant sub-cell grid (6-bit alphamosaic addressing) refreshed row by row on a cadence, distinct from quadrant-block dividers (2x2) and braille-based components (2x4 dot addressing). Pick divider-petscii-vu instead when the divider should read as a live amplitude instrument rather than a paginated broadcast graphic.

Install

npx shadcn add https://design.helpmarq.com/r/divider-teletext-mosaic.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 <DividerTeletextMosaic cellSize? className?> as a full-width <canvas> band, ROWS=6 cells tall, wrapped in a <div role="separator" aria-orientation="horizontal">, a drop-in replacement for <hr>/border-top between page sections. SOURCE, NOT INVENTED: real broadcast teletext (World System Teletext / CEEFAX-era decoders) never had a per-pixel framebuffer — each character cell holds a 6-bit code selecting one of 64 patterns across a fixed 2-wide x 3-tall sub-cell grid, addressed by position (top-left, top-right, mid-left, mid-right, bottom-left, bottom-right), the real 'alphamosaic' graphics mode of the standard. THE DISCRIMINATOR — read this before anything else: this is a genuinely different sub-cell grid from anything else in the registry. Quadrant-block dividers quantize a 2x2 sub-grid (four Unicode block-element combinations); braille components address a 2x4 dot grid. This is 2x3 — six sub-cells, a different real broadcast protocol's addressing, not a reskin of either. GLYPH ROUTE, STATED: Unicode does define a full sextant block (U+1FB00-U+1FB3B, Symbols for Legacy Computing), but this repo's mono stack (GeistMono / GeistMono Fallback / ui-monospace) does not reliably carry that range, so sub-cells are drawn as filled canvas rects directly rather than risking tofu from unverified glyph coverage — which is also what a real teletext decoder chip did: it lit sub-cell segments off the 6-bit code, it never asked a font for a glyph. CONTIGUOUS MOSAIC: sub-cells are drawn edge-to-edge with no gap between them, matching the real 'contiguous mosaic' mode broadcasters used for photo-like graphics (as opposed to 'separated mosaic', which insets a gap per block) — cellW is derived once via an offscreen canvas's measureText('MMMMMMMMMM').width / 10 in the canvas's own font family (the same house cell-metrics convention every grid-based ascii component in this repo uses), re-measured after document.fonts.ready; cellH is fixed to cellSize. CONTENT: each sub-cell's bit comes from a slow generative field — three non-commensurate traveling sine components summed over global sub-cell coordinates and the component's own continuously-advancing clock, normalized 0..1 with a 1.2 gamma — thresholded at 0.5 with a small per-sub-cell bias drawn from a scattered 2x3 ordered-dither table (spread +/-0.42, scaled by a 0.32 dither strength), the same discipline real videotex picture-mosaic dithering used to fake tone gradients out of a strictly binary 6-bit grid. ALIVE AT REST, THE MECHANIC: a real teletext page doesn't arrive all at once — it paints row by row as it's received off the broadcast VBI signal. A write cursor advances one row every 420ms, re-sampling the field into a fresh 6-bit pattern for every cell in that row and holding it there until the row's next turn; after all 6 rows have been rewritten, a 600ms sync pause elapses (page fully painted, untouched, never blank) before the sweep restarts from row 0 — the component's own clock never resets, so no two passes sample the same field state and the mosaic is never identical from one page-load to the next. The just-written row gets a brief top-edge hairline in --foreground fading from 60% alpha to 0 over 220ms — the one-frame flicker of a row actively being received — skipped entirely under prefers-reduced-motion. On mount, and after every resize (a new column count leaves nothing painted otherwise), a full warm pass writes every row once before the first paint so the opening frame is never a blank grid. TOKENS: fg is read once from getComputedStyle(canvas).color (driven by the .text-foreground class on the canvas) and re-derived on a documentElement class MutationObserver so a theme flip is live; the write-flash hairline modulates that same color's alpha only, never a second color. --border is never used as a fill or stroke (it measures under 1.2:1 against --background in light theme and would be structurally invisible as the mosaic's own ink); --ns-accent never appears — this is a resting divider with no interactive surface. prefers-reduced-motion freezes after running one additional full row-write pass past the warm start (every row rewritten at least twice, field well clear of its cold-start state), then draws once with no flash hairlines and no further updates — chosen because it's the mosaic at its most legible full-page state, never the sync pause (the least eventful frame, though not empty) and never t0's cold warm-fill. The render loop pauses on document.hidden and resumes cleanly on visibilitychange. Direct-DOM rAF, zero React state on the hot path, zero dependencies. A11Y: role=separator carries the divider's semantics with no accessible name required; the canvas is aria-hidden decoration; there is no keyboard surface because there is nothing to operate. Props: cellSize (row height px, default 14; the band is fixed at 6 rows, cell width derives from monospace metrics), className.

Props

PropTypeDefaultDescription
cellSize?number14cell size in px; row height. Cell width is derived from monospace metrics. Default 14.
className?string—extra classes merged onto the rendered root element