{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "dither-image",
  "title": "Dither image",
  "description": "Ordered Bayer dithering quantized to the theme inks, or to two to four colors of your own.",
  "dependencies": [
    "cn"
  ],
  "registryDependencies": [
    "@ui95/theme",
    "@ui95/dither-inks"
  ],
  "files": [
    {
      "path": "components/ui/dither-image.tsx",
      "content": "\"use client\"\n\nimport { cn } from \"cn\"\nimport * as React from \"react\"\n\nimport type { BayerMatrixSize } from \"@/lib/dither/bayer\"\nimport { ditherOnCpu, type DitherRequest } from \"@/lib/dither/ditherOnCpu\"\nimport { INK_TOKENS, readInks } from \"@/lib/dither/inks\"\nimport { useInkVersion } from \"@/lib/dither/useInkVersion\"\n\ntype DitherImageColors =\n  | readonly [string, string]\n  | readonly [string, string, string]\n  | readonly [string, string, string, string]\n\ntype DitherImageProps = Omit<\n  React.ComponentProps<\"canvas\">,\n  \"width\" | \"height\" | \"children\"\n> & {\n  src: string\n  alt: string\n  width: number\n  height: number\n  pixelSize?: number\n  matrixSize?: BayerMatrixSize\n  contrast?: number\n  isAnimated?: boolean\n  colors?: DitherImageColors\n}\n\nconst COLOR_TOKENS = [\"--dither-ink-0\", \"--dither-ink-1\", \"--dither-ink-2\", \"--dither-ink-3\"] as const\n\nfunction toCssColor(color: string): string {\n  return color.startsWith(\"--\") ? `var(${color})` : color\n}\n\nfunction readColorStyle(colors: DitherImageColors | undefined): React.CSSProperties {\n  if (!colors) {\n    return {}\n  }\n  return Object.fromEntries(colors.map((color, index) => [COLOR_TOKENS[index], toCssColor(color)]))\n}\n\nfunction readColorTokens(colorCount: number): readonly [string, string, ...string[]] {\n  if (colorCount < 2) {\n    return INK_TOKENS\n  }\n  const [first, second, ...rest] = COLOR_TOKENS.slice(0, colorCount)\n  return [first, second, ...rest]\n}\n\nfunction useLoadedImage(src: string): HTMLImageElement | null {\n  const [image, setImage] = React.useState<HTMLImageElement | null>(null)\n\n  React.useEffect(() => {\n    let isCurrent = true\n    const loading = new Image()\n    loading.src = src\n    loading\n      .decode()\n      .then(() => {\n        if (isCurrent) {\n          setImage(loading)\n        }\n      })\n      .catch(() => {\n        if (isCurrent) {\n          setImage(null)\n        }\n      })\n    return () => {\n      isCurrent = false\n    }\n  }, [src])\n\n  return image\n}\n\nconst RESEED_FPS = 12\n\nfunction useReseed(isAnimated: boolean): number {\n  const [seed, setSeed] = React.useState(0)\n\n  React.useEffect(() => {\n    if (!isAnimated) {\n      return\n    }\n    const interval = window.setInterval(\n      () => setSeed((current) => current + 1),\n      1000 / RESEED_FPS\n    )\n    return () => window.clearInterval(interval)\n  }, [isAnimated])\n\n  return seed\n}\n\nfunction DitherImage({\n  src,\n  alt,\n  width,\n  height,\n  pixelSize = 2,\n  matrixSize = 4,\n  contrast = 1.2,\n  isAnimated = false,\n  colors,\n  className,\n  style,\n  ...props\n}: DitherImageProps) {\n  const canvasRef = React.useRef<HTMLCanvasElement>(null)\n  const image = useLoadedImage(src)\n  const inkVersion = useInkVersion()\n  const seed = useReseed(isAnimated)\n  const outputWidth = Math.max(1, Math.round(width / pixelSize))\n  const outputHeight = Math.max(1, Math.round(height / pixelSize))\n  const colorKey = colors?.join(\"|\") ?? \"\"\n  const colorCount = colors?.length ?? 0\n\n  React.useEffect(() => {\n    const canvas = canvasRef.current\n    if (!canvas || !image) {\n      return\n    }\n    const request: DitherRequest = {\n      source: image,\n      width: outputWidth,\n      height: outputHeight,\n      matrixSize,\n      contrast,\n      seed,\n    }\n    const dithered = ditherOnCpu(request, readInks(canvas, readColorTokens(colorCount)))\n    if (dithered) {\n      canvas.getContext(\"2d\")?.putImageData(dithered, 0, 0)\n    }\n  }, [image, inkVersion, seed, outputWidth, outputHeight, matrixSize, contrast, colorCount, colorKey])\n\n  return (\n    <canvas\n      ref={canvasRef}\n      role=\"img\"\n      aria-label={alt}\n      data-slot=\"dither-image\"\n      width={outputWidth}\n      height={outputHeight}\n      className={cn(\"block pixelated\", className)}\n      style={{ width, height, ...readColorStyle(colors), ...style }}\n      {...props}\n    />\n  )\n}\n\nexport { DitherImage }\nexport type { DitherImageColors, DitherImageProps }\n",
      "type": "registry:ui"
    },
    {
      "path": "lib/dither/bayer.ts",
      "content": "export type BayerMatrixSize = 4 | 8\n\nfunction readBayer2(x: number, y: number): number {\n  const value = (Math.floor(x) * 0.5 + Math.floor(y) * Math.floor(y) * 0.75) % 1\n  return value < 0 ? value + 1 : value\n}\n\nfunction readBayer4(x: number, y: number): number {\n  return readBayer2(x * 0.5, y * 0.5) * 0.25 + readBayer2(x, y)\n}\n\nfunction readBayer8(x: number, y: number): number {\n  return readBayer4(x * 0.5, y * 0.5) * 0.25 + readBayer2(x, y)\n}\n\nconst BAYER_READERS: Record<BayerMatrixSize, (x: number, y: number) => number> = {\n  4: readBayer4,\n  8: readBayer8,\n}\n\nexport function readBayerThreshold(\n  x: number,\n  y: number,\n  size: BayerMatrixSize\n): number {\n  return BAYER_READERS[size](x, y)\n}\n",
      "type": "registry:lib",
      "target": "@lib/dither/bayer.ts"
    },
    {
      "path": "lib/dither/ditherOnCpu.ts",
      "content": "import { type BayerMatrixSize, readBayerThreshold } from \"./bayer\"\nimport type { InkPalette } from \"./inks\"\n\nexport type DitherRequest = {\n  source: CanvasImageSource\n  width: number\n  height: number\n  matrixSize: BayerMatrixSize\n  contrast: number\n  seed: number\n}\n\nconst SEED_OFFSETS = [\n  [0, 0],\n  [1, 2],\n  [3, 1],\n  [2, 3],\n  [5, 6],\n  [7, 4],\n] as const\n\nconst OPAQUE_FROM_ALPHA = 128\n\nlet sampleCanvas: HTMLCanvasElement | null = null\n\nfunction readSeedOffset(seed: number): readonly [number, number] {\n  return SEED_OFFSETS[Math.abs(Math.floor(seed)) % SEED_OFFSETS.length]\n}\n\nfunction readSampleContext(width: number, height: number) {\n  sampleCanvas ??= document.createElement(\"canvas\")\n  sampleCanvas.width = width\n  sampleCanvas.height = height\n  return sampleCanvas.getContext(\"2d\", { willReadFrequently: true })\n}\n\nexport function ditherOnCpu(\n  request: DitherRequest,\n  inks: InkPalette\n): ImageData | null {\n  const { width, height, source, matrixSize, contrast } = request\n  const context = readSampleContext(width, height)\n  if (!context) {\n    return null\n  }\n  context.drawImage(source, 0, 0, width, height)\n  const image = context.getImageData(0, 0, width, height)\n  const [offsetX, offsetY] = readSeedOffset(request.seed)\n  const lastInk = inks.length - 1\n  for (let y = 0; y < height; y += 1) {\n    for (let x = 0; x < width; x += 1) {\n      const offset = (y * width + x) * 4\n      if (image.data[offset + 3] < OPAQUE_FROM_ALPHA) {\n        image.data[offset + 3] = 0\n        continue\n      }\n      const luminance =\n        (image.data[offset] * 0.2126 +\n          image.data[offset + 1] * 0.7152 +\n          image.data[offset + 2] * 0.0722) /\n        255\n      const contrasted = Math.min(1, Math.max(0, (luminance - 0.5) * contrast + 0.5))\n      const threshold = readBayerThreshold(x + offsetX, y + offsetY, matrixSize)\n      const ink = inks[Math.min(lastInk, Math.max(0, Math.floor(contrasted * lastInk + threshold)))]\n      image.data[offset] = ink[0]\n      image.data[offset + 1] = ink[1]\n      image.data[offset + 2] = ink[2]\n      image.data[offset + 3] = 255\n    }\n  }\n  return image\n}\n",
      "type": "registry:lib",
      "target": "@lib/dither/ditherOnCpu.ts"
    },
    {
      "path": "lib/dither/useInkVersion.ts",
      "content": "\"use client\"\n\nimport { useEffect, useState } from \"react\"\n\nimport { observeInkChange } from \"./inks\"\n\nexport function useInkVersion(): number {\n  const [version, setVersion] = useState(0)\n\n  useEffect(() => observeInkChange(() => setVersion((current) => current + 1)), [])\n\n  return version\n}\n",
      "type": "registry:lib",
      "target": "@lib/dither/useInkVersion.ts"
    }
  ],
  "categories": [
    "ui"
  ],
  "type": "registry:ui"
}