{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "canvas-presentation",
  "title": "Canvas Presentation",
  "description": "The html-in-canvas scaffolding the canvas transitions and scene filters share: support detection, a full-screen WebGL2 shader fed either both sides of a cut or the single scene a filter wraps, and routing to a CSS fallback where the browser lacks the API.",
  "dependencies": ["remotion", "@remotion/transitions"],
  "files": [
    {
      "path": "registry/remocn/canvas-presentation/index.tsx",
      "content": "\"use client\";\n\nimport type {\n  TransitionPresentation,\n  TransitionPresentationComponentProps,\n} from \"@remotion/transitions\";\nimport React from \"react\";\nimport {\n  AbsoluteFill,\n  isHtmlInCanvasSupported,\n  useCurrentFrame,\n  useDelayRender,\n  useVideoConfig,\n} from \"remotion\";\n\nconst QUAD = new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]);\n\nconst VERTEX_SHADER = `#version 300 es\nin vec2 a_pos;\nout vec2 v_uv;\nvoid main() {\n  v_uv = vec2(a_pos.x * 0.5 + 0.5, 0.5 - a_pos.y * 0.5);\n  gl_Position = vec4(a_pos, 0.0, 1.0);\n}`;\n\nconst LAYER: React.CSSProperties = {\n  position: \"absolute\",\n  inset: 0,\n  width: \"100%\",\n  height: \"100%\",\n};\n\nconst OUTPUT_LAYER: React.CSSProperties = { ...LAYER, pointerEvents: \"none\" };\n\nexport const isCanvasTransitionSupported = (): boolean =>\n  isHtmlInCanvasSupported();\n\nexport const isCanvasFilterSupported = (): boolean => isHtmlInCanvasSupported();\n\nexport type SceneShaderFrame<Props> = {\n  gl: WebGL2RenderingContext;\n  uniform: (name: string) => WebGLUniformLocation | null;\n  width: number;\n  height: number;\n  progress: number;\n  passedProps: Props;\n};\n\nexport type SceneShaderDrawParams<Props> = {\n  from: OffscreenCanvas | null;\n  to: OffscreenCanvas | null;\n  width: number;\n  height: number;\n  progress: number;\n  passedProps: Props;\n};\n\nexport type SceneShaderInstance<Props> = {\n  draw: (params: SceneShaderDrawParams<Props>) => void;\n  clear: () => void;\n  cleanup: () => void;\n};\n\nexport type SceneShader<Props> = (\n  canvas: OffscreenCanvas,\n) => SceneShaderInstance<Props>;\n\nexport type SceneFilterFrame<Props> = {\n  gl: WebGL2RenderingContext;\n  uniform: (name: string) => WebGLUniformLocation | null;\n  width: number;\n  height: number;\n  frame: number;\n  time: number;\n  passedProps: Props;\n};\n\nexport type SceneFilterDrawParams<Props> = {\n  scene: OffscreenCanvas | null;\n  width: number;\n  height: number;\n  frame: number;\n  time: number;\n  passedProps: Props;\n};\n\nexport type SceneFilterInstance<Props> = {\n  draw: (params: SceneFilterDrawParams<Props>) => void;\n  clear: () => void;\n  cleanup: () => void;\n};\n\nexport type SceneFilter<Props> = (\n  canvas: OffscreenCanvas,\n) => SceneFilterInstance<Props>;\n\nfunction compileShader(\n  gl: WebGL2RenderingContext,\n  source: string,\n  type: GLenum,\n): WebGLShader {\n  const shader = gl.createShader(type);\n  if (!shader) {\n    throw new Error(\"canvas-presentation: failed to create shader\");\n  }\n  gl.shaderSource(shader, source);\n  gl.compileShader(shader);\n  if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {\n    const log = gl.getShaderInfoLog(shader);\n    gl.deleteShader(shader);\n    throw new Error(`canvas-presentation: failed to compile shader: ${log}`);\n  }\n  return shader;\n}\n\nfunction createProgram(\n  gl: WebGL2RenderingContext,\n  fragment: string,\n): WebGLProgram {\n  const program = gl.createProgram();\n  if (!program) {\n    throw new Error(\"canvas-presentation: failed to create program\");\n  }\n  const vs = compileShader(gl, VERTEX_SHADER, gl.VERTEX_SHADER);\n  const fs = compileShader(gl, fragment, gl.FRAGMENT_SHADER);\n  gl.attachShader(program, vs);\n  gl.attachShader(program, fs);\n  gl.linkProgram(program);\n  gl.deleteShader(vs);\n  gl.deleteShader(fs);\n  if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {\n    const log = gl.getProgramInfoLog(program);\n    gl.deleteProgram(program);\n    throw new Error(`canvas-presentation: failed to link program: ${log}`);\n  }\n  return program;\n}\n\nfunction createSceneTexture(gl: WebGL2RenderingContext): WebGLTexture {\n  const texture = gl.createTexture();\n  if (!texture) {\n    throw new Error(\"canvas-presentation: failed to create texture\");\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  gl.texImage2D(\n    gl.TEXTURE_2D,\n    0,\n    gl.RGBA,\n    1,\n    1,\n    0,\n    gl.RGBA,\n    gl.UNSIGNED_BYTE,\n    new Uint8Array([0, 0, 0, 0]),\n  );\n  return texture;\n}\n\nfunction createQuadSurface(canvas: OffscreenCanvas, fragment: string) {\n  const gl = canvas.getContext(\"webgl2\", { premultipliedAlpha: true });\n  if (!gl) {\n    throw new Error(\"canvas-presentation: WebGL2 is unavailable\");\n  }\n\n  const program = createProgram(gl, fragment);\n  const vao = gl.createVertexArray();\n  gl.bindVertexArray(vao);\n  const buffer = gl.createBuffer();\n  gl.bindBuffer(gl.ARRAY_BUFFER, buffer);\n  gl.bufferData(gl.ARRAY_BUFFER, QUAD, gl.STATIC_DRAW);\n  const aPos = gl.getAttribLocation(program, \"a_pos\");\n  gl.enableVertexAttribArray(aPos);\n  gl.vertexAttribPointer(aPos, 2, gl.FLOAT, false, 0, 0);\n\n  const locations = new Map<string, WebGLUniformLocation | null>();\n  const uniform = (name: string) => {\n    const cached = locations.get(name);\n    if (cached !== undefined) {\n      return cached;\n    }\n    const location = gl.getUniformLocation(program, name);\n    locations.set(name, location);\n    return location;\n  };\n\n  const clear = () => {\n    gl.clearColor(0, 0, 0, 0);\n    gl.clear(gl.COLOR_BUFFER_BIT);\n  };\n\n  const textures: WebGLTexture[] = [];\n  const createTexture = () => {\n    const texture = createSceneTexture(gl);\n    textures.push(texture);\n    return texture;\n  };\n\n  const begin = (width: number, height: number) => {\n    gl.viewport(0, 0, width, height);\n    clear();\n    gl.useProgram(program);\n    gl.bindVertexArray(vao);\n  };\n\n  const bindScene = (\n    image: OffscreenCanvas | null,\n    texture: WebGLTexture,\n    unit: number,\n    name: string,\n  ) => {\n    gl.activeTexture(gl.TEXTURE0 + unit);\n    gl.bindTexture(gl.TEXTURE_2D, texture);\n    if (image) {\n      gl.texImage2D(\n        gl.TEXTURE_2D,\n        0,\n        gl.RGBA,\n        gl.RGBA,\n        gl.UNSIGNED_BYTE,\n        image,\n      );\n    }\n    gl.uniform1i(uniform(name), unit);\n  };\n\n  const draw = () => gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);\n\n  const cleanup = () => {\n    gl.deleteProgram(program);\n    for (const texture of textures) {\n      gl.deleteTexture(texture);\n    }\n    gl.deleteBuffer(buffer);\n    gl.deleteVertexArray(vao);\n  };\n\n  return {\n    gl,\n    uniform,\n    clear,\n    createTexture,\n    begin,\n    bindScene,\n    draw,\n    cleanup,\n  };\n}\n\nexport function makeSceneShader<Props extends Record<string, unknown>>(\n  fragment: string,\n  setUniforms?: (frame: SceneShaderFrame<Props>) => void,\n): SceneShader<Props> {\n  return (canvas) => {\n    const surface = createQuadSurface(canvas, fragment);\n    const fromTexture = surface.createTexture();\n    const toTexture = surface.createTexture();\n\n    return {\n      clear: surface.clear,\n      cleanup: surface.cleanup,\n      draw: ({ from, to, width, height, progress, passedProps }) => {\n        surface.begin(width, height);\n        surface.bindScene(from, fromTexture, 0, \"u_from\");\n        surface.bindScene(to, toTexture, 1, \"u_to\");\n\n        surface.gl.uniform1f(surface.uniform(\"u_progress\"), progress);\n        surface.gl.uniform1f(surface.uniform(\"u_aspect\"), width / height);\n        setUniforms?.({\n          gl: surface.gl,\n          uniform: surface.uniform,\n          width,\n          height,\n          progress,\n          passedProps,\n        });\n\n        surface.draw();\n      },\n    };\n  };\n}\n\nexport function makeFilterShader<Props extends Record<string, unknown>>(\n  fragment: string,\n  setUniforms?: (frame: SceneFilterFrame<Props>) => void,\n): SceneFilter<Props> {\n  return (canvas) => {\n    const surface = createQuadSurface(canvas, fragment);\n    const sceneTexture = surface.createTexture();\n\n    return {\n      clear: surface.clear,\n      cleanup: surface.cleanup,\n      draw: ({ scene, width, height, frame, time, passedProps }) => {\n        surface.begin(width, height);\n        surface.bindScene(scene, sceneTexture, 0, \"u_scene\");\n\n        surface.gl.uniform1f(surface.uniform(\"u_frame\"), frame);\n        surface.gl.uniform1f(surface.uniform(\"u_time\"), time);\n        surface.gl.uniform1f(surface.uniform(\"u_aspect\"), width / height);\n        setUniforms?.({\n          gl: surface.gl,\n          uniform: surface.uniform,\n          width,\n          height,\n          frame,\n          time,\n          passedProps,\n        });\n\n        surface.draw();\n      },\n    };\n  };\n}\n\nconst transferred = new WeakMap<HTMLCanvasElement, OffscreenCanvas>();\n\nfunction takeOffscreen(canvas: HTMLCanvasElement): OffscreenCanvas {\n  const existing = transferred.get(canvas);\n  if (existing) {\n    return existing;\n  }\n  const created = canvas.transferControlToOffscreen();\n  transferred.set(canvas, created);\n  return created;\n}\n\nfunction makeCanvasComponent<Props extends Record<string, unknown>>(\n  shader: SceneShader<Props>,\n): React.FC<TransitionPresentationComponentProps<Props>> {\n  return function CanvasScenePresentation({\n    children,\n    presentationProgress,\n    presentationDirection,\n    passedProps,\n    onElementImage,\n    onUnmount,\n    bothEnteringAndExiting,\n  }) {\n    const layoutRef = React.useRef<HTMLCanvasElement | null>(null);\n    const outputRef = React.useRef<HTMLCanvasElement | null>(null);\n    const captureRef = React.useRef<OffscreenCanvas | null>(null);\n    const captureContextRef =\n      React.useRef<OffscreenCanvasRenderingContext2D | null>(null);\n    const outputSurfaceRef = React.useRef<OffscreenCanvas | null>(null);\n    const instanceRef = React.useRef<SceneShaderInstance<Props> | null>(null);\n    const passedPropsRef = React.useRef(passedProps);\n    passedPropsRef.current = passedProps;\n    const onElementImageRef = React.useRef(onElementImage);\n    onElementImageRef.current = onElementImage;\n    const onUnmountRef = React.useRef(onUnmount);\n    onUnmountRef.current = onUnmount;\n\n    const { delayRender, continueRender } = useDelayRender();\n    const passThrough =\n      bothEnteringAndExiting && presentationDirection === \"exiting\";\n\n    const drawRef = React.useRef<\n      (\n        prevImage: OffscreenCanvas | null,\n        nextImage: OffscreenCanvas | null,\n        progress: number,\n      ) => void\n    >(() => undefined);\n    drawRef.current = (prevImage, nextImage, progress) => {\n      const instance = instanceRef.current;\n      if (!instance) {\n        return;\n      }\n      const handle = delayRender(\"canvas-presentation: paint\");\n      const to = prevImage;\n      const from = nextImage;\n      const width = from?.width ?? to?.width ?? 0;\n      const height = from?.height ?? to?.height ?? 0;\n      if (width === 0 || height === 0) {\n        instance.clear();\n        continueRender(handle);\n        return;\n      }\n      instance.draw({\n        from,\n        to,\n        width,\n        height,\n        progress: !to ? 0 : !from ? 1 : progress,\n        passedProps: passedPropsRef.current,\n      });\n      continueRender(handle);\n    };\n\n    const draw = React.useCallback(\n      (\n        prevImage: OffscreenCanvas | null,\n        nextImage: OffscreenCanvas | null,\n        progress: number,\n      ) => drawRef.current(prevImage, nextImage, progress),\n      [],\n    );\n\n    React.useLayoutEffect(() => {\n      if (passThrough) {\n        return;\n      }\n      const layout = layoutRef.current;\n      const output = outputRef.current;\n      if (!layout || !output) {\n        return;\n      }\n      const capture = takeOffscreen(layout);\n      const context = capture.getContext(\"2d\");\n      if (!context) {\n        throw new Error(\"canvas-presentation: failed to acquire a 2D context\");\n      }\n      const surface = takeOffscreen(output);\n      captureRef.current = capture;\n      captureContextRef.current = context;\n      outputSurfaceRef.current = surface;\n      instanceRef.current = shader(surface);\n\n      return () => {\n        instanceRef.current?.cleanup();\n        instanceRef.current = null;\n        captureRef.current = null;\n        captureContextRef.current = null;\n        outputSurfaceRef.current = null;\n      };\n    }, [passThrough, shader]);\n\n    React.useLayoutEffect(() => {\n      if (passThrough) {\n        return;\n      }\n      const layout = layoutRef.current;\n      if (!layout) {\n        return;\n      }\n      layout.layoutSubtree = true;\n      const handlePaint = () => {\n        const scene = layout.firstElementChild;\n        const capture = captureRef.current;\n        const context = captureContextRef.current;\n        if (!scene || !capture || !context) {\n          return;\n        }\n        const elementImage = layout.captureElementImage(scene);\n        try {\n          context.reset();\n          context.drawElementImage(elementImage, 0, 0);\n        } finally {\n          elementImage.close();\n        }\n        onElementImageRef.current(capture, draw);\n      };\n      layout.addEventListener(\"paint\", handlePaint);\n      return () => layout.removeEventListener(\"paint\", handlePaint);\n    }, [passThrough, draw]);\n\n    React.useLayoutEffect(() => {\n      if (passThrough) {\n        return;\n      }\n      const layout = layoutRef.current;\n      if (!layout) {\n        return;\n      }\n      const observer = new ResizeObserver(([entry]) => {\n        const capture = captureRef.current;\n        const surface = outputSurfaceRef.current;\n        if (!capture || !surface) {\n          return;\n        }\n        const width = entry.devicePixelContentBoxSize[0].inlineSize;\n        const height = entry.devicePixelContentBoxSize[0].blockSize;\n        capture.width = width;\n        capture.height = height;\n        surface.width = width;\n        surface.height = height;\n        layout.requestPaint?.();\n      });\n      observer.observe(layout, { box: \"device-pixel-content-box\" });\n      return () => observer.disconnect();\n    }, [passThrough]);\n\n    // biome-ignore lint/correctness/useExhaustiveDependencies: presentationProgress is the intentional trigger — a new progress value is what schedules the next paint\n    React.useLayoutEffect(() => {\n      if (passThrough) {\n        return;\n      }\n      layoutRef.current?.requestPaint?.();\n    }, [passThrough, presentationProgress]);\n\n    React.useLayoutEffect(() => {\n      if (passThrough) {\n        return;\n      }\n      return () => onUnmountRef.current();\n    }, [passThrough]);\n\n    if (passThrough) {\n      return <>{children}</>;\n    }\n\n    return (\n      <AbsoluteFill>\n        <canvas ref={layoutRef} style={LAYER}>\n          {children}\n        </canvas>\n        <canvas ref={outputRef} style={OUTPUT_LAYER} />\n      </AbsoluteFill>\n    );\n  };\n}\n\nexport type CanvasFilterProps = {\n  children: React.ReactNode;\n};\n\nfunction makeFilterComponent<Props extends Record<string, unknown>>(\n  shader: SceneFilter<Props>,\n): React.FC<Props & CanvasFilterProps> {\n  return function CanvasSceneFilter(props) {\n    const frame = useCurrentFrame();\n    const { fps } = useVideoConfig();\n    const { delayRender, continueRender } = useDelayRender();\n\n    const layoutRef = React.useRef<HTMLCanvasElement | null>(null);\n    const outputRef = React.useRef<HTMLCanvasElement | null>(null);\n    const captureRef = React.useRef<OffscreenCanvas | null>(null);\n    const captureContextRef =\n      React.useRef<OffscreenCanvasRenderingContext2D | null>(null);\n    const outputSurfaceRef = React.useRef<OffscreenCanvas | null>(null);\n    const instanceRef = React.useRef<SceneFilterInstance<Props> | null>(null);\n\n    const passedPropsRef = React.useRef(props);\n    passedPropsRef.current = props;\n    const frameRef = React.useRef(frame);\n    frameRef.current = frame;\n    const timeRef = React.useRef(frame / fps);\n    timeRef.current = frame / fps;\n\n    React.useLayoutEffect(() => {\n      const layout = layoutRef.current;\n      const output = outputRef.current;\n      if (!layout || !output) {\n        return;\n      }\n      const capture = takeOffscreen(layout);\n      const context = capture.getContext(\"2d\");\n      if (!context) {\n        throw new Error(\"canvas-presentation: failed to acquire a 2D context\");\n      }\n      const surface = takeOffscreen(output);\n      captureRef.current = capture;\n      captureContextRef.current = context;\n      outputSurfaceRef.current = surface;\n      instanceRef.current = shader(surface);\n\n      return () => {\n        instanceRef.current?.cleanup();\n        instanceRef.current = null;\n        captureRef.current = null;\n        captureContextRef.current = null;\n        outputSurfaceRef.current = null;\n      };\n    }, [shader]);\n\n    React.useLayoutEffect(() => {\n      const layout = layoutRef.current;\n      if (!layout) {\n        return;\n      }\n      layout.layoutSubtree = true;\n      const handlePaint = () => {\n        const scene = layout.firstElementChild;\n        const capture = captureRef.current;\n        const context = captureContextRef.current;\n        const instance = instanceRef.current;\n        if (!scene || !capture || !context || !instance) {\n          return;\n        }\n        const elementImage = layout.captureElementImage(scene);\n        try {\n          context.reset();\n          context.drawElementImage(elementImage, 0, 0);\n        } finally {\n          elementImage.close();\n        }\n        if (capture.width === 0 || capture.height === 0) {\n          instance.clear();\n          return;\n        }\n        instance.draw({\n          scene: capture,\n          width: capture.width,\n          height: capture.height,\n          frame: frameRef.current,\n          time: timeRef.current,\n          passedProps: passedPropsRef.current,\n        });\n      };\n      layout.addEventListener(\"paint\", handlePaint);\n      return () => layout.removeEventListener(\"paint\", handlePaint);\n    }, []);\n\n    React.useLayoutEffect(() => {\n      const layout = layoutRef.current;\n      if (!layout) {\n        return;\n      }\n      const observer = new ResizeObserver(([entry]) => {\n        const capture = captureRef.current;\n        const surface = outputSurfaceRef.current;\n        if (!capture || !surface) {\n          return;\n        }\n        const width = entry.devicePixelContentBoxSize[0].inlineSize;\n        const height = entry.devicePixelContentBoxSize[0].blockSize;\n        capture.width = width;\n        capture.height = height;\n        surface.width = width;\n        surface.height = height;\n        layout.requestPaint?.();\n      });\n      observer.observe(layout, { box: \"device-pixel-content-box\" });\n      return () => observer.disconnect();\n    }, []);\n\n    React.useLayoutEffect(() => {\n      const layout = layoutRef.current;\n      if (!layout) {\n        return;\n      }\n      const handle = delayRender(\"canvas-presentation: filter paint\");\n      const settle = () => continueRender(handle);\n      layout.addEventListener(\"paint\", settle, { once: true });\n      layout.requestPaint?.();\n      return () => {\n        layout.removeEventListener(\"paint\", settle);\n        continueRender(handle);\n      };\n    });\n\n    return (\n      <AbsoluteFill>\n        <canvas ref={layoutRef} style={LAYER}>\n          {props.children}\n        </canvas>\n        <canvas ref={outputRef} style={OUTPUT_LAYER} />\n      </AbsoluteFill>\n    );\n  };\n}\n\nexport type CanvasPresentationOptions<Props extends Record<string, unknown>> = {\n  shader: SceneShader<Props>;\n  fallback: React.FC<TransitionPresentationComponentProps<Props>>;\n};\n\nexport function makeCanvasPresentation<Props extends Record<string, unknown>>({\n  shader,\n  fallback: Fallback,\n}: CanvasPresentationOptions<Props>): (\n  props: Props,\n) => TransitionPresentation<Props> {\n  const Canvas = makeCanvasComponent(shader);\n\n  const Presentation: React.FC<TransitionPresentationComponentProps<Props>> = (\n    props,\n  ) =>\n    isCanvasTransitionSupported() ? (\n      <Canvas {...props} />\n    ) : (\n      <Fallback {...props} />\n    );\n\n  return (props) => ({ component: Presentation, props });\n}\n\nexport type CanvasFilterOptions<Props extends Record<string, unknown>> = {\n  shader: SceneFilter<Props>;\n  fallback: React.FC<Props & CanvasFilterProps>;\n};\n\nexport function makeCanvasFilter<Props extends Record<string, unknown>>({\n  shader,\n  fallback: Fallback,\n}: CanvasFilterOptions<Props>): React.FC<Props & CanvasFilterProps> {\n  const Canvas = makeFilterComponent(shader);\n\n  return function CanvasFilter(props) {\n    return isCanvasFilterSupported() ? (\n      <Canvas {...props} />\n    ) : (\n      <Fallback {...props} />\n    );\n  };\n}\n",
      "type": "registry:lib",
      "target": "lib/remocn/canvas-presentation.tsx"
    }
  ],
  "type": "registry:lib"
}
