{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "halftone-print",
  "title": "Halftone Print",
  "description": "Print a scene onto paper: each colour separates onto its own rotated screen of dots sized by the ink under them, slightly out of register the way a real press runs.",
  "dependencies": ["remotion"],
  "registryDependencies": ["@remocn/canvas-presentation"],
  "files": [
    {
      "path": "registry/remocn/halftone-print/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 HalftonePrintProps = {\n  dotSize?: number;\n  angle?: number;\n  misregistration?: number;\n  paperTint?: string;\n};\n\nconst DEFAULTS = {\n  dotSize: 10,\n  angle: 0,\n  misregistration: 1.2,\n  paperTint: \"#f4efe4\",\n};\n\ntype NormalizedProps = {\n  cell: number;\n  angle: number;\n  shift: number;\n  paper: string;\n};\n\nconst FRAGMENT_SHADER = `#version 300 es\nprecision highp float;\n\nuniform sampler2D u_scene;\nuniform float u_aspect;\nuniform float u_cell;\nuniform float u_angle;\nuniform float u_shift;\nuniform vec3 u_paper;\n\nin vec2 v_uv;\nout vec4 outColor;\n\nconst vec3 CYAN_ABSORB = vec3(0.92, 0.12, 0.02);\nconst vec3 MAGENTA_ABSORB = vec3(0.10, 0.88, 0.06);\nconst vec3 YELLOW_ABSORB = vec3(0.03, 0.10, 0.90);\n\nvec2 toSquare(vec2 uv) {\n  return vec2(uv.x * u_aspect, uv.y);\n}\n\nvec2 fromSquare(vec2 p) {\n  return vec2(p.x / u_aspect, p.y);\n}\n\nfloat screenInk(float angle, vec2 drift, vec3 pick) {\n  vec2 p = toSquare(v_uv);\n  float s = sin(angle);\n  float c = cos(angle);\n  vec2 r = vec2(p.x * c - p.y * s, p.x * s + p.y * c);\n\n  vec2 center = (floor(r / u_cell) + 0.5) * u_cell;\n  vec2 unrotated =\n    vec2(center.x * c + center.y * s, -center.x * s + center.y * c);\n\n  vec4 texel = texture(u_scene, fromSquare(unrotated + drift));\n  vec3 straight = texel.rgb / max(texel.a, 0.004);\n  float ink = clamp(1.0 - dot(straight, pick), 0.0, 1.0) * texel.a;\n\n  float radius = sqrt(ink) * u_cell * 0.72;\n  float aa = u_cell * 0.045;\n  float coverage = 1.0 - smoothstep(radius - aa, radius + aa, length(r - center));\n  return coverage * step(0.002, ink);\n}\n\nvoid main() {\n  vec4 source = texture(u_scene, v_uv);\n\n  float cyan =\n    screenInk(u_angle + 0.2618, u_shift * vec2(1.0, 0.0), vec3(1.0, 0.0, 0.0));\n  float magenta =\n    screenInk(u_angle + 1.3090, u_shift * vec2(-0.5, 0.866), vec3(0.0, 1.0, 0.0));\n  float yellow =\n    screenInk(u_angle, u_shift * vec2(-0.5, -0.866), vec3(0.0, 0.0, 1.0));\n\n  vec3 color = u_paper;\n  color *= 1.0 - cyan * CYAN_ABSORB;\n  color *= 1.0 - magenta * MAGENTA_ABSORB;\n  color *= 1.0 - yellow * YELLOW_ABSORB;\n\n  outColor = vec4(color * source.a, source.a);\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 { cell, angle, shift, paper } = passedProps;\n    gl.uniform1f(uniform(\"u_cell\"), cell);\n    gl.uniform1f(uniform(\"u_angle\"), angle);\n    gl.uniform1f(uniform(\"u_shift\"), shift);\n    gl.uniform3fv(uniform(\"u_paper\"), toRgb(paper));\n  },\n);\n\nconst HalftonePrintFallback: React.FC<NormalizedProps & CanvasFilterProps> = ({\n  children,\n}) => <AbsoluteFill>{children}</AbsoluteFill>;\n\nconst HalftonePrintCanvas = makeCanvasFilter<NormalizedProps>({\n  shader,\n  fallback: HalftonePrintFallback,\n});\n\nexport const HalftonePrint: React.FC<\n  HalftonePrintProps & CanvasFilterProps\n> = ({\n  dotSize = DEFAULTS.dotSize,\n  angle = DEFAULTS.angle,\n  misregistration = DEFAULTS.misregistration,\n  paperTint = DEFAULTS.paperTint,\n  children,\n}) => {\n  const { height } = useVideoConfig();\n\n  return (\n    <HalftonePrintCanvas\n      cell={Math.max(dotSize, 1) / height}\n      angle={(angle * Math.PI) / 180}\n      shift={misregistration / height}\n      paper={paperTint}\n    >\n      {children}\n    </HalftonePrintCanvas>\n  );\n};\n",
      "type": "registry:component",
      "target": "components/remocn/halftone-print.tsx"
    }
  ],
  "type": "registry:component"
}
