{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "ascii-render",
  "title": "ASCII Render",
  "description": "Redraw a live scene as ASCII: the frame divides into glyph cells and each one becomes the character matching the brightness of the picture inside it.",
  "dependencies": ["remotion"],
  "registryDependencies": ["@remocn/canvas-presentation"],
  "files": [
    {
      "path": "registry/remocn/ascii-render/index.tsx",
      "content": "\"use client\";\n\nimport type React from \"react\";\nimport { AbsoluteFill, useVideoConfig } from \"remotion\";\nimport {\n  type CanvasFilterProps,\n  makeCanvasFilter,\n  makeFilterShader,\n} from \"@/lib/remocn/canvas-presentation\";\n\nexport type AsciiRenderProps = {\n  glyphSize?: number;\n  charset?: string;\n  colored?: boolean;\n  ink?: string;\n  intensity?: number;\n};\n\nconst DEFAULTS = {\n  glyphSize: 26,\n  charset: \" .:-=+*#%@\",\n  colored: false,\n  ink: \"#9dff9d\",\n  intensity: 1,\n};\n\nconst GLYPH_ASPECT = 0.545;\nconst ATLAS_CELL_W = 24;\nconst ATLAS_CELL_H = 44;\nconst ATLAS_FONT =\n  'ui-monospace, SFMono-Regular, Menlo, Consolas, \"Liberation Mono\", monospace';\n\ntype NormalizedProps = {\n  cellX: number;\n  cellY: number;\n  charset: string;\n  colored: boolean;\n  ink: string;\n  intensity: number;\n};\n\nconst FRAGMENT_SHADER = `#version 300 es\nprecision highp float;\n\nuniform sampler2D u_scene;\nuniform sampler2D u_atlas;\nuniform vec2 u_cell;\nuniform float u_glyph_count;\nuniform float u_colored;\nuniform vec3 u_ink;\nuniform float u_intensity;\n\nin vec2 v_uv;\nout vec4 outColor;\n\nconst int TAPS = 3;\n\nvec4 cellAverage(vec2 center, vec2 cell) {\n  vec4 sum = vec4(0.0);\n  for (int y = 0; y < TAPS; y++) {\n    for (int x = 0; x < TAPS; x++) {\n      vec2 offset = (vec2(float(x), float(y)) + 0.5) / float(TAPS) - 0.5;\n      sum += texture(u_scene, center + offset * cell);\n    }\n  }\n  return sum / float(TAPS * TAPS);\n}\n\nvoid main() {\n  vec2 cell = max(u_cell, vec2(0.001));\n  vec2 origin = floor(v_uv / cell) * cell;\n  vec4 sampled = cellAverage(origin + cell * 0.5, cell);\n\n  float luma = dot(sampled.rgb, vec3(0.299, 0.587, 0.114));\n  float index = min(floor(luma * u_glyph_count), u_glyph_count - 1.0);\n\n  vec2 local = clamp((v_uv - origin) / cell, 0.008, 0.992);\n  float mask =\n    texture(u_atlas, vec2((index + local.x) / u_glyph_count, local.y)).r;\n\n  vec3 glyph = mix(u_ink, sampled.rgb, u_colored) * mask;\n  vec4 source = texture(u_scene, v_uv);\n  float amount = clamp(u_intensity, 0.0, 1.0);\n\n  outColor = vec4(\n    mix(source.rgb, glyph, amount),\n    mix(source.a, max(mask, sampled.a), amount)\n  );\n}`;\n\ntype Atlas = { charset: string; texture: WebGLTexture };\n\nconst atlases = new WeakMap<WebGL2RenderingContext, Atlas>();\n\nfunction drawAtlas(charset: string): OffscreenCanvas {\n  const canvas = new OffscreenCanvas(\n    ATLAS_CELL_W * charset.length,\n    ATLAS_CELL_H,\n  );\n  const context = canvas.getContext(\"2d\");\n  if (!context) {\n    throw new Error(\"ascii-render: failed to acquire a 2D context\");\n  }\n  context.fillStyle = \"#000000\";\n  context.fillRect(0, 0, canvas.width, canvas.height);\n  context.fillStyle = \"#ffffff\";\n  context.font = `${Math.round(ATLAS_CELL_H * 0.74)}px ${ATLAS_FONT}`;\n  context.textAlign = \"center\";\n  context.textBaseline = \"middle\";\n  for (let i = 0; i < charset.length; i++) {\n    context.fillText(\n      charset[i],\n      i * ATLAS_CELL_W + ATLAS_CELL_W / 2,\n      ATLAS_CELL_H / 2,\n    );\n  }\n  return canvas;\n}\n\nfunction bindAtlas(gl: WebGL2RenderingContext, charset: string): void {\n  gl.activeTexture(gl.TEXTURE1);\n\n  let atlas = atlases.get(gl);\n  if (!atlas) {\n    const texture = gl.createTexture();\n    if (!texture) {\n      throw new Error(\"ascii-render: failed to create the glyph atlas\");\n    }\n    gl.bindTexture(gl.TEXTURE_2D, texture);\n    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);\n    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);\n    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);\n    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);\n    atlas = { charset: \"\", texture };\n    atlases.set(gl, atlas);\n  } else {\n    gl.bindTexture(gl.TEXTURE_2D, atlas.texture);\n  }\n\n  if (atlas.charset !== charset) {\n    gl.texImage2D(\n      gl.TEXTURE_2D,\n      0,\n      gl.RGBA,\n      gl.RGBA,\n      gl.UNSIGNED_BYTE,\n      drawAtlas(charset),\n    );\n    atlas.charset = charset;\n  }\n}\n\nfunction toRgb(hex: string): [number, number, number] {\n  const value = hex.replace(\"#\", \"\");\n  const full =\n    value.length === 3\n      ? value\n          .split(\"\")\n          .map((c) => c + c)\n          .join(\"\")\n      : value;\n  const int = Number.parseInt(full, 16);\n  if (Number.isNaN(int)) return [1, 1, 1];\n  return [\n    ((int >> 16) & 255) / 255,\n    ((int >> 8) & 255) / 255,\n    (int & 255) / 255,\n  ];\n}\n\nconst shader = makeFilterShader<NormalizedProps>(\n  FRAGMENT_SHADER,\n  ({ gl, uniform, passedProps }) => {\n    const { cellX, cellY, charset, colored, ink, intensity } = passedProps;\n    bindAtlas(gl, charset);\n    gl.uniform1i(uniform(\"u_atlas\"), 1);\n    gl.uniform1f(uniform(\"u_glyph_count\"), charset.length);\n    gl.uniform2f(uniform(\"u_cell\"), cellX, cellY);\n    gl.uniform1f(uniform(\"u_colored\"), colored ? 1 : 0);\n    gl.uniform3fv(uniform(\"u_ink\"), toRgb(ink));\n    gl.uniform1f(uniform(\"u_intensity\"), intensity);\n  },\n);\n\nconst AsciiRenderFallback: React.FC<NormalizedProps & CanvasFilterProps> = ({\n  children,\n}) => <AbsoluteFill>{children}</AbsoluteFill>;\n\nconst AsciiRenderCanvas = makeCanvasFilter<NormalizedProps>({\n  shader,\n  fallback: AsciiRenderFallback,\n});\n\nexport const AsciiRender: React.FC<AsciiRenderProps & CanvasFilterProps> = ({\n  glyphSize = DEFAULTS.glyphSize,\n  charset = DEFAULTS.charset,\n  colored = DEFAULTS.colored,\n  ink = DEFAULTS.ink,\n  intensity = DEFAULTS.intensity,\n  children,\n}) => {\n  const { width, height } = useVideoConfig();\n  const resolved = charset.length > 0 ? charset : DEFAULTS.charset;\n\n  return (\n    <AsciiRenderCanvas\n      cellX={(glyphSize * GLYPH_ASPECT) / width}\n      cellY={glyphSize / height}\n      charset={resolved}\n      colored={colored}\n      ink={ink}\n      intensity={intensity}\n    >\n      {children}\n    </AsciiRenderCanvas>\n  );\n};\n",
      "type": "registry:component",
      "target": "components/remocn/ascii-render.tsx"
    }
  ],
  "type": "registry:component"
}
