{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "shader-upscale",
  "title": "Shader Upscale",
  "author": "Visant Labs <visantsupply@gmail.com>",
  "description": "Uma imagem pequena ampliada em bicúbico com nitidez, e uma linha que segue o cursor mostrando o antes em blocos.",
  "dependencies": [],
  "registryDependencies": [
    "utils",
    "@visant/css-color"
  ],
  "files": [
    {
      "path": "registry/visant/ui/shader-upscale.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\nimport { onThemeChange, resolveCssColor } from \"@/lib/css-color\";\n\n/**\n * Shader Upscale — uma imagem pequena ampliada por interpolação bicúbica\n * (Catmull-Rom) com nitidez de máscara, ao lado da mesma imagem ampliada por\n * vizinho mais próximo. Uma linha divide as duas e segue o cursor, como\n * comparador de antes e depois.\n *\n * Colhido do motor de efeitos de impressão do Visant Labs (`print-fx`, o\n * shader `upscale` do ImageLab). Lá ele amplia a foto do cliente antes de\n * imprimir; aqui ele mostra o que a ampliação faz.\n *\n * ── As decisões que não se veem ────────────────────────────────────────────\n *\n * 1. **A peça encolhe a imagem antes.** Ampliar uma foto que já é maior que a\n *    tela não mostra nada. `resolution` reduz a imagem num canvas 2D até o\n *    lado maior ter esse tanto de px, e é essa versão pequena que o shader\n *    amplia. Mudar o knob refaz só a redução, nunca o contexto.\n * 2. **Textura em vizinho mais próximo.** Com filtro linear, o lado \"antes\"\n *    sairia borrado em vez de em blocos, e as 16 leituras do bicúbico\n *    sairiam misturadas antes da conta. `NEAREST` deixa cada leitura cair num\n *    texel inteiro, que é o que o filtro de Catmull-Rom pressupõe.\n * 3. **A nitidez sai das leituras que já existem.** O original fazia a\n *    máscara de nitidez com quatro leituras extras a uma distância menor que\n *    um texel, o que na prática não afiava nada. Aqui o borrado de referência\n *    é o bilinear dos quatro texels do meio, que o bicúbico já leu: nitidez\n *    de verdade, sem leitura nova.\n * 4. **A linha fica onde o cursor deixou.** Comparador que volta sozinho\n *    para o meio esconde justamente o pedaço que a pessoa queria ver.\n * 5. **Sem laço.** Imagem parada não pede quadro: desenha na carga, no tema,\n *    no tamanho, no knob e quando a linha anda.\n */\n\nconst VERT = `attribute vec2 a_pos;void main(){gl_Position=vec4(a_pos,0.0,1.0);}`;\n\nconst FRAG = `\n#ifdef GL_FRAGMENT_PRECISION_HIGH\nprecision highp float;\n#else\nprecision mediump float;\n#endif\nuniform vec2 uRes;\nuniform vec2 uImg;\nuniform sampler2D uTex;\nuniform float uNitidez;\nuniform float uCorte;\nuniform float uDpr;\nuniform vec3 uPapel;\nuniform vec3 uLinha;\n\nfloat mitchell(float x){\n  const float B=0.;\n  const float C=.5;\n  float ax=abs(x);\n  if(ax<1.)return((12.-9.*B-6.*C)*ax*ax*ax+(-18.+12.*B+6.*C)*ax*ax+(6.-2.*B))/6.;\n  if(ax<2.)return((-B-6.*C)*ax*ax*ax+(6.*B+30.*C)*ax*ax+(-12.*B-48.*C)*ax+(8.*B+24.*C))/6.;\n  return 0.;\n}\nvec4 cubica(float x,vec4 c0,vec4 c1,vec4 c2,vec4 c3){\n  return c0*mitchell(x+1.)+c1*mitchell(x)+c2*mitchell(1.-x)+c3*mitchell(2.-x);\n}\nvec4 T(vec2 t,float dx,float dy){return texture2D(uTex,(t+vec2(dx,dy))/uImg);}\n\nvec4 ampliar(vec2 uv){\n  vec2 t=uv*uImg-.5;\n  vec2 f=fract(t);\n  t=floor(t)+.5;\n  vec4 a0=T(t,-1.,-1.),a1=T(t,0.,-1.),a2=T(t,1.,-1.),a3=T(t,2.,-1.);\n  vec4 b0=T(t,-1.,0.),b1=T(t,0.,0.),b2=T(t,1.,0.),b3=T(t,2.,0.);\n  vec4 c0=T(t,-1.,1.),c1=T(t,0.,1.),c2=T(t,1.,1.),c3=T(t,2.,1.);\n  vec4 d0=T(t,-1.,2.),d1=T(t,0.,2.),d2=T(t,1.,2.),d3=T(t,2.,2.);\n  vec4 cub=cubica(f.y,cubica(f.x,a0,a1,a2,a3),cubica(f.x,b0,b1,b2,b3),cubica(f.x,c0,c1,c2,c3),cubica(f.x,d0,d1,d2,d3));\n  vec4 bil=mix(mix(b1,b2,f.x),mix(c1,c2,f.x),f.y);\n  cub.rgb=clamp(cub.rgb+(cub.rgb-bil.rgb)*uNitidez*2.,0.,1.);\n  return clamp(cub,0.,1.);\n}\n\nvoid main(){\n  vec2 px=vec2(gl_FragCoord.x,uRes.y-gl_FragCoord.y);\n  float s=max(uRes.x/uImg.x,uRes.y/uImg.y);\n  vec2 tam=uImg*s;\n  vec2 uv=(px-(uRes-tam)*.5)/tam;\n  vec3 cor=uPapel;\n  if(uv.x>=0.&&uv.y>=0.&&uv.x<=1.&&uv.y<=1.){\n    vec4 c=px.x<uCorte?texture2D(uTex,uv):ampliar(uv);\n    cor=mix(uPapel,c.rgb,c.a);\n  }\n  float linha=1.-smoothstep(uDpr*.5,uDpr*1.5,abs(px.x-uCorte));\n  gl_FragColor=vec4(mix(cor,uLinha,linha),1.);\n}\n`;\n\n/** Paradas padrão: papel e linha do comparador. Tudo token. */\nconst CORES_PADRAO = [\"var(--background)\", \"var(--brand)\"];\n\nexport interface ShaderUpscaleProps extends Omit<React.HTMLAttributes<HTMLDivElement>, \"children\"> {\n  /** A imagem a reduzir e ampliar de volta. Fundo transparente vira papel. */\n  src: string;\n  /** Texto alternativo da imagem. */\n  alt?: string;\n  /**\n   * Lado maior da versão pequena, em px: quanto menor, mais a ampliação trabalha.\n   * @min 16\n   * @max 256\n   * @step 1\n   */\n  resolution?: number;\n  /**\n   * Nitidez depois de ampliar.\n   * @min 0\n   * @max 100\n   * @step 1\n   */\n  sharpening?: number;\n  /** Mostra o antes (vizinho mais próximo) à esquerda de uma linha que segue o cursor. */\n  compare?: boolean;\n  /** Paradas: papel e linha do comparador. Qualquer cor CSS ou token. */\n  colors?: string[];\n}\n\nfunction compilar(gl: WebGLRenderingContext, frag: string): WebGLProgram | null {\n  const shader = (tipo: number, src: string) => {\n    const s = gl.createShader(tipo);\n    if (!s) return null;\n    gl.shaderSource(s, src);\n    gl.compileShader(s);\n    return gl.getShaderParameter(s, gl.COMPILE_STATUS) ? s : null;\n  };\n  const vs = shader(gl.VERTEX_SHADER, VERT);\n  const fs = shader(gl.FRAGMENT_SHADER, frag);\n  const p = gl.createProgram();\n  if (!vs || !fs || !p) return null;\n  gl.attachShader(p, vs);\n  gl.attachShader(p, fs);\n  gl.linkProgram(p);\n  gl.deleteShader(vs);\n  gl.deleteShader(fs);\n  return gl.getProgramParameter(p, gl.LINK_STATUS) ? p : null;\n}\n\nexport function ShaderUpscale({\n  src,\n  alt = \"Imagem pequena ampliada\",\n  resolution = 64,\n  sharpening = 30,\n  compare = true,\n  colors,\n  className,\n  ...props\n}: ShaderUpscaleProps) {\n  const wrapRef = React.useRef<HTMLDivElement>(null);\n  const params = React.useRef({ resolution, sharpening, compare });\n  params.current = { resolution, sharpening, compare };\n  const redesenhar = React.useRef<() => void>(() => {});\n  const reduzir = React.useRef<() => void>(() => {});\n  const coresChave = (colors ?? CORES_PADRAO).join(\"|\");\n\n  React.useEffect(() => {\n    const wrap = wrapRef.current;\n    if (!wrap) return;\n    const canvas = document.createElement(\"canvas\");\n    canvas.setAttribute(\"aria-hidden\", \"true\");\n    canvas.className = \"absolute inset-0 block h-full w-full\";\n    wrap.appendChild(canvas);\n    const gl = canvas.getContext(\"webgl\", { antialias: false, alpha: false });\n    const prog = gl && compilar(gl, FRAG);\n    if (!gl || !prog) {\n      canvas.remove();\n      return;\n    }\n\n    const buf = gl.createBuffer();\n    gl.bindBuffer(gl.ARRAY_BUFFER, buf);\n    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW);\n    gl.useProgram(prog);\n    const aPos = gl.getAttribLocation(prog, \"a_pos\");\n    gl.enableVertexAttribArray(aPos);\n    gl.vertexAttribPointer(aPos, 2, gl.FLOAT, false, 0, 0);\n    const u = (n: string) => gl.getUniformLocation(prog, n);\n    const U = {\n      res: u(\"uRes\"),\n      img: u(\"uImg\"),\n      tex: u(\"uTex\"),\n      nitidez: u(\"uNitidez\"),\n      corte: u(\"uCorte\"),\n      dpr: u(\"uDpr\"),\n      papel: u(\"uPapel\"),\n      linha: u(\"uLinha\"),\n    };\n\n    const tex = gl.createTexture();\n    gl.bindTexture(gl.TEXTURE_2D, tex);\n    // 1×1 transparente até a imagem chegar: o primeiro quadro é só papel.\n    gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, new Uint8Array(4));\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.NEAREST);\n    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);\n    gl.uniform1i(U.tex, 0);\n    gl.uniform2f(U.img, 1, 1);\n\n    let dpr = 1;\n    let corte = 0.5; // fração da largura onde a linha está\n    const pequena = document.createElement(\"canvas\");\n    const ctx = pequena.getContext(\"2d\");\n\n    const cores = () => {\n      const [a, b] = coresChave.split(\"|\").map((cor) => resolveCssColor(cor, wrap));\n      gl.uniform3fv(U.papel, a);\n      gl.uniform3fv(U.linha, b ?? a);\n    };\n\n    const medir = () => {\n      // offsetWidth é a medida local: a vitrine escala o palco com transform.\n      dpr = Math.min(2, window.devicePixelRatio || 1);\n      canvas.width = Math.max(1, Math.round(wrap.offsetWidth * dpr));\n      canvas.height = Math.max(1, Math.round(wrap.offsetHeight * dpr));\n      gl.viewport(0, 0, canvas.width, canvas.height);\n    };\n\n    const desenhar = () => {\n      const p = params.current;\n      gl.uniform2f(U.res, canvas.width, canvas.height);\n      gl.uniform1f(U.nitidez, Math.max(0, Math.min(100, p.sharpening)) / 100);\n      // Sem comparador, a linha sai do canvas e tudo é o lado ampliado.\n      gl.uniform1f(U.corte, p.compare ? corte * canvas.width : -10 * dpr);\n      gl.uniform1f(U.dpr, dpr);\n      gl.drawArrays(gl.TRIANGLES, 0, 3);\n    };\n\n    redesenhar.current = desenhar;\n\n    let vivo = true;\n    let pronta = false;\n    const img = new Image();\n    img.crossOrigin = \"anonymous\";\n    img.decoding = \"async\";\n\n    reduzir.current = () => {\n      if (!pronta || !ctx) return;\n      const w = img.naturalWidth || 1;\n      const h = img.naturalHeight || 1;\n      const lado = Math.max(16, Math.min(256, Math.round(params.current.resolution)));\n      const k = Math.min(1, lado / Math.max(w, h));\n      pequena.width = Math.max(1, Math.round(w * k));\n      pequena.height = Math.max(1, Math.round(h * k));\n      ctx.imageSmoothingQuality = \"high\";\n      ctx.clearRect(0, 0, pequena.width, pequena.height);\n      ctx.drawImage(img, 0, 0, pequena.width, pequena.height);\n      gl.bindTexture(gl.TEXTURE_2D, tex);\n      gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, pequena);\n      gl.uniform2f(U.img, pequena.width, pequena.height);\n      desenhar();\n    };\n\n    img.onload = () => {\n      if (!vivo) return;\n      pronta = true;\n      reduzir.current();\n    };\n    img.src = src;\n\n    const mover = (e: PointerEvent) => {\n      if (!params.current.compare) return;\n      const r = wrap.getBoundingClientRect();\n      corte = r.width ? Math.max(0, Math.min(1, (e.clientX - r.left) / r.width)) : 0.5;\n      desenhar();\n    };\n\n    cores();\n    medir();\n    desenhar();\n\n    const ro = new ResizeObserver(() => {\n      medir();\n      desenhar();\n    });\n    ro.observe(wrap);\n    const offTema = onThemeChange(() => {\n      cores();\n      desenhar();\n    });\n    wrap.addEventListener(\"pointermove\", mover);\n\n    return () => {\n      vivo = false;\n      img.onload = null;\n      ro.disconnect();\n      offTema();\n      wrap.removeEventListener(\"pointermove\", mover);\n      redesenhar.current = () => {};\n      reduzir.current = () => {};\n      gl.deleteTexture(tex);\n      gl.deleteBuffer(buf);\n      gl.deleteProgram(prog);\n      gl.getExtension(\"WEBGL_lose_context\")?.loseContext();\n      canvas.remove();\n    };\n  }, [src, coresChave]);\n\n  React.useEffect(() => {\n    reduzir.current();\n  }, [resolution]);\n\n  React.useEffect(() => {\n    redesenhar.current();\n  }, [sharpening, compare]);\n\n  return (\n    <div\n      ref={wrapRef}\n      role=\"img\"\n      aria-label={alt}\n      className={cn(\"relative block aspect-square w-full overflow-hidden bg-background\", className)}\n      {...props}\n    />\n  );\n}\n",
      "type": "registry:ui"
    }
  ],
  "meta": {
    "uso": {
      "quando": "Mostrar o ganho de uma ampliação lado a lado, em página de ferramenta de imagem ou de IA",
      "evite": "Comparar duas fotos diferentes: photo-compare; foto em blocos como estilo, shader-pixelate"
    }
  },
  "categories": [
    "shaders",
    "shader",
    "webgl",
    "imagem",
    "ampliacao",
    "antes-e-depois"
  ],
  "type": "registry:ui"
}