{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "dither-image",
  "title": "Dither Image",
  "description": "A photograph reduced to two theme colours through an ordered dither.",
  "registryDependencies": [
    "utils",
    "https://ui.tinkererslabs.com/r/render-surface.json"
  ],
  "files": [
    {
      "path": "registry/default/dither-image/dither-image.tsx",
      "content": "\"use client\"\n\n/* eslint-disable @next/next/no-img-element -- This must work outside Next.js. */\n\nimport * as React from \"react\"\nimport {\n  RenderSurface,\n  createQuadProgram,\n  useThemeColors,\n  type QuadProgram,\n  type SurfaceColor,\n} from \"@/registry/default/render-surface/render-surface\"\nimport { cn } from \"@/lib/utils\"\n\nexport type DitherImageProps = React.HTMLAttributes<HTMLDivElement> & {\n  src: string\n  alt: string\n  /** Size of one dot in screen pixels. Larger is coarser and more printed. */\n  cell?: number\n  /** How many tones survive. Two is a pure halftone. */\n  levels?: number\n  /** The paper. */\n  base?: string\n  /** The ink. */\n  tint?: string\n  contrast?: number\n}\n\nconst FRAGMENT = `\nprecision highp float;\nvarying vec2 vUv;\nuniform sampler2D uImage;\nuniform vec2 uResolution;\nuniform float uCell;\nuniform float uLevels;\nuniform float uContrast;\nuniform float uBoxAspect;\nuniform float uImageAspect;\nuniform vec3 uBase;\nuniform vec3 uTint;\n\nfloat bayer2(vec2 a) {\n  a = floor(a);\n  return fract(dot(a, vec2(0.5, a.y * 0.75)));\n}\n\nfloat bayer4(vec2 a) {\n  return bayer2(0.5 * a) * 0.25 + bayer2(a);\n}\n\nvoid main() {\n  vec2 uv = vec2(vUv.x, 1.0 - vUv.y);\n\n  if (uBoxAspect > uImageAspect) {\n    uv.y = (uv.y - 0.5) * (uImageAspect / uBoxAspect) + 0.5;\n  } else {\n    uv.x = (uv.x - 0.5) * (uBoxAspect / uImageAspect) + 0.5;\n  }\n\n  vec2 sampleUv = uv;\n  sampleUv = floor(sampleUv * uResolution / uCell) * uCell / uResolution;\n\n  vec3 texel = texture2D(uImage, sampleUv).rgb;\n  float luma = dot(texel, vec3(0.2126, 0.7152, 0.0722));\n  luma = clamp((luma - 0.5) * uContrast + 0.5, 0.0, 1.0);\n\n  float threshold = bayer4(vUv * uResolution / uCell) * 1.0667;\n  float steps = max(uLevels - 1.0, 1.0);\n  float value = floor(luma * steps + threshold) / steps;\n\n  // value counts light, so it selects the paper. Mixing the other way round\n  // prints the highlights in ink and the shadows in white.\n  gl_FragColor = vec4(mix(uTint, uBase, clamp(value, 0.0, 1.0)), 1.0);\n}\n`\n\ntype Scene = {\n  quad: QuadProgram | null\n  texture: WebGLTexture | null\n  aspect: number\n  cancelled: boolean\n}\n\n/**\n * Reduces a photograph to two theme colours through an ordered dither, the way\n * a newspaper reduced one to ink and paper. The original is kept underneath as\n * ordinary markup, so nothing that cannot run the shader is left with a gap.\n */\nexport function DitherImage({\n  src,\n  alt,\n  cell = 4,\n  levels = 2,\n  base = \"--background\",\n  tint = \"--foreground\",\n  contrast = 1.15,\n  className,\n  children,\n  ...rootProps\n}: DitherImageProps) {\n  const rootRef = React.useRef<HTMLDivElement>(null)\n  const [arrived, setArrived] = React.useState(0)\n\n  const tokens = React.useMemo(\n    () => [base, tint].filter((entry) => entry.startsWith(\"--\")),\n    [base, tint]\n  )\n\n  const resolved = useThemeColors(rootRef, tokens)\n  const baseColor = base.startsWith(\"--\") ? resolved[base] : undefined\n  const tintColor = tint.startsWith(\"--\") ? resolved[tint] : undefined\n  const ready = baseColor !== undefined && tintColor !== undefined\n\n  const colorsRef = React.useRef<{ base: SurfaceColor; tint: SurfaceColor }>({\n    base: [1, 1, 1],\n    tint: [0, 0, 0],\n  })\n\n  React.useEffect(() => {\n    if (baseColor && tintColor) {\n      colorsRef.current = { base: baseColor, tint: tintColor }\n    }\n  }, [baseColor, tintColor])\n\n  const setup = React.useCallback(\n    ({ context }: { context: WebGLRenderingContext }): Scene => {\n      const scene: Scene = {\n        quad: createQuadProgram(context, FRAGMENT),\n        texture: context.createTexture(),\n        aspect: 1,\n        cancelled: false,\n      }\n\n      context.bindTexture(context.TEXTURE_2D, scene.texture)\n      context.texImage2D(\n        context.TEXTURE_2D,\n        0,\n        context.RGBA,\n        1,\n        1,\n        0,\n        context.RGBA,\n        context.UNSIGNED_BYTE,\n        new Uint8Array([0, 0, 0, 0])\n      )\n\n      const image = new Image()\n      image.crossOrigin = \"anonymous\"\n      image.decoding = \"async\"\n\n      image.onload = () => {\n        if (scene.cancelled || !scene.texture) return\n\n        setArrived((count) => count + 1)\n        scene.aspect = image.naturalWidth / Math.max(image.naturalHeight, 1)\n        context.bindTexture(context.TEXTURE_2D, scene.texture)\n        context.texImage2D(\n          context.TEXTURE_2D,\n          0,\n          context.RGBA,\n          context.RGBA,\n          context.UNSIGNED_BYTE,\n          image\n        )\n        context.texParameteri(\n          context.TEXTURE_2D,\n          context.TEXTURE_WRAP_S,\n          context.CLAMP_TO_EDGE\n        )\n        context.texParameteri(\n          context.TEXTURE_2D,\n          context.TEXTURE_WRAP_T,\n          context.CLAMP_TO_EDGE\n        )\n        context.texParameteri(\n          context.TEXTURE_2D,\n          context.TEXTURE_MIN_FILTER,\n          context.LINEAR\n        )\n        context.texParameteri(\n          context.TEXTURE_2D,\n          context.TEXTURE_MAG_FILTER,\n          context.LINEAR\n        )\n      }\n\n      image.src = src\n\n      return scene\n    },\n    [src]\n  )\n\n  const draw = React.useCallback(\n    ({\n      context,\n      size,\n      state,\n    }: {\n      context: WebGLRenderingContext\n      size: { width: number; height: number; dpr: number }\n      state: Scene\n    }) => {\n      const quad = state.quad\n      if (!quad) return\n\n      const { width, height, dpr } = size\n      context.useProgram(quad.program)\n      context.activeTexture(context.TEXTURE0)\n      context.bindTexture(context.TEXTURE_2D, state.texture)\n      context.uniform1i(quad.uniform(\"uImage\"), 0)\n      context.uniform2f(quad.uniform(\"uResolution\"), width * dpr, height * dpr)\n      context.uniform1f(quad.uniform(\"uCell\"), Math.max(cell, 1) * dpr)\n      context.uniform1f(quad.uniform(\"uLevels\"), Math.max(levels, 2))\n      context.uniform1f(quad.uniform(\"uContrast\"), contrast)\n      context.uniform1f(quad.uniform(\"uBoxAspect\"), width / Math.max(height, 1))\n      context.uniform1f(quad.uniform(\"uImageAspect\"), state.aspect)\n      context.uniform3fv(quad.uniform(\"uBase\"), colorsRef.current.base)\n      context.uniform3fv(quad.uniform(\"uTint\"), colorsRef.current.tint)\n\n      quad.paint(width, height, dpr)\n    },\n    [cell, contrast, levels]\n  )\n\n  const teardown = React.useCallback((state: Scene) => {\n    state.cancelled = true\n    state.quad?.dispose()\n  }, [])\n\n  return (\n    <div\n      ref={rootRef}\n      data-slot=\"dither-image\"\n      className={cn(\n        \"bg-muted relative isolate overflow-hidden rounded-[var(--radius)]\",\n        className\n      )}\n      {...rootProps}\n    >\n      <img\n        src={src}\n        alt={alt}\n        className=\"absolute inset-0 size-full object-cover\"\n      />\n\n      {ready && (\n        <RenderSurface<Scene, \"webgl\">\n          contextType=\"webgl\"\n          setup={setup}\n          draw={draw}\n          teardown={teardown}\n          revision={arrived}\n          label={alt}\n          className=\"absolute inset-0\"\n        />\n      )}\n\n      {children !== undefined && <div className=\"relative\">{children}</div>}\n    </div>\n  )\n}\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "scene",
    "media",
    "shader",
    "webgl"
  ],
  "type": "registry:ui"
}