{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "shader-light-tunnel",
  "title": "Light Tunnel",
  "description": "Fly straight through a tunnel made from the original OpenShaders hero shader.",
  "dependencies": ["remotion"],
  "files": [
    {
      "path": "registry/remocn/shader-light-tunnel/index.tsx",
      "content": "\"use client\";\n\nimport { useLayoutEffect, useRef } from \"react\";\nimport { useCurrentFrame, useDelayRender, useVideoConfig } from \"remotion\";\n\nexport interface ShaderLightTunnelProps {\n  speed?: number;\n  /** Spiral winding and circular motion of the material, from 0 to 2. */\n  twist?: number;\n  /** Repetitions of the hero ribbons around the tunnel, integer from 1 to 6. */\n  spirals?: number;\n  /** Exposure of the original hero material, from 0 to 3. */\n  glow?: number;\n  /** Camera perspective, from 0.5 to 2. */\n  depth?: number;\n  /** Starting shader time, in seconds. */\n  timeOffset?: number;\n}\n\nconst VERTEX = `#version 300 es\nvoid main() {\n  vec2 p = vec2(float((gl_VertexID << 1) & 2), float(gl_VertexID & 2));\n  gl_Position = vec4(p * 2.0 - 1.0, 0.0, 1.0);\n}`;\n\n/* @remocn · OpenShaders — https://openshaders.com/@remocn\n * Original hero field and halftone source. The tunnel adapter only remaps\n * sampling coordinates, holds the material phase, and applies exposure.\n */\nconst HERO_FIELD = `#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 HERO_HALFTONE = `#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\n// Project the unchanged hero field onto a seamless cylindrical surface.\n// Material flows inward along the tunnel; the camera never moves sideways.\n// Hold the hero's color/breath cycle at its initial phase. Only the tunnel\n// coordinates advance, so the ribbons do not enter the hero's darker phases.\nconst TUNNEL_FIELD = HERO_FIELD.replaceAll(\"iTime\", \"uMaterialTime\")\n  .replace(\n    \"uniform float uMaterialTime;\",\n    `uniform float iTime;\n  const float uMaterialTime = 0.0;\n  uniform float uDepth;\n  uniform float uTwist;\n  uniform float uSpirals;\n  uniform float uCameraZoom;\n  vec2 tunnelPosition(vec2 frag) {\n    vec2 view = (frag - iResolution * 0.5) / iResolution.y / uCameraZoom;\n    float radius = max(length(view), 0.005);\n    float z = 0.55 * uDepth / radius + iTime * 1.2;\n    // Rotate the material explicitly so circular motion reads at every depth.\n    // The camera and its centered projection remain fixed.\n    // Integer angular repeats add ribbons without a seam or extra field passes.\n    float angle = (atan(view.y, view.x)\n      + (z * 0.3 + iTime * 1.5) * uTwist) * uSpirals;\n    // Constant phase travels to smaller radii: r(t) = r(0) * exp(-2.4 * t).\n    // Log spacing keeps the inward folds readable across the whole tunnel.\n    float inward = log(radius / (0.55 * uDepth)) * 5.0 + iTime * 12.0;\n    float ring = 0.68 + 0.1 * sin(inward);\n    return vec2(cos(angle), sin(angle)) * ring\n      + vec2(sin(inward) * 0.08, cos(inward) * 0.06 - 0.25);\n  }`,\n  )\n  .replace(\n    \"vec2 pos = (gl_FragCoord.xy - 0.5 * R) / R.y;\",\n    \"vec2 pos = tunnelPosition(gl_FragCoord.xy);\",\n  );\n\nconst TUNNEL_HALFTONE = HERO_HALFTONE.replace(\n  \"uniform vec2 iResolution;\",\n  \"uniform vec2 iResolution;\\nuniform float uGlow;\",\n).replace(\n  \"fragColor = vec4(clamp(color, 0.0, 1.0), 1.0);\",\n  `vec2 center = (gl_FragCoord.xy - iResolution * 0.5) / iResolution.y;\n  // Defocus the actual field at the vanishing point instead of cutting a dark\n  // circle into it. A Gaussian blend has no visible boundary around the center.\n  float centerWeight = exp(-dot(center, center) / (2.0 * 0.065 * 0.065));\n  if (centerWeight > 0.001) {\n    vec3 blurred = vec3(0.0);\n    float totalWeight = 0.0;\n    vec2 spread = vec2(iResolution.y * 0.025) / iResolution;\n    // Prefilter each tap so tight spiral turns do not reappear as ghost images.\n    float blurLod = max(0.0, log2(iResolution.y * 0.025));\n    vec2 uv = gl_FragCoord.xy / iResolution;\n    for (int y = -2; y <= 2; y++) {\n      for (int x = -2; x <= 2; x++) {\n        vec2 offset = vec2(float(x), float(y));\n        float weight = exp(-dot(offset, offset) * 0.5);\n        blurred += textureLod(tScene, clamp(uv + offset * spread, 0.0, 1.0), blurLod).rgb * weight;\n        totalWeight += weight;\n      }\n    }\n    color = mix(color, blurred / totalWeight, centerWeight);\n  }\n  fragColor = vec4(clamp(color * uGlow, 0.0, 1.0), 1.0);`,\n);\n\nconst bounded = (value: number, fallback: number, min: number, max: number) =>\n  Math.min(max, Math.max(min, Number.isFinite(value) ? value : fallback));\n\n/** Shared GPU renderer for the background and the spiral-pass transition. */\nexport function createTunnelRenderer(canvas: HTMLCanvasElement) {\n  const gl = canvas.getContext(\"webgl2\", {\n    alpha: false,\n    antialias: false,\n    depth: false,\n    stencil: false,\n  });\n  if (!gl) throw new Error(\"Shader Light Tunnel requires WebGL2.\");\n  const shaders: WebGLShader[] = [];\n  const programs: WebGLProgram[] = [];\n  let texture: WebGLTexture | null = null;\n  let framebuffer: WebGLFramebuffer | null = null;\n  const dispose = () => {\n    for (const shader of shaders) gl.deleteShader(shader);\n    for (const program of programs) gl.deleteProgram(program);\n    gl.deleteTexture(texture);\n    gl.deleteFramebuffer(framebuffer);\n  };\n  const compile = (fragment: string) => {\n    const program = gl.createProgram();\n    if (!program)\n      throw new Error(\"Shader Light Tunnel: GPU allocation failed.\");\n    programs.push(program);\n    for (const [type, source] of [\n      [gl.VERTEX_SHADER, VERTEX],\n      [gl.FRAGMENT_SHADER, fragment],\n    ] as const) {\n      const shader = gl.createShader(type);\n      if (!shader)\n        throw new Error(\"Shader Light Tunnel: shader allocation failed.\");\n      shaders.push(shader);\n      gl.shaderSource(shader, source);\n      gl.compileShader(shader);\n      if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS))\n        throw new Error(\n          gl.getShaderInfoLog(shader) ?? \"Tunnel shader compilation failed.\",\n        );\n      gl.attachShader(program, shader);\n    }\n    gl.linkProgram(program);\n    if (!gl.getProgramParameter(program, gl.LINK_STATUS))\n      throw new Error(\n        gl.getProgramInfoLog(program) ?? \"Tunnel shader link failed.\",\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(program, name));\n      return locations.get(name) ?? null;\n    };\n    return {\n      begin: (\n        time: number,\n        twist: number,\n        glow: number,\n        depth: number,\n        spirals: number,\n        cameraZoom = 1,\n      ) => {\n        gl.useProgram(program);\n        gl.viewport(0, 0, canvas.width, canvas.height);\n        gl.uniform2f(uniform(\"iResolution\"), canvas.width, canvas.height);\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.uniform1f(uniform(\"uTwist\"), bounded(twist, 0.7, 0, 2));\n        gl.uniform1f(\n          uniform(\"uSpirals\"),\n          Math.round(bounded(spirals, 3, 1, 6)),\n        );\n        gl.uniform1f(uniform(\"uGlow\"), bounded(glow, 1, 0, 3));\n        gl.uniform1f(uniform(\"uDepth\"), bounded(depth, 1, 0.5, 2));\n        gl.uniform1f(uniform(\"uCameraZoom\"), bounded(cameraZoom, 1, 1, 32));\n        gl.uniform1i(uniform(\"tScene\"), 0);\n      },\n    };\n  };\n  try {\n    const field = compile(TUNNEL_FIELD);\n    const halftone = compile(TUNNEL_HALFTONE);\n    texture = gl.createTexture();\n    framebuffer = gl.createFramebuffer();\n    if (!texture || !framebuffer)\n      throw new Error(\"Shader Light Tunnel: render target allocation failed.\");\n    gl.activeTexture(gl.TEXTURE0);\n    gl.bindTexture(gl.TEXTURE_2D, texture);\n    gl.texParameteri(\n      gl.TEXTURE_2D,\n      gl.TEXTURE_MIN_FILTER,\n      gl.LINEAR_MIPMAP_LINEAR,\n    );\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 targetWidth = 0,\n      targetHeight = 0;\n    return {\n      dispose,\n      draw: (\n        time: number,\n        twist: number,\n        glow: number,\n        depth: number,\n        spirals: number,\n        cameraZoom = 1,\n      ) => {\n        if (gl.isContextLost())\n          throw new Error(\"Shader Light Tunnel: WebGL context lost.\");\n        gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);\n        gl.bindTexture(gl.TEXTURE_2D, texture);\n        if (targetWidth !== canvas.width || targetHeight !== canvas.height) {\n          gl.texImage2D(\n            gl.TEXTURE_2D,\n            0,\n            gl.RGBA,\n            canvas.width,\n            canvas.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 Light Tunnel: incomplete framebuffer.\");\n          targetWidth = canvas.width;\n          targetHeight = canvas.height;\n        }\n        field.begin(time, twist, glow, depth, spirals, cameraZoom);\n        gl.drawArrays(gl.TRIANGLES, 0, 3);\n        gl.bindFramebuffer(gl.FRAMEBUFFER, null);\n        gl.generateMipmap(gl.TEXTURE_2D);\n        halftone.begin(time, twist, glow, depth, spirals);\n        gl.drawArrays(gl.TRIANGLES, 0, 3);\n      },\n    };\n  } catch (error) {\n    dispose();\n    throw error;\n  }\n}\n\nexport function ShaderLightTunnel({\n  speed = 1,\n  twist = 0.7,\n  spirals = 3,\n  glow = 1,\n  depth = 1,\n  timeOffset = 0,\n}: ShaderLightTunnelProps) {\n  const frame = useCurrentFrame();\n  const { width, height, fps } = useVideoConfig();\n  const { delayRender, continueRender, cancelRender } = useDelayRender();\n  const canvas = useRef<HTMLCanvasElement>(null);\n  const renderer = useRef<ReturnType<typeof createTunnelRenderer> | null>(null);\n\n  useLayoutEffect(() => {\n    if (!canvas.current) return;\n    const handle = delayRender(\"Initialize Shader Light Tunnel\");\n    try {\n      renderer.current = createTunnelRenderer(canvas.current);\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  }, [delayRender, continueRender, cancelRender]);\n\n  // biome-ignore lint/correctness/useExhaustiveDependencies: canvas resizing clears its buffer, including on paused frames.\n  useLayoutEffect(() => {\n    try {\n      const time =\n        bounded(timeOffset, 0, -10000, 10000) +\n        (frame / fps) * bounded(speed, 1, 0, 5);\n      renderer.current?.draw(time, twist, glow, depth, spirals);\n    } catch (error) {\n      cancelRender(error instanceof Error ? error : new Error(String(error)));\n    }\n  }, [\n    frame,\n    fps,\n    width,\n    height,\n    speed,\n    twist,\n    spirals,\n    glow,\n    depth,\n    timeOffset,\n    cancelRender,\n  ]);\n\n  return (\n    <canvas\n      ref={canvas}\n      width={width}\n      height={height}\n      aria-hidden=\"true\"\n      tabIndex={-1}\n      style={{\n        position: \"absolute\",\n        inset: 0,\n        width: \"100%\",\n        height: \"100%\",\n        pointerEvents: \"none\",\n      }}\n    />\n  );\n}\n",
      "type": "registry:component",
      "target": "components/remocn/shader-light-tunnel.tsx"
    }
  ],
  "type": "registry:component"
}
