{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"react-shader-toy","title":"React Shader Toy","description":"A shadertoy-like shader renderer component for React.","files":[{"path":"components/agents-ui/react-shader-toy.tsx","content":"/**\n * @license\n *\n * MIT License\n *\n * Copyright (c) 2018 Morgan Villedieu\n * Copyright (c) 2023 Rysana, Inc. (forked from the above)\n * Copyright (c) 2026 LiveKit, Inc. (forked from the above)\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to deal\n * in the Software without restriction, including without limitation the rights\n * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n * copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n * SOFTWARE.\n */\n\nimport React, { useEffect, useRef, type ComponentPropsWithoutRef } from 'react';\n\nconst PRECISIONS = ['lowp', 'mediump', 'highp'];\nconst FS_MAIN_SHADER = `\\nvoid main(void){\n    vec4 color = vec4(0.0,0.0,0.0,1.0);\n    mainImage( color, gl_FragCoord.xy );\n    gl_FragColor = color;\n}`;\nconst BASIC_FS = `void mainImage( out vec4 fragColor, in vec2 fragCoord ) {\n    vec2 uv = fragCoord/iResolution.xy;\n    vec3 col = 0.5 + 0.5*cos(iTime+uv.xyx+vec3(0,2,4));\n    fragColor = vec4(col,1.0);\n}`;\nconst BASIC_VS = `attribute vec3 aVertexPosition;\nvoid main(void) {\n    gl_Position = vec4(aVertexPosition, 1.0);\n}`;\nconst UNIFORM_TIME = 'iTime';\nconst UNIFORM_TIMEDELTA = 'iTimeDelta';\nconst UNIFORM_DATE = 'iDate';\nconst UNIFORM_FRAME = 'iFrame';\nconst UNIFORM_MOUSE = 'iMouse';\nconst UNIFORM_RESOLUTION = 'iResolution';\nconst UNIFORM_CHANNEL = 'iChannel';\nconst UNIFORM_CHANNELRESOLUTION = 'iChannelResolution';\nconst UNIFORM_DEVICEORIENTATION = 'iDeviceOrientation';\n\ntype Vector4<T = number> = [T, T, T, T];\ntype UniformType = keyof Uniforms;\n\nfunction isMatrixType(t: string, v: number[] | number): v is number[] {\n  return t.includes('Matrix') && Array.isArray(v);\n}\nfunction isVectorListType(t: string, v: number[] | number): v is number[] {\n  return t.includes('v') && Array.isArray(v) && v.length > Number.parseInt(t.charAt(0));\n}\nfunction isVectorType(t: string, v: number[] | number): v is Vector4 {\n  return !t.includes('v') && Array.isArray(v) && v.length > Number.parseInt(t.charAt(0));\n}\nconst processUniform = <T extends UniformType>(\n  gl: WebGLRenderingContext,\n  location: WebGLUniformLocation,\n  t: T,\n  value: number | number[],\n) => {\n  if (isVectorType(t, value)) {\n    switch (t) {\n      case '2f':\n        return gl.uniform2f(location, value[0], value[1]);\n      case '3f':\n        return gl.uniform3f(location, value[0], value[1], value[2]);\n      case '4f':\n        return gl.uniform4f(location, value[0], value[1], value[2], value[3]);\n      case '2i':\n        return gl.uniform2i(location, value[0], value[1]);\n      case '3i':\n        return gl.uniform3i(location, value[0], value[1], value[2]);\n      case '4i':\n        return gl.uniform4i(location, value[0], value[1], value[2], value[3]);\n    }\n  }\n  if (typeof value === 'number') {\n    switch (t) {\n      case '1i':\n        return gl.uniform1i(location, value);\n      default:\n        return gl.uniform1f(location, value);\n    }\n  }\n  switch (t) {\n    case '1iv':\n      return gl.uniform1iv(location, value);\n    case '2iv':\n      return gl.uniform2iv(location, value);\n    case '3iv':\n      return gl.uniform3iv(location, value);\n    case '4iv':\n      return gl.uniform4iv(location, value);\n    case '1fv':\n      return gl.uniform1fv(location, value);\n    case '2fv':\n      return gl.uniform2fv(location, value);\n    case '3fv':\n      return gl.uniform3fv(location, value);\n    case '4fv':\n      return gl.uniform4fv(location, value);\n    case 'Matrix2fv':\n      return gl.uniformMatrix2fv(location, false, value);\n    case 'Matrix3fv':\n      return gl.uniformMatrix3fv(location, false, value);\n    case 'Matrix4fv':\n      return gl.uniformMatrix4fv(location, false, value);\n  }\n};\n\nconst uniformTypeToGLSLType = (t: string) => {\n  switch (t) {\n    case '1f':\n      return 'float';\n    case '2f':\n      return 'vec2';\n    case '3f':\n      return 'vec3';\n    case '4f':\n      return 'vec4';\n    case '1i':\n      return 'int';\n    case '2i':\n      return 'ivec2';\n    case '3i':\n      return 'ivec3';\n    case '4i':\n      return 'ivec4';\n    case '1iv':\n      return 'int';\n    case '2iv':\n      return 'ivec2';\n    case '3iv':\n      return 'ivec3';\n    case '4iv':\n      return 'ivec4';\n    case '1fv':\n      return 'float';\n    case '2fv':\n      return 'vec2';\n    case '3fv':\n      return 'vec3';\n    case '4fv':\n      return 'vec4';\n    case 'Matrix2fv':\n      return 'mat2';\n    case 'Matrix3fv':\n      return 'mat3';\n    case 'Matrix4fv':\n      return 'mat4';\n    default:\n      console.error(\n        log(`The uniform type \"${t}\" is not valid, please make sure your uniform type is valid`),\n      );\n  }\n};\n\nconst LinearFilter = 9729;\nconst NearestFilter = 9728;\nconst LinearMipMapLinearFilter = 9987;\nconst ClampToEdgeWrapping = 33071;\nconst RepeatWrapping = 10497;\n\nclass Texture {\n  gl: WebGLRenderingContext;\n  url?: string;\n  wrapS?: number;\n  wrapT?: number;\n  minFilter?: number;\n  magFilter?: number;\n  source?: HTMLImageElement | HTMLVideoElement;\n  pow2canvas?: HTMLCanvasElement;\n  isLoaded = false;\n  isVideo = false;\n  flipY = -1;\n  width = 0;\n  height = 0;\n  _webglTexture: WebGLTexture | null = null;\n  constructor(gl: WebGLRenderingContext) {\n    this.gl = gl;\n  }\n  updateTexture = (texture: WebGLTexture, video: HTMLVideoElement, flipY: number) => {\n    const { gl } = this;\n    const level = 0;\n    const internalFormat = gl.RGBA;\n    const srcFormat = gl.RGBA;\n    const srcType = gl.UNSIGNED_BYTE;\n    gl.bindTexture(gl.TEXTURE_2D, texture);\n    gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, flipY);\n    gl.texImage2D(gl.TEXTURE_2D, level, internalFormat, srcFormat, srcType, video);\n  };\n  setupVideo = (url: string) => {\n    const video = document.createElement('video');\n    let playing = false;\n    let timeupdate = false;\n    video.autoplay = true;\n    video.muted = true;\n    video.loop = true;\n    video.crossOrigin = 'anonymous';\n    const checkReady = () => {\n      if (playing && timeupdate) {\n        this.isLoaded = true;\n      }\n    };\n    video.addEventListener(\n      'playing',\n      () => {\n        playing = true;\n        this.width = video.videoWidth || 0;\n        this.height = video.videoHeight || 0;\n        checkReady();\n      },\n      true,\n    );\n    video.addEventListener(\n      'timeupdate',\n      () => {\n        timeupdate = true;\n        checkReady();\n      },\n      true,\n    );\n    video.src = url;\n    return video;\n  };\n  makePowerOf2 = <T extends HTMLCanvasElement | HTMLImageElement | ImageBitmap>(image: T): T => {\n    if (\n      image instanceof HTMLImageElement ||\n      image instanceof HTMLCanvasElement ||\n      image instanceof ImageBitmap\n    ) {\n      if (this.pow2canvas === undefined) this.pow2canvas = document.createElement('canvas');\n      this.pow2canvas.width = 2 ** Math.floor(Math.log(image.width) / Math.LN2);\n      this.pow2canvas.height = 2 ** Math.floor(Math.log(image.height) / Math.LN2);\n      const context = this.pow2canvas.getContext('2d');\n      context?.drawImage(image, 0, 0, this.pow2canvas.width, this.pow2canvas.height);\n      console.warn(\n        log(\n          `Image is not power of two ${image.width} x ${image.height}. Resized to ${this.pow2canvas.width} x ${this.pow2canvas.height};`,\n        ),\n      );\n      return this.pow2canvas as T;\n    }\n    return image;\n  };\n  load = async (textureArgs: TextureParams) => {\n    const { gl } = this;\n    const { url, wrapS, wrapT, minFilter, magFilter, flipY = -1 }: TextureParams = textureArgs;\n    if (!url) {\n      return Promise.reject(\n        new Error(\n          log('Missing url, please make sure to pass the url of your texture { url: ... }'),\n        ),\n      );\n    }\n    const isImage = /(\\.jpg|\\.jpeg|\\.png|\\.gif|\\.bmp)$/i.exec(url);\n    const isVideo = /(\\.mp4|\\.3gp|\\.webm|\\.ogv)$/i.exec(url);\n    if (isImage === null && isVideo === null) {\n      return Promise.reject(\n        new Error(log(`Please upload a video or an image with a valid format (url: ${url})`)),\n      );\n    }\n    Object.assign(this, { url, wrapS, wrapT, minFilter, magFilter, flipY });\n    const level = 0;\n    const internalFormat = gl.RGBA;\n    const width = 1;\n    const height = 1;\n    const border = 0;\n    const srcFormat = gl.RGBA;\n    const srcType = gl.UNSIGNED_BYTE;\n    const pixel = new Uint8Array([255, 255, 255, 0]);\n    const texture = gl.createTexture();\n    gl.bindTexture(gl.TEXTURE_2D, texture);\n    gl.texImage2D(\n      gl.TEXTURE_2D,\n      level,\n      internalFormat,\n      width,\n      height,\n      border,\n      srcFormat,\n      srcType,\n      pixel,\n    );\n    if (isVideo) {\n      const video = this.setupVideo(url);\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      this._webglTexture = texture;\n      this.source = video;\n      this.isVideo = true;\n      return video.play().then(() => this);\n    }\n    async function loadImage() {\n      return new Promise((resolve, reject) => {\n        const image = new Image();\n        image.crossOrigin = 'anonymous';\n        image.onload = () => {\n          resolve(image);\n        };\n        image.onerror = () => {\n          reject(new Error(log(`failed loading url: ${url}`)));\n        };\n        image.src = url ?? '';\n      });\n    }\n    let image = (await loadImage()) as HTMLImageElement;\n    let isPowerOf2 =\n      (image.width & (image.width - 1)) === 0 && (image.height & (image.height - 1)) === 0;\n    if (\n      (textureArgs.wrapS !== ClampToEdgeWrapping ||\n        textureArgs.wrapT !== ClampToEdgeWrapping ||\n        (textureArgs.minFilter !== NearestFilter && textureArgs.minFilter !== LinearFilter)) &&\n      !isPowerOf2\n    ) {\n      image = this.makePowerOf2(image);\n      isPowerOf2 = true;\n    }\n    gl.bindTexture(gl.TEXTURE_2D, texture);\n    gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, flipY);\n    gl.texImage2D(gl.TEXTURE_2D, level, internalFormat, srcFormat, srcType, image);\n    if (\n      isPowerOf2 &&\n      textureArgs.minFilter !== NearestFilter &&\n      textureArgs.minFilter !== LinearFilter\n    ) {\n      gl.generateMipmap(gl.TEXTURE_2D);\n    }\n    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, this.wrapS || RepeatWrapping);\n    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, this.wrapT || RepeatWrapping);\n    gl.texParameteri(\n      gl.TEXTURE_2D,\n      gl.TEXTURE_MIN_FILTER,\n      this.minFilter || LinearMipMapLinearFilter,\n    );\n    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, this.magFilter || LinearFilter);\n    this._webglTexture = texture;\n    this.source = image;\n    this.isVideo = false;\n    this.isLoaded = true;\n    this.width = image.width || 0;\n    this.height = image.height || 0;\n    return this;\n  };\n}\n\nconst log = (text: string) => `react-shaders: ${text}`;\n\nconst latestPointerClientCoords = (e: MouseEvent | TouchEvent) => {\n  if ('changedTouches' in e) {\n    const t = e.changedTouches[0];\n    return [t?.clientX ?? 0, t?.clientY ?? 0];\n  }\n  return [e.clientX, e.clientY];\n};\n\nconst lerpVal = (v0: number, v1: number, t: number) => v0 * (1 - t) + v1 * t;\nconst insertStringAtIndex = (currentString: string, string: string, index: number) =>\n  index > 0\n    ? currentString.substring(0, index) +\n      string +\n      currentString.substring(index, currentString.length)\n    : string + currentString;\n\ntype Uniform = { type: string; value: number[] | number };\ntype Uniforms = Record<string, Uniform>;\ntype TextureParams = {\n  url: string;\n  wrapS?: number;\n  wrapT?: number;\n  minFilter?: number;\n  magFilter?: number;\n  flipY?: number;\n};\n\nexport interface ReactShaderToyProps {\n  /** Fragment shader GLSL code. */\n  fs: string;\n\n  /** Vertex shader GLSL code. */\n  vs?: string;\n\n  /**\n   * Textures to be passed to the shader. Textures need to be squared or will be\n   * automatically resized.\n   *\n   * Options default to:\n   *\n   * ```js\n   * {\n   *   minFilter: LinearMipMapLinearFilter,\n   *   magFilter: LinearFilter,\n   *   wrapS: RepeatWrapping,\n   *   wrapT: RepeatWrapping,\n   * }\n   * ```\n   *\n   * See [textures in the docs](https://rysana.com/docs/react-shaders#textures)\n   * for details.\n   */\n  textures?: TextureParams[];\n\n  /**\n   * Custom uniforms to be passed to the shader.\n   *\n   * See [custom uniforms in the\n   * docs](https://rysana.com/docs/react-shaders#custom-uniforms) for details.\n   */\n  uniforms?: Uniforms;\n\n  /**\n   * Color used when clearing the canvas.\n   *\n   * See [the WebGL\n   * docs](https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/clearColor)\n   * for details.\n   */\n  clearColor?: Vector4;\n\n  /**\n   * GLSL precision qualifier. Defaults to `'highp'`. Balance between\n   * performance and quality.\n   */\n  precision?: 'highp' | 'lowp' | 'mediump';\n\n  /** Custom inline style for canvas. */\n  style?: React.CSSProperties;\n\n  /** Customize WebGL context attributes. See [the WebGL docs](https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/getContextAttributes) for details. */\n  contextAttributes?: Record<string, unknown>;\n\n  /** Lerp value for `iMouse` built-in uniform. Must be between 0 and 1. */\n  lerp?: number;\n\n  /** Device pixel ratio. */\n  devicePixelRatio?: number;\n\n  /**\n   * Callback for when the textures are done loading. Useful if you want to do\n   * something like e.g. hide the canvas until textures are done loading.\n   */\n  onDoneLoadingTextures?: () => void;\n\n  /** Custom callback to handle errors. Defaults to `console.error`. */\n  onError?: (error: string) => void;\n\n  /** Custom callback to handle warnings. Defaults to `console.warn`. */\n  onWarning?: (warning: string) => void;\n\n  /**\n   * When true, the animation loop runs even when the canvas is not visible in the viewport.\n   * When false (default), animation runs only while visible (uses Intersection Observer),\n   * reducing CPU/GPU usage when the shader is off-screen.\n   */\n  animateWhenNotVisible?: boolean;\n}\n\nexport function ReactShaderToy({\n  fs,\n  vs = BASIC_VS,\n  textures = [],\n  uniforms: propUniforms,\n  clearColor = [0, 0, 0, 1],\n  precision = 'highp',\n  style,\n  contextAttributes = {},\n  lerp = 1,\n  devicePixelRatio = 1,\n  onDoneLoadingTextures,\n  onError = console.error,\n  onWarning = console.warn,\n  animateWhenNotVisible = false,\n  ...canvasProps\n}: ReactShaderToyProps & ComponentPropsWithoutRef<'canvas'>) {\n  // Refs for WebGL state\n  const canvasRef = useRef<HTMLCanvasElement>(null);\n  const glRef = useRef<WebGLRenderingContext | null>(null);\n  const squareVerticesBufferRef = useRef<WebGLBuffer | null>(null);\n  const shaderProgramRef = useRef<WebGLProgram | null>(null);\n  const vertexPositionAttributeRef = useRef<number | undefined>(undefined);\n  const animFrameIdRef = useRef<number | undefined>(undefined);\n  const initFrameIdRef = useRef<number | undefined>(undefined);\n  const isVisibleRef = useRef(true);\n  const animateWhenNotVisibleRef = useRef(animateWhenNotVisible);\n  const mousedownRef = useRef(false);\n  const canvasPositionRef = useRef<DOMRect | undefined>(undefined);\n  const timerRef = useRef(0);\n  const lastMouseArrRef = useRef<number[]>([0, 0]);\n  const texturesArrRef = useRef<Texture[]>([]);\n  const lastTimeRef = useRef(0);\n  const resizeObserverRef = useRef<ResizeObserver | undefined>(undefined);\n  const uniformsRef = useRef<\n    Record<\n      string,\n      { type: string; isNeeded: boolean; value?: number[] | number; arraySize?: string }\n    >\n  >({\n    [UNIFORM_TIME]: { type: 'float', isNeeded: false, value: 0 },\n    [UNIFORM_TIMEDELTA]: { type: 'float', isNeeded: false, value: 0 },\n    [UNIFORM_DATE]: { type: 'vec4', isNeeded: false, value: [0, 0, 0, 0] },\n    [UNIFORM_MOUSE]: { type: 'vec4', isNeeded: false, value: [0, 0, 0, 0] },\n    [UNIFORM_RESOLUTION]: { type: 'vec2', isNeeded: false, value: [0, 0] },\n    [UNIFORM_FRAME]: { type: 'int', isNeeded: false, value: 0 },\n    [UNIFORM_DEVICEORIENTATION]: { type: 'vec4', isNeeded: false, value: [0, 0, 0, 0] },\n  });\n  const propsUniformsRef = useRef<Uniforms | undefined>(propUniforms);\n\n  const setupChannelRes = (texture: TextureParams | Texture, id: number) => {\n    const width = 'width' in texture ? (texture.width ?? 0) : 0;\n    const height = 'height' in texture ? (texture.height ?? 0) : 0;\n    const channelResUniform = uniformsRef.current.iChannelResolution;\n    if (!channelResUniform) return;\n    const channelResValue = Array.isArray(channelResUniform.value)\n      ? channelResUniform.value\n      : (channelResUniform.value = []);\n    channelResValue[id * 3] = width * devicePixelRatio;\n    channelResValue[id * 3 + 1] = height * devicePixelRatio;\n    channelResValue[id * 3 + 2] = 0;\n  };\n\n  const initWebGL = () => {\n    if (!canvasRef.current) return;\n    glRef.current = (canvasRef.current.getContext('webgl', contextAttributes) ||\n      canvasRef.current.getContext(\n        'experimental-webgl',\n        contextAttributes,\n      )) as WebGLRenderingContext | null;\n    glRef.current?.getExtension('OES_standard_derivatives');\n    glRef.current?.getExtension('EXT_shader_texture_lod');\n  };\n\n  const initBuffers = () => {\n    const gl = glRef.current;\n    squareVerticesBufferRef.current = gl?.createBuffer() ?? null;\n    gl?.bindBuffer(gl.ARRAY_BUFFER, squareVerticesBufferRef.current);\n    const vertices = [1.0, 1.0, 0.0, -1.0, 1.0, 0.0, 1.0, -1.0, 0.0, -1.0, -1.0, 0.0];\n    gl?.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);\n  };\n\n  const onDeviceOrientationChange = ({ alpha, beta, gamma }: DeviceOrientationEvent) => {\n    uniformsRef.current.iDeviceOrientation!.value = [\n      alpha ?? 0,\n      beta ?? 0,\n      gamma ?? 0,\n      window.orientation ?? 0,\n    ];\n  };\n\n  const mouseDown = (e: MouseEvent | TouchEvent) => {\n    const [clientX = 0, clientY = 0] = latestPointerClientCoords(e);\n    const mouseX = clientX - (canvasPositionRef.current?.left ?? 0) - window.pageXOffset;\n    const mouseY =\n      (canvasPositionRef.current?.height ?? 0) -\n      clientY -\n      (canvasPositionRef.current?.top ?? 0) -\n      window.pageYOffset;\n    mousedownRef.current = true;\n    const mouseValue = Array.isArray(uniformsRef.current.iMouse?.value)\n      ? uniformsRef.current.iMouse.value\n      : undefined;\n    if (mouseValue) {\n      mouseValue[2] = mouseX;\n      mouseValue[3] = mouseY;\n    }\n    lastMouseArrRef.current[0] = mouseX;\n    lastMouseArrRef.current[1] = mouseY;\n  };\n\n  const mouseMove = (e: MouseEvent | TouchEvent) => {\n    canvasPositionRef.current = canvasRef.current?.getBoundingClientRect();\n    const [clientX = 0, clientY = 0] = latestPointerClientCoords(e);\n    const mouseX = clientX - (canvasPositionRef.current?.left ?? 0);\n    const mouseY =\n      (canvasPositionRef.current?.height ?? 0) - clientY - (canvasPositionRef.current?.top ?? 0);\n    if (lerp !== 1) {\n      lastMouseArrRef.current[0] = mouseX;\n      lastMouseArrRef.current[1] = mouseY;\n    } else {\n      const mouseValue = Array.isArray(uniformsRef.current.iMouse?.value)\n        ? uniformsRef.current.iMouse.value\n        : undefined;\n      if (mouseValue) {\n        mouseValue[0] = mouseX;\n        mouseValue[1] = mouseY;\n      }\n    }\n  };\n\n  const mouseUp = () => {\n    const mouseValue = Array.isArray(uniformsRef.current.iMouse?.value)\n      ? uniformsRef.current.iMouse.value\n      : undefined;\n    if (mouseValue) {\n      mouseValue[2] = 0;\n      mouseValue[3] = 0;\n    }\n  };\n\n  const onResize = () => {\n    const gl = glRef.current;\n    if (!gl) return;\n    canvasPositionRef.current = canvasRef.current?.getBoundingClientRect();\n    // Force pixel ratio to be one to avoid expensive calculus on retina display.\n    const realToCSSPixels = devicePixelRatio;\n    const displayWidth = Math.floor((canvasPositionRef.current?.width ?? 1) * realToCSSPixels);\n    const displayHeight = Math.floor((canvasPositionRef.current?.height ?? 1) * realToCSSPixels);\n    gl.canvas.width = displayWidth;\n    gl.canvas.height = displayHeight;\n    if (uniformsRef.current.iResolution?.isNeeded && shaderProgramRef.current) {\n      const rUniform = gl.getUniformLocation(shaderProgramRef.current, UNIFORM_RESOLUTION);\n      gl.uniform2fv(rUniform, [gl.canvas.width, gl.canvas.height]);\n    }\n  };\n\n  const createShader = (type: number, shaderCodeAsText: string) => {\n    const gl = glRef.current;\n    if (!gl) return null;\n    const shader = gl.createShader(type);\n    if (!shader) return null;\n    gl.shaderSource(shader, shaderCodeAsText);\n    gl.compileShader(shader);\n    if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {\n      onWarning?.(log(`Error compiling the shader:\\n${shaderCodeAsText}`));\n      const compilationLog = gl.getShaderInfoLog(shader);\n      gl.deleteShader(shader);\n      onError?.(log(`Shader compiler log: ${compilationLog}`));\n    }\n    return shader;\n  };\n\n  const initShaders = (fragmentShader: string, vertexShader: string) => {\n    const gl = glRef.current;\n    if (!gl) return;\n    const fragmentShaderObj = createShader(gl.FRAGMENT_SHADER, fragmentShader);\n    const vertexShaderObj = createShader(gl.VERTEX_SHADER, vertexShader);\n    shaderProgramRef.current = gl.createProgram();\n    if (!shaderProgramRef.current || !vertexShaderObj || !fragmentShaderObj) return;\n    gl.attachShader(shaderProgramRef.current, vertexShaderObj);\n    gl.attachShader(shaderProgramRef.current, fragmentShaderObj);\n    gl.linkProgram(shaderProgramRef.current);\n    if (!gl.getProgramParameter(shaderProgramRef.current, gl.LINK_STATUS)) {\n      onError?.(\n        log(\n          `Unable to initialize the shader program: ${gl.getProgramInfoLog(shaderProgramRef.current)}`,\n        ),\n      );\n      return;\n    }\n    gl.useProgram(shaderProgramRef.current);\n    vertexPositionAttributeRef.current = gl.getAttribLocation(\n      shaderProgramRef.current,\n      'aVertexPosition',\n    );\n    gl.enableVertexAttribArray(vertexPositionAttributeRef.current);\n  };\n\n  const processCustomUniforms = () => {\n    if (propUniforms) {\n      for (const name of Object.keys(propUniforms)) {\n        const uniform = propUniforms[name];\n        if (!uniform) continue;\n        const { value, type } = uniform;\n        const glslType = uniformTypeToGLSLType(type);\n        if (!glslType) continue;\n        const tempObject: { arraySize?: string } = {};\n        if (isMatrixType(type, value)) {\n          const arrayLength = type.length;\n          const val = Number.parseInt(type.charAt(arrayLength - 3));\n          const numberOfMatrices = Math.floor(value.length / (val * val));\n          if (value.length > val * val) tempObject.arraySize = `[${numberOfMatrices}]`;\n        } else if (isVectorListType(type, value)) {\n          tempObject.arraySize = `[${Math.floor(value.length / Number.parseInt(type.charAt(0)))}]`;\n        }\n        uniformsRef.current[name] = { type: glslType, isNeeded: false, value, ...tempObject };\n      }\n    }\n  };\n\n  const processTextures = () => {\n    const gl = glRef.current;\n    if (!gl) return;\n    if (textures && textures.length > 0) {\n      uniformsRef.current[`${UNIFORM_CHANNELRESOLUTION}`] = {\n        type: 'vec3',\n        isNeeded: false,\n        arraySize: `[${textures.length}]`,\n        value: [],\n      };\n      const texturePromisesArr = textures.map((texture: TextureParams, id: number) => {\n        // Dynamically add textures uniforms.\n        uniformsRef.current[`${UNIFORM_CHANNEL}${id}`] = {\n          type: 'sampler2D',\n          isNeeded: false,\n        };\n        setupChannelRes(texture, id);\n        texturesArrRef.current[id] = new Texture(gl);\n        return texturesArrRef.current[id]?.load(texture).then((t: Texture) => {\n          setupChannelRes(t, id);\n        });\n      });\n      Promise.all(texturePromisesArr)\n        .then(() => {\n          if (onDoneLoadingTextures) onDoneLoadingTextures();\n        })\n        .catch((e) => {\n          onError?.(e);\n          if (onDoneLoadingTextures) onDoneLoadingTextures();\n        });\n    } else if (onDoneLoadingTextures) onDoneLoadingTextures();\n  };\n\n  const preProcessFragment = (fragment: string) => {\n    const isValidPrecision = PRECISIONS.includes(precision ?? 'highp');\n    const precisionString = `precision ${isValidPrecision ? precision : PRECISIONS[1]} float;\\n`;\n    if (!isValidPrecision) {\n      onWarning?.(\n        log(\n          `wrong precision type ${precision}, please make sure to pass one of a valid precision lowp, mediump, highp, by default you shader precision will be set to highp.`,\n        ),\n      );\n    }\n    let fragmentShader = precisionString\n      .concat(`#define DPR ${devicePixelRatio.toFixed(1)}\\n`)\n      .concat(fragment.replace(/texture\\(/g, 'texture2D('));\n    for (const uniform of Object.keys(uniformsRef.current)) {\n      if (fragment.includes(uniform)) {\n        const u = uniformsRef.current[uniform];\n        if (!u) continue;\n        fragmentShader = insertStringAtIndex(\n          fragmentShader,\n          `uniform ${u.type} ${uniform}${u.arraySize || ''}; \\n`,\n          fragmentShader.lastIndexOf(precisionString) + precisionString.length,\n        );\n        u.isNeeded = true;\n      }\n    }\n    const isShadertoy = fragment.includes('mainImage');\n    if (isShadertoy) fragmentShader = fragmentShader.concat(FS_MAIN_SHADER);\n    return fragmentShader;\n  };\n\n  const setUniforms = (timestamp: number) => {\n    const gl = glRef.current;\n    if (!gl || !shaderProgramRef.current) return;\n    const delta = lastTimeRef.current ? (timestamp - lastTimeRef.current) / 1000 : 0;\n    lastTimeRef.current = timestamp;\n    const propUniforms = propsUniformsRef.current;\n    if (propUniforms) {\n      for (const name of Object.keys(propUniforms)) {\n        const currentUniform = propUniforms[name];\n        if (!currentUniform) continue;\n        if (uniformsRef.current[name]?.isNeeded) {\n          if (!shaderProgramRef.current) return;\n          const customUniformLocation = gl.getUniformLocation(shaderProgramRef.current, name);\n          if (!customUniformLocation) return;\n          processUniform(\n            gl,\n            customUniformLocation,\n            currentUniform.type as UniformType,\n            currentUniform.value,\n          );\n        }\n      }\n    }\n    if (uniformsRef.current.iMouse?.isNeeded) {\n      const mouseUniform = gl.getUniformLocation(shaderProgramRef.current, UNIFORM_MOUSE);\n      gl.uniform4fv(mouseUniform, uniformsRef.current.iMouse.value as number[]);\n    }\n    if (uniformsRef.current.iChannelResolution?.isNeeded) {\n      const channelResUniform = gl.getUniformLocation(\n        shaderProgramRef.current,\n        UNIFORM_CHANNELRESOLUTION,\n      );\n      gl.uniform3fv(channelResUniform, uniformsRef.current.iChannelResolution.value as number[]);\n    }\n    if (uniformsRef.current.iDeviceOrientation?.isNeeded) {\n      const deviceOrientationUniform = gl.getUniformLocation(\n        shaderProgramRef.current,\n        UNIFORM_DEVICEORIENTATION,\n      );\n      gl.uniform4fv(\n        deviceOrientationUniform,\n        uniformsRef.current.iDeviceOrientation.value as number[],\n      );\n    }\n    if (uniformsRef.current.iTime?.isNeeded) {\n      const timeUniform = gl.getUniformLocation(shaderProgramRef.current, UNIFORM_TIME);\n      gl.uniform1f(timeUniform, (timerRef.current += delta));\n    }\n    if (uniformsRef.current.iTimeDelta?.isNeeded) {\n      const timeDeltaUniform = gl.getUniformLocation(shaderProgramRef.current, UNIFORM_TIMEDELTA);\n      gl.uniform1f(timeDeltaUniform, delta);\n    }\n    if (uniformsRef.current.iDate?.isNeeded) {\n      const d = new Date();\n      const month = d.getMonth() + 1;\n      const day = d.getDate();\n      const year = d.getFullYear();\n      const time =\n        d.getHours() * 60 * 60 + d.getMinutes() * 60 + d.getSeconds() + d.getMilliseconds() * 0.001;\n      const dateUniform = gl.getUniformLocation(shaderProgramRef.current, UNIFORM_DATE);\n      gl.uniform4fv(dateUniform, [year, month, day, time]);\n    }\n    if (uniformsRef.current.iFrame?.isNeeded) {\n      const timeDeltaUniform = gl.getUniformLocation(shaderProgramRef.current, UNIFORM_FRAME);\n      const frame = uniformsRef.current.iFrame.value as number;\n      gl.uniform1i(timeDeltaUniform, frame);\n      uniformsRef.current.iFrame.value = frame + 1;\n    }\n    if (texturesArrRef.current.length > 0) {\n      for (let index = 0; index < texturesArrRef.current.length; index++) {\n        const texture = texturesArrRef.current[index];\n        if (!texture) return;\n        const { isVideo, _webglTexture, source, flipY, isLoaded } = texture;\n        if (!isLoaded || !_webglTexture || !source) return;\n        if (uniformsRef.current[`iChannel${index}`]?.isNeeded) {\n          if (!shaderProgramRef.current) return;\n          const iChannel = gl.getUniformLocation(shaderProgramRef.current, `iChannel${index}`);\n          gl.activeTexture(gl.TEXTURE0 + index);\n          gl.bindTexture(gl.TEXTURE_2D, _webglTexture);\n          gl.uniform1i(iChannel, index);\n          if (isVideo) {\n            texture.updateTexture(_webglTexture, source as HTMLVideoElement, flipY);\n          }\n        }\n      }\n    }\n  };\n\n  const drawScene = (timestamp: number) => {\n    const gl = glRef.current;\n    if (!gl) return;\n    gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);\n    gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);\n    gl.bindBuffer(gl.ARRAY_BUFFER, squareVerticesBufferRef.current);\n    gl.vertexAttribPointer(vertexPositionAttributeRef.current ?? 0, 3, gl.FLOAT, false, 0, 0);\n    setUniforms(timestamp);\n    gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);\n    const mouseValue = uniformsRef.current.iMouse?.value;\n    if (uniformsRef.current.iMouse?.isNeeded && lerp !== 1 && Array.isArray(mouseValue)) {\n      const currentX = mouseValue[0] ?? 0;\n      const currentY = mouseValue[1] ?? 0;\n      mouseValue[0] = lerpVal(currentX, lastMouseArrRef.current[0] ?? 0, lerp);\n      mouseValue[1] = lerpVal(currentY, lastMouseArrRef.current[1] ?? 0, lerp);\n    }\n    if (animateWhenNotVisibleRef.current || isVisibleRef.current) {\n      animFrameIdRef.current = requestAnimationFrame(drawScene);\n    }\n  };\n\n  const addEventListeners = () => {\n    const options = { passive: true };\n    if (uniformsRef.current.iMouse?.isNeeded && canvasRef.current) {\n      canvasRef.current.addEventListener('mousemove', mouseMove, options);\n      canvasRef.current.addEventListener('mouseout', mouseUp, options);\n      canvasRef.current.addEventListener('mouseup', mouseUp, options);\n      canvasRef.current.addEventListener('mousedown', mouseDown, options);\n      canvasRef.current.addEventListener('touchmove', mouseMove, options);\n      canvasRef.current.addEventListener('touchend', mouseUp, options);\n      canvasRef.current.addEventListener('touchstart', mouseDown, options);\n    }\n    if (uniformsRef.current.iDeviceOrientation?.isNeeded) {\n      window.addEventListener('deviceorientation', onDeviceOrientationChange, options);\n    }\n    if (canvasRef.current) {\n      resizeObserverRef.current = new ResizeObserver(onResize);\n      resizeObserverRef.current.observe(canvasRef.current);\n      window.addEventListener('resize', onResize, options);\n    }\n  };\n\n  const removeEventListeners = () => {\n    const options = { passive: true } as EventListenerOptions;\n    if (uniformsRef.current.iMouse?.isNeeded && canvasRef.current) {\n      canvasRef.current.removeEventListener('mousemove', mouseMove, options);\n      canvasRef.current.removeEventListener('mouseout', mouseUp, options);\n      canvasRef.current.removeEventListener('mouseup', mouseUp, options);\n      canvasRef.current.removeEventListener('mousedown', mouseDown, options);\n      canvasRef.current.removeEventListener('touchmove', mouseMove, options);\n      canvasRef.current.removeEventListener('touchend', mouseUp, options);\n      canvasRef.current.removeEventListener('touchstart', mouseDown, options);\n    }\n    if (uniformsRef.current.iDeviceOrientation?.isNeeded) {\n      window.removeEventListener('deviceorientation', onDeviceOrientationChange, options);\n    }\n    if (resizeObserverRef.current) {\n      resizeObserverRef.current.disconnect();\n      window.removeEventListener('resize', onResize, options);\n    }\n  };\n\n  useEffect(() => {\n    propsUniformsRef.current = propUniforms;\n  }, [propUniforms]);\n\n  useEffect(() => {\n    animateWhenNotVisibleRef.current = animateWhenNotVisible;\n    if (animateWhenNotVisible) {\n      isVisibleRef.current = true;\n    }\n  }, [animateWhenNotVisible]);\n\n  // Intersection Observer: pause animation when off-screen when animateWhenNotVisible is false\n  useEffect(() => {\n    if (animateWhenNotVisible || !canvasRef.current) return;\n    const canvas = canvasRef.current;\n    const observer = new IntersectionObserver(\n      (entries) => {\n        for (const entry of entries) {\n          isVisibleRef.current = entry.isIntersecting;\n          if (entry.isIntersecting) {\n            requestAnimationFrame(drawScene);\n          }\n        }\n      },\n      { threshold: 0 },\n    );\n    observer.observe(canvas);\n    return () => observer.disconnect();\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [animateWhenNotVisible]);\n\n  // Main effect for initialization and cleanup\n  useEffect(() => {\n    const textures = texturesArrRef.current;\n\n    function init() {\n      initWebGL();\n      const gl = glRef.current;\n      if (gl && canvasRef.current) {\n        gl.clearColor(...clearColor);\n        gl.clearDepth(1.0);\n        gl.enable(gl.DEPTH_TEST);\n        gl.depthFunc(gl.LEQUAL);\n        gl.viewport(0, 0, canvasRef.current.width, canvasRef.current.height);\n        canvasRef.current.height = canvasRef.current.clientHeight;\n        canvasRef.current.width = canvasRef.current.clientWidth;\n        processCustomUniforms();\n        processTextures();\n        initShaders(preProcessFragment(fs || BASIC_FS), vs || BASIC_VS);\n        initBuffers();\n        requestAnimationFrame(drawScene);\n        addEventListeners();\n        onResize();\n      }\n    }\n\n    initFrameIdRef.current = requestAnimationFrame(init);\n\n    // Cleanup function\n    return () => {\n      const gl = glRef.current;\n      if (gl) {\n        gl.getExtension('WEBGL_lose_context')?.loseContext();\n        gl.useProgram(null);\n        gl.deleteProgram(shaderProgramRef.current ?? null);\n        if (textures.length > 0) {\n          for (const texture of textures as Texture[]) {\n            gl.deleteTexture(texture._webglTexture);\n          }\n        }\n        shaderProgramRef.current = null;\n      }\n      removeEventListeners();\n      cancelAnimationFrame(initFrameIdRef.current ?? 0);\n      cancelAnimationFrame(animFrameIdRef.current ?? 0);\n    };\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, []); // Empty dependency array to run only once on mount\n\n  return (\n    <canvas ref={canvasRef} style={{ height: '100%', width: '100%', ...style }} {...canvasProps} />\n  );\n}\n","type":"registry:component"}],"type":"registry:component"}