{
  "name": "webgl-rgb",
  "type": "registry:component",
  "description": "WebGL RGB displacement effect",
  "files": [
    {
      "path": "ui/WebglRgb.astro",
      "type": "registry:component",
      "content": "---\ntype PabloTextureName =\n  | 'none'\n  | 'Adamantium.jpg'\n  | 'AppleGIOS18.jpeg'\n  | 'BlueRays.jpeg'\n  | 'Celestials.jpeg'\n  | 'ChaoticGradients-b.jpg'\n  | 'ChaoticGradients-c.jpg'\n  | 'ChaoticGradients.jpg'\n  | 'Chroma.jpeg'\n  | 'ColdOcean.jpeg'\n  | 'CubicGlass.jpg'\n  | 'Darkmist.jpg'\n  | 'Darkshell.jpeg'\n  | 'DramaticGradient.jpeg'\n  | 'DreamyFabrica.jpeg'\n  | 'FractalMaze-a.jpg'\n  | 'FractalMaze.jpg'\n  | 'FractalNight.jpeg'\n  | 'FractalWall.jpg'\n  | 'Francium.jpeg'\n  | 'Fuchsia.jpeg'\n  | 'GalacticRing.jpeg'\n  | 'GlassSand.jpeg'\n  | 'GradientBurst.jpeg'\n  | 'HeroGradient.jpg'\n  | 'Inflated.jpeg'\n  | 'Japaneasy.jpg'\n  | 'Lavendery.jpeg'\n  | 'LightGlow.jpg'\n  | 'Luma.jpeg'\n  | 'Mandrillian.jpeg'\n  | 'Mistmusk.jpg'\n  | 'Mistnova.jpeg'\n  | 'moon.png'\n  | 'Nova.jpeg'\n  | 'Oceanic.jpeg'\n  | 'Phantom.jpeg'\n  | 'Redillium.jpeg'\n  | 'RubyGradient.jpeg'\n  | 'Serenmist.jpeg'\n  | 'Shockwave.jpeg'\n  | 'Sparkle.jpeg'\n  | 'SpectralDark.jpg'\n  | 'SpectralLight.jpg'\n  | 'Tangerine.jpeg'\n  | 'Ultravibe.png'\n  | 'silent_gravity.png';\n\ntype TextureFilename = PabloTextureName | string;\n\nconst EXTERNAL_BASE_URL = 'https://storage.googleapis.com/staging-pablo-textures/' as const;\n\ninterface Props {\n  filename?: TextureFilename;\n  className?: string;\n  hAlign?: 'left' | 'center' | 'right';\n  vAlign?: 'top' | 'center' | 'bottom';\n  fit?: 'cover' | 'contain' | 'fill' | 'fitWidth' | 'fitHeight';\n  opacity?: number;\n  // CSS variable names for finalized channel colors (fixed ok)\n  rVar?: string; // default: \"--sp-webgl-r\"\n  gVar?: string; // default: \"--sp-webgl-g\"\n  bVar?: string; // default: \"--sp-webgl-b\"\n}\n\nconst {\n  filename = 'Ultravibe.png',\n  className = '',\n  hAlign,\n  vAlign,\n  fit,\n  opacity = 0.5,\n  rVar = '--sp-webgl-r',\n  gVar = '--sp-webgl-g',\n  bVar = '--sp-webgl-b',\n} = Astro.props as Props;\n\nconst textureModules = (\n  import.meta as ImportMeta & {\n    glob: (pattern: string, options: { eager: true; import: 'default' }) => Record<string, unknown>;\n  }\n).glob('../../assets/textures/*', {\n  eager: true,\n  import: 'default',\n}) as Record<string, string>;\n\nconst textureUrlMap = Object.fromEntries(\n  Object.entries(textureModules).map(([fullPath, url]) => [fullPath.split('/').pop()!, url]),\n);\n\nconst uid = `rgb-${Math.random().toString(36).slice(2)}`;\nconst isDisabled =\n  String(filename || '')\n    .trim()\n    .toLowerCase() === 'none';\n---\n{isDisabled ? null : (\n  <div class={`absolute inset-0 -z-10 pointer-events-none ${className}`}>\n    <canvas id={`${uid}-canvas`} class=\"h-full w-full\"></canvas>\n    <script\n      type=\"application/json\"\n      id={`${uid}-data`}\n      set:html={JSON.stringify({\n        filename,\n        hAlign,\n        vAlign,\n        fit,\n        opacity,\n        rVar,\n        gVar,\n        bVar,\n      })}\n    />\n    <script\n      type=\"application/json\"\n      id={`${uid}-assets`}\n      set:html={JSON.stringify(textureUrlMap)}\n    />\n    <script type=\"module\" define:vars={{ uid, EXTERNAL_BASE_URL }}>\n      {\n        (function () {\n        const canvas = document.getElementById(uid + \"-canvas\");\n        if (!canvas) return;\n        const root = canvas.parentElement;\n        const dataEl = document.getElementById(uid + \"-data\");\n        const assetsEl = document.getElementById(uid + \"-assets\");\n        const externalBase = EXTERNAL_BASE_URL;\n\n        /** @type {{ filename: string; hAlign?: string; vAlign?: string; fit?: string; opacity?: number; rVar?: string; gVar?: string; bVar?: string }} */\n        let cfg = {\n          filename: \"\",\n          hAlign: \"\",\n          vAlign: \"\",\n          fit: \"\",\n          opacity: 1,\n          rVar: \"--sp-webgl-r\",\n          gVar: \"--sp-webgl-g\",\n          bVar: \"--sp-webgl-b\",\n        };\n        /** @type {Record<string,string>} */\n        let assetsMap = {};\n        try {\n          const base = JSON.parse(dataEl?.textContent || \"{}\") || {};\n          cfg = Object.assign(cfg, base);\n        } catch {}\n        try {\n          assetsMap = JSON.parse(assetsEl?.textContent || \"{}\");\n        } catch {}\n        function sanitizeCfg() {\n          // Keep props if valid; otherwise leave empty to allow CSS var fallback\n          if (cfg.hAlign) {\n            cfg.hAlign =\n              cfg.hAlign === \"left\" ||\n              cfg.hAlign === \"center\" ||\n              cfg.hAlign === \"right\"\n                ? cfg.hAlign\n                : \"\";\n          }\n          if (cfg.vAlign) {\n            cfg.vAlign =\n              cfg.vAlign === \"top\" ||\n              cfg.vAlign === \"center\" ||\n              cfg.vAlign === \"bottom\"\n                ? cfg.vAlign\n                : \"\";\n          }\n          if (cfg.fit) {\n            cfg.fit =\n              cfg.fit === \"cover\" ||\n              cfg.fit === \"contain\" ||\n              cfg.fit === \"fill\" ||\n              cfg.fit === \"fitWidth\" ||\n              cfg.fit === \"fitHeight\"\n                ? cfg.fit\n                : \"\";\n          }\n          const o = parseFloat(String(cfg.opacity ?? 1));\n          cfg.opacity = isFinite(o) ? Math.max(0, Math.min(1, o)) : 1;\n          // Ensure vars and fallbacks are strings\n          cfg.rVar = String(cfg.rVar || \"--sp-webgl-r\");\n          cfg.gVar = String(cfg.gVar || \"--sp-webgl-g\");\n          cfg.bVar = String(cfg.bVar || \"--sp-webgl-b\");\n        }\n        sanitizeCfg();\n\n        function parseColorToRgb01(input) {\n          if (!input) return new Float32Array([1, 1, 1]);\n          const s = String(input).trim();\n          const hex = /^#?([\\da-f]{2})([\\da-f]{2})([\\da-f]{2})$/i.exec(s);\n          if (hex) {\n            return new Float32Array([\n              parseInt(hex[1], 16) / 255,\n              parseInt(hex[2], 16) / 255,\n              parseInt(hex[3], 16) / 255,\n            ]);\n          }\n          const rgb =\n            /^rgba?\\(\\s*([\\d.]+)\\s*,\\s*([\\d.]+)\\s*,\\s*([\\d.]+)(?:\\s*,\\s*[\\d.]+\\s*)?\\)$/.exec(\n              s,\n            );\n          if (rgb) {\n            return new Float32Array([\n              Math.max(0, Math.min(255, parseFloat(rgb[1]))) / 255,\n              Math.max(0, Math.min(255, parseFloat(rgb[2]))) / 255,\n              Math.max(0, Math.min(255, parseFloat(rgb[3]))) / 255,\n            ]);\n          }\n          const span = document.createElement(\"span\");\n          span.style.color = s;\n          document.body.appendChild(span);\n          const cs = getComputedStyle(span).color;\n          document.body.removeChild(span);\n          return parseColorToRgb01(cs);\n        }\n\n        function resolveRgbColors() {\n          const css = (name) =>\n            getComputedStyle(root || document.documentElement).getPropertyValue(\n              name,\n            ) || \"\";\n          function read(name) {\n            const v = String(css(name) || \"\").trim();\n            return parseColorToRgb01(v);\n          }\n          const r = read(String(cfg.rVar || \"--sp-webgl-r\"));\n          const g = read(String(cfg.gVar || \"--sp-webgl-g\"));\n          const b = read(String(cfg.bVar || \"--sp-webgl-b\"));\n          return [r, g, b];\n        }\n\n        // Resolve alignment and fit with priority: props (cfg.*) > CSS vars > defaults\n        function resolveAlignFit() {\n          function readCssToken(varName, allowed, fallback) {\n            const raw = getComputedStyle(\n              root || document.documentElement,\n            ).getPropertyValue(varName);\n            let v = String(raw || \"\").trim();\n            if (v) {\n              const q = v[0];\n              if ((q === '\"' || q === \"'\") && v[v.length - 1] === q) {\n                v = v.slice(1, -1);\n              }\n              v = v.toLowerCase();\n            }\n            return allowed.includes(v) ? v : fallback;\n          }\n\n          const h = (function () {\n            const v = String(cfg.hAlign || \"\")\n              .trim()\n              .toLowerCase();\n            if (v === \"left\" || v === \"center\" || v === \"right\") return v;\n            return readCssToken(\n              \"--sp-webgl-h-align\",\n              [\"left\", \"center\", \"right\"],\n              \"center\",\n            );\n          })();\n\n          const v = (function () {\n            const s = String(cfg.vAlign || \"\")\n              .trim()\n              .toLowerCase();\n            if (s === \"top\" || s === \"center\" || s === \"bottom\") return s;\n            return readCssToken(\n              \"--sp-webgl-v-align\",\n              [\"top\", \"center\", \"bottom\"],\n              \"center\",\n            );\n          })();\n\n          const f = (function () {\n            const s = String(cfg.fit || \"\").trim();\n            if (\n              s === \"cover\" ||\n              s === \"contain\" ||\n              s === \"fill\" ||\n              s === \"fitWidth\" ||\n              s === \"fitHeight\"\n            )\n              return s;\n            return (\n              readCssToken(\n                \"--sp-webgl-fit\",\n                [\"cover\", \"contain\", \"fill\", \"fitwidth\", \"fitheight\"],\n                \"cover\",\n              )\n                // normalize case for width/height tokens\n                .replace(\"fitwidth\", \"fitWidth\")\n                .replace(\"fitheight\", \"fitHeight\")\n            );\n          })();\n\n          return { hAlign: h, vAlign: v, fit: f };\n        }\n\n        // Resolve filename from CSS var --sp-webgl-name with props fallback\n        function resolveFilename() {\n          const css = (name) =>\n            getComputedStyle(root || document.documentElement).getPropertyValue(\n              name,\n            ) || \"\";\n          let name = css(\"--sp-webgl-name\").trim();\n          if (name) {\n            // Strip wrapping quotes if present\n            const q = name[0];\n            if ((q === '\"' || q === \"'\") && name[name.length - 1] === q) {\n              name = name.slice(1, -1);\n            }\n          } else {\n            name = String(cfg.filename || \"\");\n          }\n          return name;\n        }\n\n        function urlForFilename(fname) {\n          const asset = assetsMap[fname];\n          const localUrl =\n            typeof asset === \"string\"\n              ? asset\n              : asset && asset.src\n                ? asset.src\n                : undefined;\n          if (localUrl) return localUrl;\n\n          const s = String(fname || \"\").trim();\n          if (!s) return \"\";\n          // Treat special token \"none\" as disabled (no image)\n          if (s.toLowerCase() === \"none\") return \"\";\n\n          // If s looks like an absolute URL (http/https/data/blob), use as-is\n          if (\n            /^(?:https?:)?\\/\\//i.test(s) ||\n            /^data:/i.test(s) ||\n            /^blob:/i.test(s)\n          ) {\n            return s;\n          }\n\n          // If s is an absolute or root-relative path, use as-is\n          if (s.startsWith(\"/\") || s.startsWith(\"./\") || s.startsWith(\"../\")) {\n            return s;\n          }\n\n          // Otherwise, treat as remote filename under external base\n          return externalBase + encodeURIComponent(s);\n        }\n\n        function alignXCode(s) {\n          return s === \"left\" ? 0 : s === \"right\" ? 2 : 1;\n        }\n        function alignYCode(s) {\n          // Keep intuitive mapping like WebglEffect: 0=top,1=center,2=bottom\n          // We'll compute offset in shader-side coordinates with bottom-left UV origin.\n          return s === \"top\" ? 0 : s === \"bottom\" ? 2 : 1;\n        }\n\n        function computeWindow(canvasAspect, imgAspect, fitMode) {\n          // Return the sub-rectangle of the texture to sample (winX, winY) in [0,1]\n          // so that sampling stays within the texture and alignment can be applied by offsets.\n          // This mirrors CSS background-size behavior using a crop window:\n          // - cover => crop the minor axis\n          // - contain => no crop: show full image (window 1.0 on the limiting axis)\n          // - fitWidth => crop vertical like cover when necessary\n          // - fitHeight => crop horizontal like cover when necessary\n\n          const eps = 0.0001;\n          const a = Math.max(eps, canvasAspect);\n          const b = Math.max(eps, imgAspect);\n\n          if (fitMode === \"fill\") return { winX: 1.0, winY: 1.0 };\n\n          if (fitMode === \"fitWidth\") {\n            // Fit image width to canvas width; crop vertically if needed\n            const scaleY = a / b; // how many image-heights fit into canvas height when width fits\n            return { winX: 1.0, winY: 1.0 / scaleY };\n          }\n\n          if (fitMode === \"fitHeight\") {\n            // Fit image height to canvas height; crop horizontally if needed\n            const scaleX = b / a;\n            return { winX: 1.0 / scaleX, winY: 1.0 };\n          }\n\n          if (fitMode === \"contain\") {\n            // Show full image with letter/pillar boxes (no crop)\n            if (a >= b) {\n              // canvas is wider → pillarbox -> fitHeight\n              const scaleX = b / a;\n              return { winX: 1.0 / scaleX, winY: 1.0 };\n            } else {\n              // canvas is taller → letterbox -> fitWidth\n              const scaleY = a / b;\n              return { winX: 1.0, winY: 1.0 / scaleY };\n            }\n          }\n\n          // default: cover\n          if (a >= b) {\n            // canvas wider → crop vertically (fitWidth)\n            const scaleY = a / b;\n            return { winX: 1.0, winY: 1.0 / scaleY };\n          } else {\n            // canvas taller → crop horizontally (fitHeight)\n            const scaleX = b / a;\n            return { winX: 1.0 / scaleX, winY: 1.0 };\n          }\n        }\n\n        /** @param {WebGLRenderingContext} gl */\n        function createProgram(gl, vsSource, fsSource) {\n          const vs = gl.createShader(gl.VERTEX_SHADER);\n          gl.shaderSource(vs, vsSource);\n          gl.compileShader(vs);\n          if (!gl.getShaderParameter(vs, gl.COMPILE_STATUS)) {\n            throw new Error(\"VS compile: \" + gl.getShaderInfoLog(vs));\n          }\n          const fs = gl.createShader(gl.FRAGMENT_SHADER);\n          gl.shaderSource(fs, fsSource);\n          gl.compileShader(fs);\n          if (!gl.getShaderParameter(fs, gl.COMPILE_STATUS)) {\n            throw new Error(\"FS compile: \" + gl.getShaderInfoLog(fs));\n          }\n          const prg = gl.createProgram();\n          gl.attachShader(prg, vs);\n          gl.attachShader(prg, fs);\n          gl.linkProgram(prg);\n          if (!gl.getProgramParameter(prg, gl.LINK_STATUS)) {\n            throw new Error(\"Link: \" + gl.getProgramInfoLog(prg));\n          }\n          return prg;\n        }\n\n        const VS = `\nattribute vec2 a_position;\nattribute vec2 a_texcoord;\nvarying vec2 v_uv;\nvoid main() {\n  v_uv = a_texcoord;\n  gl_Position = vec4(a_position, 0.0, 1.0);\n}`;\n\n        const FS_SPLIT = `\nprecision mediump float;\nuniform sampler2D u_image;\nuniform int u_channel;\nvarying vec2 v_uv;\nvoid main(){\n  vec4 c = texture2D(u_image, v_uv);\n  float v = (u_channel==0)?c.r:((u_channel==1)?c.g:c.b);\n  gl_FragColor = vec4(v, v, v, 1.0);\n}`;\n\n        const FS_COMPOSE = `\nprecision mediump float;\nuniform sampler2D u_r;\nuniform sampler2D u_g;\nuniform sampler2D u_b;\nuniform vec3 u_rColor;\nuniform vec3 u_gColor;\nuniform vec3 u_bColor;\nuniform float u_winX;\nuniform float u_winY;\nuniform float u_offX;\nuniform float u_offY;\nuniform float u_alpha;\nuniform float u_keyLuma;\nuniform float u_keyFeather;\nvarying vec2 v_uv;\nvoid main(){\n  vec2 uv = vec2(v_uv.x * u_winX + u_offX, v_uv.y * u_winY + u_offY);\n  float inside = step(0.0, uv.x) * step(uv.x, 1.0) * step(0.0, uv.y) * step(uv.y, 1.0);\n  float r = texture2D(u_r, uv).r;\n  float g = texture2D(u_g, uv).r;\n  float b = texture2D(u_b, uv).r;\n  vec3 color = r * u_rColor + g * u_gColor + b * u_bColor;\n  float luma = dot(color, vec3(0.299, 0.587, 0.114));\n  float a = smoothstep(u_keyLuma - u_keyFeather, u_keyLuma + u_keyFeather, luma) * u_alpha * inside;\n  if (a <= 0.0001) { discard; }\n  gl_FragColor = vec4(color * a, a);\n}`;\n\n        const gl = canvas.getContext(\"webgl\", {\n          alpha: true,\n          premultipliedAlpha: false,\n          antialias: true,\n        });\n        if (!gl) return;\n\n        const prgSplit = createProgram(gl, VS, FS_SPLIT);\n        const prgCompose = createProgram(gl, VS, FS_COMPOSE);\n\n        const posLocS = gl.getAttribLocation(prgSplit, \"a_position\");\n        const uvLocS = gl.getAttribLocation(prgSplit, \"a_texcoord\");\n        const posLocC = gl.getAttribLocation(prgCompose, \"a_position\");\n        const uvLocC = gl.getAttribLocation(prgCompose, \"a_texcoord\");\n        const posBuf = gl.createBuffer();\n        gl.bindBuffer(gl.ARRAY_BUFFER, posBuf);\n        gl.bufferData(\n          gl.ARRAY_BUFFER,\n          new Float32Array([-1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, 1]),\n          gl.STATIC_DRAW,\n        );\n        const uvBuf = gl.createBuffer();\n        gl.bindBuffer(gl.ARRAY_BUFFER, uvBuf);\n        gl.bufferData(\n          gl.ARRAY_BUFFER,\n          new Float32Array([0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1, 1]),\n          gl.STATIC_DRAW,\n        );\n\n        function enableAttribs(posLoc, uvLoc) {\n          gl.bindBuffer(gl.ARRAY_BUFFER, posBuf);\n          gl.enableVertexAttribArray(posLoc);\n          gl.vertexAttribPointer(posLoc, 2, gl.FLOAT, false, 0, 0);\n          gl.bindBuffer(gl.ARRAY_BUFFER, uvBuf);\n          gl.enableVertexAttribArray(uvLoc);\n          gl.vertexAttribPointer(uvLoc, 2, gl.FLOAT, false, 0, 0);\n        }\n\n        function createTextureFromImage(img) {\n          const tex = gl.createTexture();\n          gl.bindTexture(gl.TEXTURE_2D, tex);\n          gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);\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          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.texImage2D(\n            gl.TEXTURE_2D,\n            0,\n            gl.RGBA,\n            gl.RGBA,\n            gl.UNSIGNED_BYTE,\n            img,\n          );\n          return tex;\n        }\n\n        function createFboTexture(w, h) {\n          const tex = gl.createTexture();\n          gl.bindTexture(gl.TEXTURE_2D, tex);\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          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.texImage2D(\n            gl.TEXTURE_2D,\n            0,\n            gl.RGBA,\n            Math.max(2, w),\n            Math.max(2, h),\n            0,\n            gl.RGBA,\n            gl.UNSIGNED_BYTE,\n            null,\n          );\n          const fb = gl.createFramebuffer();\n          gl.bindFramebuffer(gl.FRAMEBUFFER, fb);\n          gl.framebufferTexture2D(\n            gl.FRAMEBUFFER,\n            gl.COLOR_ATTACHMENT0,\n            gl.TEXTURE_2D,\n            tex,\n            0,\n          );\n          gl.bindFramebuffer(gl.FRAMEBUFFER, null);\n          return { tex, fb };\n        }\n\n        function resize() {\n          const width = Math.max(\n            2,\n            Math.floor(canvas.clientWidth * window.devicePixelRatio),\n          );\n          const height = Math.max(\n            2,\n            Math.floor(canvas.clientHeight * window.devicePixelRatio),\n          );\n          if (canvas.width !== width || canvas.height !== height) {\n            canvas.width = width;\n            canvas.height = height;\n          }\n          gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);\n        }\n\n        // Stateful image + render pipeline that can update when filename changes\n        let texImage = null;\n        let rRT = null;\n        let gRT = null;\n        let bRT = null;\n        const img = new Image();\n        img.crossOrigin = \"\";\n\n        function drawSplit(rt, channel) {\n          if (!rt || !texImage) return;\n          gl.bindFramebuffer(gl.FRAMEBUFFER, rt.fb);\n          gl.useProgram(prgSplit);\n          enableAttribs(posLocS, uvLocS);\n          gl.activeTexture(gl.TEXTURE0);\n          gl.bindTexture(gl.TEXTURE_2D, texImage);\n          gl.uniform1i(gl.getUniformLocation(prgSplit, \"u_image\"), 0);\n          gl.uniform1i(gl.getUniformLocation(prgSplit, \"u_channel\"), channel);\n          gl.viewport(0, 0, img.width, img.height);\n          gl.clearColor(0, 0, 0, 0);\n          gl.clear(gl.COLOR_BUFFER_BIT);\n          gl.drawArrays(gl.TRIANGLES, 0, 6);\n          gl.bindFramebuffer(gl.FRAMEBUFFER, null);\n        }\n\n        function render() {\n          if (!texImage || !rRT || !gRT || !bRT) return;\n          sanitizeCfg();\n          resize();\n          drawSplit(rRT, 0);\n          drawSplit(gRT, 1);\n          drawSplit(bRT, 2);\n\n          const [rCol, gCol, bCol] = resolveRgbColors();\n          gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);\n          const canvasAspect = Math.max(\n            0.0001,\n            gl.drawingBufferWidth / Math.max(1, gl.drawingBufferHeight),\n          );\n          const imgAspect = Math.max(\n            0.0001,\n            img.width / Math.max(1, img.height),\n          );\n          const eff = resolveAlignFit();\n          const win = computeWindow(canvasAspect, imgAspect, eff.fit);\n          const ax = alignXCode(eff.hAlign);\n          const ay = alignYCode(eff.vAlign);\n          const offX =\n            ax === 0 ? 0.0 : ax === 1 ? (1.0 - win.winX) * 0.5 : 1.0 - win.winX;\n          const offY =\n            ay === 0 ? 1.0 - win.winY : ay === 1 ? (1.0 - win.winY) * 0.5 : 0.0;\n          gl.useProgram(prgCompose);\n          enableAttribs(posLocC, uvLocC);\n          gl.activeTexture(gl.TEXTURE0);\n          gl.bindTexture(gl.TEXTURE_2D, rRT.tex);\n          gl.uniform1i(gl.getUniformLocation(prgCompose, \"u_r\"), 0);\n          gl.activeTexture(gl.TEXTURE1);\n          gl.bindTexture(gl.TEXTURE_2D, gRT.tex);\n          gl.uniform1i(gl.getUniformLocation(prgCompose, \"u_g\"), 1);\n          gl.activeTexture(gl.TEXTURE2);\n          gl.bindTexture(gl.TEXTURE_2D, bRT.tex);\n          gl.uniform1i(gl.getUniformLocation(prgCompose, \"u_b\"), 2);\n          gl.uniform3fv(gl.getUniformLocation(prgCompose, \"u_rColor\"), rCol);\n          gl.uniform3fv(gl.getUniformLocation(prgCompose, \"u_gColor\"), gCol);\n          gl.uniform3fv(gl.getUniformLocation(prgCompose, \"u_bColor\"), bCol);\n          gl.uniform1f(gl.getUniformLocation(prgCompose, \"u_winX\"), win.winX);\n          gl.uniform1f(gl.getUniformLocation(prgCompose, \"u_winY\"), win.winY);\n          gl.uniform1f(gl.getUniformLocation(prgCompose, \"u_offX\"), offX);\n          gl.uniform1f(gl.getUniformLocation(prgCompose, \"u_offY\"), offY);\n          gl.uniform1f(\n            gl.getUniformLocation(prgCompose, \"u_alpha\"),\n            cfg.opacity,\n          );\n          gl.uniform1f(gl.getUniformLocation(prgCompose, \"u_keyLuma\"), 0.08);\n          gl.uniform1f(gl.getUniformLocation(prgCompose, \"u_keyFeather\"), 0.04);\n          gl.clearColor(0, 0, 0, 0);\n          gl.clear(gl.COLOR_BUFFER_BIT);\n          gl.drawArrays(gl.TRIANGLES, 0, 6);\n        }\n\n        function loadImageAndSetup(src) {\n          img.onload = () => {\n            texImage = createTextureFromImage(img);\n            rRT = createFboTexture(img.width, img.height);\n            gRT = createFboTexture(img.width, img.height);\n            bRT = createFboTexture(img.width, img.height);\n            render();\n          };\n          img.src = src;\n        }\n\n        (function setupObservers() {\n          let lastKey = \"\";\n          let lastUrl = \"\";\n          let isVisible = true;\n          function keyOf(c) {\n            return (\n              c[0].toFixed(3) + \",\" + c[1].toFixed(3) + \",\" + c[2].toFixed(3)\n            );\n          }\n          function maybeRefresh() {\n            // Check filename via CSS var first\n            const fname = resolveFilename();\n            const url = urlForFilename(fname);\n            if (url !== lastUrl) {\n              lastUrl = url;\n              if (!url) {\n                // Disabled: clear resources and canvas, skip loading\n                texImage = null;\n                rRT = null;\n                gRT = null;\n                bRT = null;\n                resize();\n                gl.clearColor(0, 0, 0, 0);\n                gl.clear(gl.COLOR_BUFFER_BIT);\n              } else {\n                loadImageAndSetup(url);\n              }\n              return;\n            }\n            // Otherwise, check color/opacity diff and re-render if changed\n            const cols = resolveRgbColors();\n            const op = Math.max(\n              0.0,\n              Math.min(1.0, parseFloat(String(cfg.opacity ?? 1)) || 1),\n            );\n            const eff = resolveAlignFit();\n            const k =\n              keyOf(cols[0]) +\n              \"|\" +\n              keyOf(cols[1]) +\n              \"|\" +\n              keyOf(cols[2]) +\n              \"|\" +\n              op.toFixed(3) +\n              \"|\" +\n              eff.hAlign +\n              \"|\" +\n              eff.vAlign +\n              \"|\" +\n              eff.fit;\n            if (k !== lastKey) {\n              lastKey = k;\n              if (isVisible) render();\n            }\n          }\n          // Initialize\n          try {\n            const io = new IntersectionObserver(\n              (entries) => {\n                if (!entries || !entries.length) return;\n                isVisible = !!entries[0].isIntersecting;\n                if (isVisible) maybeRefresh();\n              },\n              { threshold: 0.01 },\n            );\n            if (root) io.observe(root);\n          } catch {}\n          try {\n            const mo = new MutationObserver(() => {\n              // Re-read cfg from inline script in case props changed via Astro rerender\n              try {\n                const base = JSON.parse(dataEl?.textContent || \"{}\") || {};\n                cfg = Object.assign(cfg, base);\n                sanitizeCfg();\n              } catch {}\n              if (isVisible) maybeRefresh();\n            });\n            if (root)\n              mo.observe(root, {\n                attributes: true,\n                attributeFilter: [\"style\", \"class\"],\n              });\n            mo.observe(document.documentElement, {\n              attributes: true,\n              attributeFilter: [\"style\", \"class\"],\n            });\n            if (document.body)\n              mo.observe(document.body, {\n                attributes: true,\n                attributeFilter: [\"style\", \"class\"],\n              });\n            const moHead = new MutationObserver(() => {\n              if (isVisible) maybeRefresh();\n            });\n            moHead.observe(document.head, {\n              childList: true,\n              subtree: true,\n              characterData: true,\n              attributes: true,\n            });\n          } catch {}\n          window.addEventListener(\n            \"resize\",\n            () => {\n              if (isVisible) render();\n            },\n            { passive: true },\n          );\n          // Kick off first load\n          maybeRefresh();\n          // Fallback polling in case style text updates are not caught\n          try {\n            setInterval(() => {\n              if (isVisible) maybeRefresh();\n            }, 300);\n          } catch {}\n        })();\n        })();\n      }\n    </script>\n  </div>\n)}\n"
    }
  ],
  "category": "ui"
}