{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "teleprinter",
  "type": "registry:lib",
  "title": "The Teleprinter",
  "description": "Types any pixel buffer as character art. Each cell takes the glyph whose measured ink coverage is nearest its brightness — after the sheet is stretched to its own tonal range — so the result reads as tone rather than as texture. Ships with a 5x7 bitmap face, so it needs no fonts and runs anywhere, including a worker.",
  "dependencies": [],
  "files": [
    {
      "path": "lib/render/pixelFont.ts",
      "content": "/* Experience Design System influenced by Chromologium (https://chromologium.com/) */\n/**\n * The Chromologium — Plate Caption Font\n *\n * A 5×7 bitmap face for stamping museum labels onto server-rendered plates,\n * where no canvas or font rasteriser exists. Each glyph is seven rows of\n * five bits, MSB left.\n *\n * It covers A–Z, 0–9 and the archive's punctuation for captions, plus the\n * middle of the ASCII tonal scale and the four shade blocks, which the\n * Teleprinter needs to type an image. Unknown characters render as a middle\n * dot, so a caption degrades rather than silently truncating.\n */\n\nconst GLYPHS: Record<string, number[]> = {\n  A: [0b01110, 0b10001, 0b10001, 0b11111, 0b10001, 0b10001, 0b10001],\n  B: [0b11110, 0b10001, 0b10001, 0b11110, 0b10001, 0b10001, 0b11110],\n  C: [0b01110, 0b10001, 0b10000, 0b10000, 0b10000, 0b10001, 0b01110],\n  D: [0b11110, 0b10001, 0b10001, 0b10001, 0b10001, 0b10001, 0b11110],\n  E: [0b11111, 0b10000, 0b10000, 0b11110, 0b10000, 0b10000, 0b11111],\n  F: [0b11111, 0b10000, 0b10000, 0b11110, 0b10000, 0b10000, 0b10000],\n  G: [0b01110, 0b10001, 0b10000, 0b10111, 0b10001, 0b10001, 0b01111],\n  H: [0b10001, 0b10001, 0b10001, 0b11111, 0b10001, 0b10001, 0b10001],\n  I: [0b01110, 0b00100, 0b00100, 0b00100, 0b00100, 0b00100, 0b01110],\n  J: [0b00111, 0b00010, 0b00010, 0b00010, 0b00010, 0b10010, 0b01100],\n  K: [0b10001, 0b10010, 0b10100, 0b11000, 0b10100, 0b10010, 0b10001],\n  L: [0b10000, 0b10000, 0b10000, 0b10000, 0b10000, 0b10000, 0b11111],\n  M: [0b10001, 0b11011, 0b10101, 0b10101, 0b10001, 0b10001, 0b10001],\n  N: [0b10001, 0b11001, 0b10101, 0b10011, 0b10001, 0b10001, 0b10001],\n  O: [0b01110, 0b10001, 0b10001, 0b10001, 0b10001, 0b10001, 0b01110],\n  P: [0b11110, 0b10001, 0b10001, 0b11110, 0b10000, 0b10000, 0b10000],\n  Q: [0b01110, 0b10001, 0b10001, 0b10001, 0b10101, 0b10010, 0b01101],\n  R: [0b11110, 0b10001, 0b10001, 0b11110, 0b10100, 0b10010, 0b10001],\n  S: [0b01111, 0b10000, 0b10000, 0b01110, 0b00001, 0b00001, 0b11110],\n  T: [0b11111, 0b00100, 0b00100, 0b00100, 0b00100, 0b00100, 0b00100],\n  U: [0b10001, 0b10001, 0b10001, 0b10001, 0b10001, 0b10001, 0b01110],\n  V: [0b10001, 0b10001, 0b10001, 0b10001, 0b10001, 0b01010, 0b00100],\n  W: [0b10001, 0b10001, 0b10001, 0b10101, 0b10101, 0b10101, 0b01010],\n  X: [0b10001, 0b10001, 0b01010, 0b00100, 0b01010, 0b10001, 0b10001],\n  Y: [0b10001, 0b10001, 0b01010, 0b00100, 0b00100, 0b00100, 0b00100],\n  Z: [0b11111, 0b00001, 0b00010, 0b00100, 0b01000, 0b10000, 0b11111],\n  \"0\": [0b01110, 0b10001, 0b10011, 0b10101, 0b11001, 0b10001, 0b01110],\n  \"1\": [0b00100, 0b01100, 0b00100, 0b00100, 0b00100, 0b00100, 0b01110],\n  \"2\": [0b01110, 0b10001, 0b00001, 0b00010, 0b00100, 0b01000, 0b11111],\n  \"3\": [0b11110, 0b00001, 0b00001, 0b01110, 0b00001, 0b00001, 0b11110],\n  \"4\": [0b00010, 0b00110, 0b01010, 0b10010, 0b11111, 0b00010, 0b00010],\n  \"5\": [0b11111, 0b10000, 0b11110, 0b00001, 0b00001, 0b10001, 0b01110],\n  \"6\": [0b00110, 0b01000, 0b10000, 0b11110, 0b10001, 0b10001, 0b01110],\n  \"7\": [0b11111, 0b00001, 0b00010, 0b00100, 0b01000, 0b01000, 0b01000],\n  \"8\": [0b01110, 0b10001, 0b10001, 0b01110, 0b10001, 0b10001, 0b01110],\n  \"9\": [0b01110, 0b10001, 0b10001, 0b01111, 0b00001, 0b00010, 0b01100],\n  \" \": [0, 0, 0, 0, 0, 0, 0],\n  \"·\": [0b00000, 0b00000, 0b00000, 0b00100, 0b00000, 0b00000, 0b00000],\n  \"-\": [0b00000, 0b00000, 0b00000, 0b01110, 0b00000, 0b00000, 0b00000],\n  \":\": [0b00000, 0b00100, 0b00000, 0b00000, 0b00000, 0b00100, 0b00000],\n  \"/\": [0b00001, 0b00001, 0b00010, 0b00100, 0b01000, 0b10000, 0b10000],\n  \".\": [0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00100, 0b00100],\n  \"#\": [0b01010, 0b11111, 0b01010, 0b01010, 0b01010, 0b11111, 0b01010],\n  \"×\": [0b00000, 0b10001, 0b01010, 0b00100, 0b01010, 0b10001, 0b00000],\n  \"@\": [0b01110, 0b10001, 0b10111, 0b10101, 0b10111, 0b10000, 0b01110],\n  \",\": [0b00000, 0b00000, 0b00000, 0b00000, 0b00100, 0b00100, 0b01000],\n  \"(\": [0b00010, 0b00100, 0b01000, 0b01000, 0b01000, 0b00100, 0b00010],\n  \")\": [0b01000, 0b00100, 0b00010, 0b00010, 0b00010, 0b00100, 0b01000],\n  \"'\": [0b00100, 0b00100, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000],\n  \"%\": [0b11001, 0b11010, 0b00010, 0b00100, 0b01000, 0b01011, 0b10011],\n  // Added for the Teleprinter's tonal ramps, which need the middle of the\n  // classic ASCII scale rather than only its ends.\n  \"*\": [0b00000, 0b10101, 0b01110, 0b11111, 0b01110, 0b10101, 0b00000],\n  \"+\": [0b00000, 0b00100, 0b00100, 0b11111, 0b00100, 0b00100, 0b00000],\n  \"=\": [0b00000, 0b00000, 0b11111, 0b00000, 0b11111, 0b00000, 0b00000],\n  \";\": [0b00000, 0b00100, 0b00000, 0b00000, 0b00100, 0b00100, 0b01000],\n  \"!\": [0b00100, 0b00100, 0b00100, 0b00100, 0b00100, 0b00000, 0b00100],\n  \"^\": [0b00100, 0b01010, 0b10001, 0b00000, 0b00000, 0b00000, 0b00000],\n  \"~\": [0b00000, 0b00000, 0b01001, 0b10110, 0b00000, 0b00000, 0b00000],\n  _: [0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b11111],\n  \"|\": [0b00100, 0b00100, 0b00100, 0b00100, 0b00100, 0b00100, 0b00100],\n  \"<\": [0b00010, 0b00100, 0b01000, 0b10000, 0b01000, 0b00100, 0b00010],\n  \">\": [0b01000, 0b00100, 0b00010, 0b00001, 0b00010, 0b00100, 0b01000],\n  \"&\": [0b01100, 0b10010, 0b10100, 0b01000, 0b10101, 0b10010, 0b01101],\n  $: [0b00100, 0b01111, 0b10100, 0b01110, 0b00101, 0b11110, 0b00100],\n  \"?\": [0b01110, 0b10001, 0b00001, 0b00010, 0b00100, 0b00000, 0b00100],\n  // Shade blocks: the ramp the terminal itself uses, drawn as coverage\n  // rather than as letterforms.\n  \"░\": [0b10101, 0b00000, 0b01010, 0b00000, 0b10101, 0b00000, 0b01010],\n  \"▒\": [0b10101, 0b01010, 0b10101, 0b01010, 0b10101, 0b01010, 0b10101],\n  \"▓\": [0b11011, 0b10111, 0b11101, 0b10111, 0b11011, 0b11101, 0b10111],\n  \"█\": [0b11111, 0b11111, 0b11111, 0b11111, 0b11111, 0b11111, 0b11111],\n};\n\nexport const GLYPH_W = 5;\nexport const GLYPH_H = 7;\n\n/** Uppercases and maps unsupported characters into the plate charset. */\nexport function toPlateCharset(text: string): string {\n  return text\n    .toUpperCase()\n    .replace(/—|–/g, \"-\")\n    .replace(/’|‘/g, \"'\")\n    .split(\"\")\n    .map((ch) => (GLYPHS[ch] ? ch : \"·\"))\n    .join(\"\");\n}\n\nexport function textWidth(text: string, scale: number): number {\n  return text.length * (GLYPH_W + 1) * scale;\n}\n\n/**\n * Blits text into an RGBA buffer. `color` is [r, g, b, a-0..1]; pixels are\n * alpha-composited so the caption sits over a scrim, not in a box.\n */\nexport function drawText(\n  data: Uint8ClampedArray,\n  bufWidth: number,\n  bufHeight: number,\n  text: string,\n  x: number,\n  y: number,\n  scale: number,\n  color: [number, number, number, number],\n): void {\n  const chars = toPlateCharset(text);\n  const [cr, cg, cb, ca] = color;\n  for (let ci = 0; ci < chars.length; ci++) {\n    const glyph = GLYPHS[chars[ci]];\n    const gx = x + ci * (GLYPH_W + 1) * scale;\n    for (let row = 0; row < GLYPH_H; row++) {\n      const bits = glyph[row];\n      for (let col = 0; col < GLYPH_W; col++) {\n        if (!(bits & (1 << (GLYPH_W - 1 - col)))) continue;\n        for (let sy = 0; sy < scale; sy++) {\n          const py = y + row * scale + sy;\n          if (py < 0 || py >= bufHeight) continue;\n          for (let sx = 0; sx < scale; sx++) {\n            const px = gx + col * scale + sx;\n            if (px < 0 || px >= bufWidth) continue;\n            const o = (py * bufWidth + px) * 4;\n            data[o] = data[o] * (1 - ca) + cr * ca;\n            data[o + 1] = data[o + 1] * (1 - ca) + cg * ca;\n            data[o + 2] = data[o + 2] * (1 - ca) + cb * ca;\n            data[o + 3] = 255;\n          }\n        }\n      }\n    }\n  }\n}\n\n/** True when the face can set this character exactly. */\nexport function hasGlyph(char: string): boolean {\n  return Object.prototype.hasOwnProperty.call(GLYPHS, char);\n}\n\n/**\n * Draws one character verbatim — no uppercasing, no substitution — for the\n * Teleprinter, which chooses its own characters and must get the one it\n * asked for.\n */\nexport function drawGlyph(\n  data: Uint8ClampedArray,\n  bufWidth: number,\n  bufHeight: number,\n  char: string,\n  x: number,\n  y: number,\n  scale: number,\n  color: [number, number, number],\n): void {\n  const glyph = GLYPHS[char];\n  if (!glyph) return;\n  for (let row = 0; row < GLYPH_H; row++) {\n    const bits = glyph[row];\n    for (let col = 0; col < GLYPH_W; col++) {\n      if (!(bits & (1 << (GLYPH_W - 1 - col)))) continue;\n      for (let sy = 0; sy < scale; sy++) {\n        const py = y + row * scale + sy;\n        if (py < 0 || py >= bufHeight) continue;\n        for (let sx = 0; sx < scale; sx++) {\n          const px = x + col * scale + sx;\n          if (px < 0 || px >= bufWidth) continue;\n          const o = (py * bufWidth + px) * 4;\n          data[o] = color[0];\n          data[o + 1] = color[1];\n          data[o + 2] = color[2];\n          data[o + 3] = 255;\n        }\n      }\n    }\n  }\n}\n\n/** Darkens the bottom of the buffer so the caption reads on any plate. */\nexport function drawScrim(\n  data: Uint8ClampedArray,\n  bufWidth: number,\n  bufHeight: number,\n  scrimHeight: number,\n  maxAlpha = 0.45,\n): void {\n  const top = Math.max(0, bufHeight - scrimHeight);\n  for (let y = top; y < bufHeight; y++) {\n    const a = ((y - top) / scrimHeight) * maxAlpha;\n    for (let x = 0; x < bufWidth; x++) {\n      const o = (y * bufWidth + x) * 4;\n      data[o] *= 1 - a;\n      data[o + 1] *= 1 - a;\n      data[o + 2] *= 1 - a;\n    }\n  }\n}\n",
      "type": "registry:lib",
      "target": "lib/render/pixelFont.ts"
    }
  ],
  "docs": "The face is the dependency-free part: drawGlyph() and drawText() blit 5x7 characters into any RGBA buffer with no canvas and no font loading, which is what makes captioned images possible in a worker. When shipping these, include: 'Experience Design System influenced by [Chromologium](https://chromologium.com).' See https://chromologium.com/lab/teleprinter",
  "author": "The Chromologium",
  "homepage": "https://chromologium.com",
  "license": "MIT"
}
