{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "shader-seam",
  "title": "Shader Seam",
  "description": "Dissolve a stationary OpenShaders material to uncover the next scene.",
  "dependencies": ["remotion", "@remotion/transitions"],
  "files": [
    {
      "path": "registry/remocn/shader-seam/index.tsx",
      "content": "\"use client\";\n\nimport type {\n  TransitionPresentation,\n  TransitionPresentationComponentProps,\n} from \"@remotion/transitions\";\nimport { useLayoutEffect, useRef } from \"react\";\nimport { AbsoluteFill, useDelayRender, useVideoConfig } from \"remotion\";\n\n/* @remocn · OpenShaders — https://openshaders.com/@remocn\n * Original field and halftone GLSL from the site's React · WebGL export.\n * The frame-driven renderer, dissolve compositing and scene exchange are remocn's.\n */\n\nconst VERTEX_SHADER = `#version 300 es\nvoid main() {\n  vec2 position = vec2(float((gl_VertexID << 1) & 2), float(gl_VertexID & 2));\n  gl_Position = vec4(position * 2.0 - 1.0, 0.0, 1.0);\n}\n`;\n\nconst FIELD_SHADER = `#version 300 es\nprecision highp float;\n\nuniform vec2 iResolution;\nuniform float iTime;\nuniform float uLightMode;\nuniform vec3 uDarkBackground;\nuniform vec3 uLightBackground;\nout vec4 fragColor;\n\nconst float HUE = 0.663466752;\nconst float HUE_SPREAD = 0.314346701;\nconst float HUE_TRAVEL = 1.65112376;\nconst float CHROMA = 0.160295546;\nconst float LIGHTNESS = 0.537409723;\nconst float COLOUR_CYCLE = 0.124862425;\nconst float THETA = 2.12986207;\nconst float SHEAR = 0.968176901;\nconst float SHRINK = 0.955401778;\nconst float LAYERS = 72.0;\nconst float WARP_FREQ_X = 0.439311415;\nconst float WARP_FREQ_Y = 2.87923527;\nconst float WARP_AMP_X = 0.156202048;\nconst float WARP_AMP_Y = 0.0256033838;\nconst float ASPECT_X = 2.23183894;\nconst float ASPECT_Y = 0.15731883;\nconst float OFFSET_X = 0.375628948;\nconst float OFFSET_Y = -0.017801486;\nconst float TILT = 1.63293767;\nconst float ZOOM = 0.93135649;\nconst float CENTRE_X = -0.0956316143;\nconst float CENTRE_Y = -0.520057321;\nconst float GLOW_SIZE = 0.00201825937;\nconst float FALLOFF = 0.352821857;\nconst float VIGNETTE = 0.136085436;\nconst float FLOW_SPEED = 0.391012281;\nconst float FLOW_DIRECTION = 1.0;\nconst float BREATH_RATE = 0.382053256;\nconst float BREATH_AMOUNT = 0.0873704553;\nconst float PHASE = 56.3862724;\nconst float ECHO = 0.0;\nconst float ECHO_SHIFT = 0.178754866;\nconst float SOFTNESS = 0.00164903817;\nconst float LIGHT_SWING = 0.149726808;\n\nconst float TAU = 6.28318530718;\n\nvec3 oklchToLinear(float L, float C, float h) {\n  float a = C * cos(h), b = C * sin(h);\n  float l_ = L + 0.3963377774 * a + 0.2158037573 * b;\n  float m_ = L - 0.1055613458 * a - 0.0638541728 * b;\n  float s_ = L - 0.0894841775 * a - 1.2914855480 * b;\n  vec3 lms = vec3(l_, m_, s_);\n  lms = lms * lms * lms;\n  return mat3(4.0767416621, -1.2684380046, -0.0041960863,\n              -3.3077115913, 2.6097574011, -0.7034186147,\n              0.2309699292, -0.3413193965, 1.7076147010) * lms;\n}\n\nfloat blueNoise(vec2 p, float frame) {\n  p += 5.588238 * mod(frame, 64.0);\n  return fract(52.9829189 * fract(0.06711056 * p.x + 0.00583715 * p.y));\n}\n\nvoid main() {\n  vec2 R = iResolution.xy;\n  vec2 pos = (gl_FragCoord.xy - 0.5 * R) / R.y;\n  float t = iTime * FLOW_SPEED * FLOW_DIRECTION + PHASE;\n  float breath = (-sin(iTime * BREATH_RATE * 1.5) + sin(iTime * BREATH_RATE + 1.0)) * 0.25 + 0.5;\n\n  vec2 u = (pos - vec2(CENTRE_X, CENTRE_Y)) * (ZOOM - breath * BREATH_AMOUNT);\n  float ct = cos(TILT), st = sin(TILT);\n  u = mat2(ct, st, -st, ct) * u;\n\n  mat2 fold = mat2(cos(THETA), sin(THETA), -SHEAR, cos(THETA));\n\n  float hue0 = HUE * TAU;\n  float hue1 = hue0 + HUE_SPREAD * TAU;\n  vec3 color = vec3(0.0);\n\n  for (float i = 1.0; i <= 96.0; i += 1.0) {\n    if (i > LAYERS) break;\n    u.x += -sin(u.y * WARP_FREQ_X + t + i * 0.007) * WARP_AMP_X;\n    u.y += -sin(u.x * WARP_FREQ_Y - t + i * 0.02) * WARP_AMP_Y;\n    u = fold * u * SHRINK;\n\n    vec2 q = u - vec2(OFFSET_X + breath * 0.1, OFFSET_Y);\n    vec2 s = vec2(q.x * ASPECT_X, q.y * ASPECT_Y);\n    float glow = GLOW_SIZE / (dot(s, s) + SOFTNESS);\n#ifndef SKIP_ECHO\n    vec2 e = vec2((q.x - ECHO_SHIFT) * ASPECT_X, s.y);\n    glow += ECHO * GLOW_SIZE / (dot(e, e) + SOFTNESS);\n#endif\n    glow *= 0.25 + breath * 0.4;\n\n    float r = length(u);\n    float k = sin(i * COLOUR_CYCLE + t * 1.2 + r * HUE_TRAVEL) * 0.5 + 0.5;\n    vec3 tint = clamp(oklchToLinear(LIGHTNESS + LIGHT_SWING * k, CHROMA * (0.75 + 0.35 * k), mix(hue0, hue1, k)), 0.0, 1.0);\n    color += glow * tint * exp2(-r * FALLOFF);\n  }\n\n  vec3 x = max(color, 0.0);\n  color = (x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14);\n  color = pow(clamp(color, 0.0, 1.0), vec3(0.85, 0.92, 0.98));\n\n  float edge = smoothstep(0.5, 1.6, length(pos));\n  color *= 1.0 - edge * VIGNETTE;\n\n  vec3 dark = uDarkBackground + color * (1.0 - uDarkBackground);\n  float strength = max(color.r, max(color.g, color.b));\n  vec3 light = uLightBackground * (1.0 - strength) + color * 0.96;\n  color = mix(dark, light, uLightMode);\n\n  color += (blueNoise(gl_FragCoord.xy, floor(iTime * 24.0)) - 0.5) / 255.0;\n  fragColor = vec4(clamp(color, 0.0, 1.0), 1.0);\n}\n`;\n\nconst RARITY_SHADER = `#version 300 es\nprecision highp float;\n\nuniform sampler2D tScene;\nuniform vec2 iResolution;\nuniform float iTime;\nuniform float uLightMode;\nuniform vec3 uDarkBackground;\nuniform vec3 uLightBackground;\nuniform float uPixelRatio;\nout vec4 fragColor;\n\nconst float uStrength = 0.797335207;\nconst float uScale = 0.985214949;\nconst float uSeed = 0.435611069;\n\nconst float TAU = 6.28318530718;\nconst vec3 LUMA = vec3(0.2126, 0.7152, 0.0722);\n\nvec3 toInk(vec3 c) { return mix(c - uDarkBackground, uLightBackground - c, uLightMode); }\nvec3 fromInk(vec3 ink) { return mix(uDarkBackground + ink, uLightBackground - ink, uLightMode); }\nvec3 sceneInk(vec2 uv) { return toInk(texture(tScene, clamp(uv, 0.0, 1.0)).rgb); }\n\nfloat blueNoise(vec2 p, float frame) {\n  p += 5.588238 * mod(frame, 64.0);\n  return fract(52.9829189 * fract(0.06711056 * p.x + 0.00583715 * p.y));\n}\n\nvec3 halftone(vec2 frag) {\n  float cell = max(3.0, uScale * 3.6 * uPixelRatio);\n  float angle = 0.26 + uSeed * 0.3;\n  mat2 turn = mat2(cos(angle), -sin(angle), sin(angle), cos(angle));\n  vec2 rotated = turn * frag;\n  vec2 grid = floor(rotated / cell);\n  vec2 centre = (grid + 0.5) * cell;\n  vec2 source = transpose(turn) * centre;\n  vec3 soft = sceneInk(frag / iResolution);\n  vec3 ink = sceneInk(source / iResolution);\n  float level = clamp(dot(ink, LUMA), 0.0, 1.0);\n  float radius = cell * sqrt(pow(level, 0.9) / 3.14159265);\n  float dist = length(rotated - centre);\n  float aa = 0.7 * uPixelRatio;\n  float dot_ = 1.0 - smoothstep(radius - aa, radius + aa, dist);\n  vec3 dots = ink * min(0.8 / max(level, 1e-3), 2.2) * dot_;\n  float presence = smoothstep(0.03, 0.16, level) * (0.3 + 0.14 * uStrength);\n  return mix(soft, dots, presence);\n}\n\nvoid main() {\n  vec2 frag = gl_FragCoord.xy;\n  vec3 ink = halftone(frag);\n  vec3 color = fromInk(clamp(ink, 0.0, 1.0));\n  color += (blueNoise(frag, floor(iTime * 24.0)) - 0.5) / 255.0;\n  fragColor = vec4(clamp(color, 0.0, 1.0), 1.0);\n}\n`;\n\nexport type OpenShaderSource = {\n  /** OpenShaders' exported FIELD_SHADER, including #version 300 es. */\n  field: string;\n  /** Optional RARITY_SHADER using tScene. Extra texture inputs need an adapter. */\n  postprocess?: string;\n};\n\nexport const remocnOpenShader: OpenShaderSource = {\n  field: FIELD_SHADER,\n  postprocess: RARITY_SHADER,\n};\n\nexport type ShaderSeamProps = {\n  /** Softness of the material's dissolve edge, clamped to 0.02–0.4. */\n  softness?: number;\n  /** Organic variation mixed into the material's brightness, from 0 to 1. */\n  detail?: number;\n  /** Internal field animation speed. The default 0 keeps the material still. */\n  speed?: number;\n  /** Starting time in seconds in the selected shader. */\n  timeOffset?: number;\n  shader?: OpenShaderSource;\n};\n\nconst clamp = (value: number, min: number, max: number) =>\n  Math.min(max, Math.max(min, value));\n\n// Exchange the scenes only while the shader is opaque across the entire frame.\nexport function getShaderSeamPhase(progress: number) {\n  const p = clamp(progress, 0, 1);\n  const smoother = (t: number) => t * t * t * (t * (t * 6 - 15) + 10);\n  return {\n    presence: smoother(clamp(p / 0.34, 0, 1)),\n    dissolve: smoother(clamp((p - 0.46) / 0.54, 0, 1)),\n    showNext: p >= 0.4,\n  };\n}\n\ntype SeamPhase = ReturnType<typeof getShaderSeamPhase>;\n\nconst DISSOLVE_COMPOSITE = `#version 300 es\nprecision highp float;\nuniform vec2 iResolution;\nuniform sampler2D tScene;\nuniform sampler2D tField;\nuniform float uPresence;\nuniform float uDissolve;\nuniform float uSoftness;\nuniform float uDetail;\nout vec4 fragColor;\n\nfloat hash(vec2 p) {\n  return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453);\n}\nfloat noise(vec2 p) {\n  vec2 i = floor(p), f = fract(p);\n  f = f * f * (3.0 - 2.0 * f);\n  return mix(mix(hash(i), hash(i + vec2(1, 0)), f.x),\n    mix(hash(i + vec2(0, 1)), hash(i + vec2(1, 1)), f.x), f.y);\n}\nvoid main() {\n  vec2 uv = gl_FragCoord.xy / iResolution;\n  vec2 p = gl_FragCoord.xy / iResolution.y;\n  vec3 field = texture(tField, uv).rgb;\n  float energy = max(field.r, max(field.g, field.b));\n  // Static, continuous noise breaks up the field without making it travel.\n  float grain = noise(p * 7.0) * 0.6 + noise(p * 19.0) * 0.28\n    + noise(p * 43.0) * 0.12;\n  float density = mix(smoothstep(0.025, 0.95, energy), grain, uDetail * 0.55);\n  float appear = mix(1.0 + uSoftness, -uSoftness, uPresence);\n  float disappear = mix(-uSoftness, 1.0 + uSoftness, uDissolve);\n  float alpha = smoothstep(appear - uSoftness, appear + uSoftness, density)\n    * smoothstep(disappear - uSoftness, disappear + uSoftness, density);\n  vec3 material = texture(tScene, uv).rgb;\n  fragColor = vec4(material * alpha, alpha);\n}\n`;\n\nfunction createSeamRenderer(\n  canvas: HTMLCanvasElement,\n  source: OpenShaderSource,\n) {\n  const gl = canvas.getContext(\"webgl2\", {\n    alpha: true,\n    premultipliedAlpha: true,\n    antialias: false,\n    depth: false,\n    stencil: false,\n  });\n  if (!gl) throw new Error(\"Shader Seam requires WebGL2.\");\n  const programs: WebGLProgram[] = [];\n  const textures: WebGLTexture[] = [];\n  const buffers: WebGLFramebuffer[] = [];\n  const shaders: WebGLShader[] = [];\n  const dispose = () => {\n    for (const program of programs) gl.deleteProgram(program);\n    for (const texture of textures) gl.deleteTexture(texture);\n    for (const buffer of buffers) gl.deleteFramebuffer(buffer);\n    for (const shader of shaders) gl.deleteShader(shader);\n  };\n  const program = (fragment: string, sceneInput = false, flowInput = false) => {\n    const result = gl.createProgram();\n    if (!result) throw new Error(\"Shader Seam: could not create a program.\");\n    programs.push(result);\n    for (const [type, code] of [\n      [gl.VERTEX_SHADER, VERTEX_SHADER],\n      [gl.FRAGMENT_SHADER, fragment],\n    ] as const) {\n      const shader = gl.createShader(type);\n      if (!shader) throw new Error(\"Shader Seam: could not allocate a shader.\");\n      shaders.push(shader);\n      gl.shaderSource(shader, code);\n      gl.compileShader(shader);\n      if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {\n        throw new Error(`Shader Seam: ${gl.getShaderInfoLog(shader)}`);\n      }\n      gl.attachShader(result, shader);\n    }\n    gl.linkProgram(result);\n    if (!gl.getProgramParameter(result, gl.LINK_STATUS)) {\n      throw new Error(`Shader Seam: ${gl.getProgramInfoLog(result)}`);\n    }\n    const locations = new Map<string, WebGLUniformLocation | null>();\n    const uniform = (name: string) => {\n      if (!locations.has(name))\n        locations.set(name, gl.getUniformLocation(result, name));\n      return locations.get(name) ?? null;\n    };\n    // Fail explicitly for exports requiring assets this adapter cannot supply.\n    for (\n      let i = 0;\n      i < gl.getProgramParameter(result, gl.ACTIVE_UNIFORMS);\n      i++\n    ) {\n      const info = gl.getActiveUniform(result, i);\n      if (\n        info &&\n        (info.type === gl.SAMPLER_2D || info.type === gl.SAMPLER_CUBE) &&\n        !(sceneInput && info.name === \"tScene\") &&\n        !(flowInput && info.name === \"tField\")\n      ) {\n        throw new Error(\n          `Shader Seam: ${info.name} needs an additional texture adapter.`,\n        );\n      }\n    }\n    return {\n      begin: (\n        w: number,\n        h: number,\n        time: number,\n        phase: SeamPhase,\n        softness: number,\n        detail: number,\n      ) => {\n        gl.useProgram(result);\n        gl.viewport(0, 0, w, h);\n        gl.uniform2f(uniform(\"iResolution\"), w, h);\n        gl.uniform1f(uniform(\"iTime\"), time);\n        gl.uniform1f(uniform(\"uLightMode\"), 0);\n        gl.uniform3f(uniform(\"uDarkBackground\"), 9 / 255, 9 / 255, 9 / 255);\n        gl.uniform3f(uniform(\"uLightBackground\"), 1, 1, 1);\n        gl.uniform1f(uniform(\"uPixelRatio\"), 1);\n        gl.uniform2f(uniform(\"uTilt\"), 0, 0);\n        gl.uniform1i(uniform(\"tScene\"), 0);\n        gl.uniform1i(uniform(\"tField\"), 1);\n        gl.uniform1f(uniform(\"uPresence\"), phase.presence);\n        gl.uniform1f(uniform(\"uDissolve\"), phase.dissolve);\n        gl.uniform1f(uniform(\"uSoftness\"), softness);\n        gl.uniform1f(uniform(\"uDetail\"), detail);\n      },\n    };\n  };\n  const target = () => {\n    const texture = gl.createTexture();\n    const framebuffer = gl.createFramebuffer();\n    if (texture) textures.push(texture);\n    if (framebuffer) buffers.push(framebuffer);\n    if (!texture || !framebuffer)\n      throw new Error(\"Shader Seam: could not create render target.\");\n    gl.activeTexture(gl.TEXTURE0);\n    gl.bindTexture(gl.TEXTURE_2D, texture);\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    let size = \"\";\n    return {\n      texture,\n      bind: (width: number, height: number) => {\n        gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);\n        if (size !== `${width}:${height}`) {\n          gl.activeTexture(gl.TEXTURE0);\n          gl.bindTexture(gl.TEXTURE_2D, texture);\n          gl.texImage2D(\n            gl.TEXTURE_2D,\n            0,\n            gl.RGBA,\n            width,\n            height,\n            0,\n            gl.RGBA,\n            gl.UNSIGNED_BYTE,\n            null,\n          );\n          gl.framebufferTexture2D(\n            gl.FRAMEBUFFER,\n            gl.COLOR_ATTACHMENT0,\n            gl.TEXTURE_2D,\n            texture,\n            0,\n          );\n          if (\n            gl.checkFramebufferStatus(gl.FRAMEBUFFER) !==\n            gl.FRAMEBUFFER_COMPLETE\n          ) {\n            throw new Error(\"Shader Seam: incomplete render target.\");\n          }\n          size = `${width}:${height}`;\n        }\n      },\n    };\n  };\n  try {\n    const field = program(source.field);\n    const post = source.postprocess ? program(source.postprocess, true) : null;\n    const composite = program(DISSOLVE_COMPOSITE, true, true);\n    const full = target();\n    const material = target();\n    const bindTexture = (texture: WebGLTexture, unit: number) => {\n      gl.activeTexture(unit === 0 ? gl.TEXTURE0 : gl.TEXTURE1);\n      gl.bindTexture(gl.TEXTURE_2D, texture);\n    };\n    return {\n      dispose,\n      draw: (\n        width: number,\n        height: number,\n        time: number,\n        phase: SeamPhase,\n        softness: number,\n        detail: number,\n      ) => {\n        if (gl.isContextLost())\n          throw new Error(\"Shader Seam: WebGL context was lost.\");\n        if (canvas.width !== width) canvas.width = width;\n        if (canvas.height !== height) canvas.height = height;\n        full.bind(width, height);\n        field.begin(width, height, time, phase, softness, detail);\n        gl.drawArrays(gl.TRIANGLES, 0, 3);\n        if (post) {\n          material.bind(width, height);\n          post.begin(width, height, time, phase, softness, detail);\n          bindTexture(full.texture, 0);\n          gl.drawArrays(gl.TRIANGLES, 0, 3);\n        }\n        gl.bindFramebuffer(gl.FRAMEBUFFER, null);\n        composite.begin(width, height, time, phase, softness, detail);\n        bindTexture(post ? material.texture : full.texture, 0);\n        bindTexture(full.texture, 1);\n        gl.drawArrays(gl.TRIANGLES, 0, 3);\n      },\n    };\n  } catch (error) {\n    dispose();\n    throw error;\n  }\n}\n\nfunction ShaderSeamPresentation({\n  children,\n  presentationProgress,\n  presentationDirection,\n  presentationDurationInFrames,\n  passedProps,\n}: TransitionPresentationComponentProps<ShaderSeamProps>) {\n  const { width, height, fps } = useVideoConfig();\n  const { delayRender, continueRender, cancelRender } = useDelayRender();\n  const canvas = useRef<HTMLCanvasElement>(null);\n  const renderer = useRef<ReturnType<typeof createSeamRenderer> | null>(null);\n  const entering = presentationDirection === \"entering\";\n  const {\n    softness = 0.18,\n    detail = 0.65,\n    speed = 0,\n    timeOffset = 0,\n    shader = remocnOpenShader,\n  } = passedProps;\n  const { field, postprocess } = shader;\n  const p = clamp(presentationProgress, 0, 1);\n  const active = entering && p > 0 && p < 1;\n  const phase = getShaderSeamPhase(p);\n\n  useLayoutEffect(() => {\n    if (!active || !canvas.current) return;\n    const handle = delayRender(\"Initialize Shader Seam\");\n    try {\n      renderer.current = createSeamRenderer(canvas.current, {\n        field,\n        postprocess,\n      });\n      continueRender(handle);\n    } catch (error) {\n      cancelRender(error instanceof Error ? error : new Error(String(error)));\n    }\n    return () => {\n      renderer.current?.dispose();\n      renderer.current = null;\n    };\n  }, [active, field, postprocess, delayRender, continueRender, cancelRender]);\n\n  // biome-ignore lint/correctness/useExhaustiveDependencies: redraw a newly compiled source even when the playhead is paused.\n  useLayoutEffect(() => {\n    if (!active || !canvas.current) return;\n    try {\n      const time =\n        timeOffset + ((p * presentationDurationInFrames) / fps) * speed;\n      renderer.current?.draw(\n        width,\n        height,\n        time,\n        getShaderSeamPhase(p),\n        clamp(softness, 0.02, 0.4),\n        clamp(detail, 0, 1),\n      );\n    } catch (error) {\n      cancelRender(error instanceof Error ? error : new Error(String(error)));\n    }\n  }, [\n    p,\n    active,\n    width,\n    height,\n    fps,\n    softness,\n    detail,\n    speed,\n    timeOffset,\n    presentationDurationInFrames,\n    entering,\n    field,\n    postprocess,\n    cancelRender,\n  ]);\n\n  return (\n    <AbsoluteFill style={{ overflow: \"hidden\" }}>\n      <AbsoluteFill\n        style={{\n          visibility: entering !== phase.showNext ? \"hidden\" : \"visible\",\n        }}\n      >\n        {children}\n      </AbsoluteFill>\n      <canvas\n        ref={canvas}\n        aria-hidden=\"true\"\n        tabIndex={-1}\n        style={{\n          position: \"absolute\",\n          inset: 0,\n          width: \"100%\",\n          height: \"100%\",\n          visibility: entering && active ? \"visible\" : \"hidden\",\n          pointerEvents: \"none\",\n        }}\n      />\n    </AbsoluteFill>\n  );\n}\n\nexport function shaderSeam(\n  props: ShaderSeamProps = {},\n): TransitionPresentation<ShaderSeamProps> {\n  return { component: ShaderSeamPresentation, props };\n}\n",
      "type": "registry:component",
      "target": "components/remocn/shader-seam.tsx"
    }
  ],
  "type": "registry:component"
}
