Catalog

Edit Props (live)

config
config
config
config

Source

320 lines
import React, { useEffect, useRef, useMemo } from "react";
import {
  AbsoluteFill,
  useCurrentFrame,
  useVideoConfig,
  staticFile,
  continueRender,
  delayRender,
} from "remotion";

type ShaderName = "glitch" | "domain-warp" | "whip-pan" | "ridged-burn" | "sdf-iris";

interface ShaderTransitionProps {
  fromSrc: string;
  toSrc: string;
  shader: ShaderName;
  accent?: [number, number, number];
}

const VERT = `
attribute vec2 a_pos;
varying vec2 v_uv;
void main() {
  v_uv = (a_pos + 1.0) * 0.5;
  gl_Position = vec4(a_pos, 0.0, 1.0);
}
`;

const COMMON_GLSL = `
precision highp float;
varying vec2 v_uv;
uniform sampler2D u_from;
uniform sampler2D u_to;
uniform float u_progress;
uniform vec2 u_resolution;
uniform vec3 u_accent;

float hash(vec2 p){return fract(sin(dot(p,vec2(127.1,311.7)))*43758.5453);}
float noise(vec2 p){
  vec2 i=floor(p), f=fract(p);
  float a=hash(i),b=hash(i+vec2(1,0)),c=hash(i+vec2(0,1)),d=hash(i+vec2(1,1));
  vec2 u=f*f*(3.0-2.0*f);
  return mix(mix(a,b,u.x),mix(c,d,u.x),u.y);
}
float fbm(vec2 p){
  float v=0.0, a=0.5;
  for(int i=0;i<5;i++){v+=a*noise(p);p*=2.03;a*=0.5;}
  return v;
}
`;

const FRAGS: Record<ShaderName, string> = {
  glitch: `${COMMON_GLSL}
void main() {
  vec2 uv = v_uv;
  float p = u_progress;
  float intensity = sin(p * 3.14159);
  // Scanline displacement
  float scan = step(0.92, hash(vec2(floor(uv.y*80.0), floor(p*30.0))));
  float offset = (hash(vec2(floor(uv.y*40.0), floor(p*25.0))) - 0.5) * 0.15 * intensity * scan;
  vec2 uv2 = vec2(uv.x + offset, uv.y);
  // Chromatic aberration
  float ca = 0.02 * intensity;
  float r = mix(texture2D(u_from, uv2 + vec2(ca,0)).r, texture2D(u_to, uv2 + vec2(ca,0)).r, p);
  float g = mix(texture2D(u_from, uv2).g, texture2D(u_to, uv2).g, p);
  float b = mix(texture2D(u_from, uv2 - vec2(ca,0)).b, texture2D(u_to, uv2 - vec2(ca,0)).b, p);
  vec3 col = vec3(r,g,b);
  // Random blocks scramble
  float block = step(0.85, hash(vec2(floor(uv.y*15.0), floor(p*20.0))));
  vec2 jitter = vec2((hash(vec2(floor(uv.y*15.0), p)) - 0.5) * 0.25, 0);
  col = mix(col, mix(texture2D(u_from, uv+jitter).rgb, texture2D(u_to, uv+jitter).rgb, p), block * intensity);
  // Scanline darkening
  col *= 0.85 + 0.15 * sin(uv.y * u_resolution.y * 0.8);
  // Posterize color levels during peak intensity
  float levels = mix(256.0, 12.0, intensity);
  col = floor(col * levels) / levels;
  // Brightness flicker
  col *= 1.0 + (hash(vec2(floor(p*30.0), 0)) - 0.5) * 0.3 * intensity;
  gl_FragColor = vec4(col, 1.0);
}
`,
  "domain-warp": `${COMMON_GLSL}
void main() {
  vec2 uv = v_uv;
  float p = u_progress;
  // Two FBM layers create warp vectors
  vec2 warp = vec2(fbm(uv * 3.0 + p * 2.0), fbm(uv * 3.0 + 20.0 - p * 2.0));
  warp = (warp - 0.5) * 0.4;
  vec2 fromUV = uv + warp * p;
  vec2 toUV = uv - warp * (1.0 - p);
  vec4 from = texture2D(u_from, clamp(fromUV, 0.0, 1.0));
  vec4 to = texture2D(u_to, clamp(toUV, 0.0, 1.0));
  // Dissolve threshold
  float thresh = fbm(uv * 4.0);
  float m = smoothstep(p - 0.15, p + 0.15, thresh);
  vec3 col = mix(to.rgb, from.rgb, m);
  // Glow edge
  float edge = 1.0 - abs(thresh - p) * 10.0;
  edge = max(edge, 0.0);
  col += u_accent * edge * sin(p * 3.14159) * 1.2;
  gl_FragColor = vec4(col, 1.0);
}
`,
  "whip-pan": `${COMMON_GLSL}
void main() {
  vec2 uv = v_uv;
  float p = u_progress;
  float blur = sin(p * 3.14159) * 0.25;
  vec3 fromCol = vec3(0.0);
  vec3 toCol = vec3(0.0);
  for (int i = 0; i < 10; i++) {
    float t = float(i) / 10.0;
    fromCol += texture2D(u_from, vec2(uv.x + blur * t + p * 1.5, uv.y)).rgb;
    toCol += texture2D(u_to, vec2(uv.x - blur * (1.0-t) + (p-1.0) * 1.5, uv.y)).rgb;
  }
  fromCol /= 10.0;
  toCol /= 10.0;
  float m = smoothstep(0.4, 0.6, p);
  gl_FragColor = vec4(mix(fromCol, toCol, m), 1.0);
}
`,
  "ridged-burn": `${COMMON_GLSL}
float ridge(vec2 p){return 1.0 - abs(noise(p) - 0.5) * 2.0;}
float ridgedFbm(vec2 p){
  float v=0.0, a=0.5;
  for(int i=0;i<5;i++){v+=a*ridge(p);p*=2.05;a*=0.5;}
  return v;
}
void main() {
  vec2 uv = v_uv;
  float p = u_progress;
  float n = ridgedFbm(uv * 5.0 + p * 3.0);
  float edge = smoothstep(p - 0.05, p + 0.05, n);
  vec3 dark = u_accent * 0.3;
  vec3 bright = u_accent * 1.5 + vec3(0.3, 0.1, 0.0);
  vec3 glow = mix(dark, bright, smoothstep(0.0, 0.08, n - p + 0.08));
  glow = mix(glow, vec3(1.0, 0.95, 0.7), smoothstep(-0.03, 0.03, n - p));
  // Sparks
  float spark = step(0.93, noise(uv * 80.0 + p * 20.0));
  vec3 from = texture2D(u_from, uv).rgb;
  vec3 to = texture2D(u_to, uv).rgb;
  vec3 col = mix(to, from, edge);
  // Burning edge glow
  float edgeBand = smoothstep(0.08, 0.0, abs(n - p));
  col = mix(col, glow, edgeBand * sin(p * 3.14159));
  col += spark * edgeBand * vec3(1.0, 0.8, 0.4) * sin(p * 3.14159);
  gl_FragColor = vec4(col, 1.0);
}
`,
  "sdf-iris": `${COMMON_GLSL}
void main() {
  vec2 uv = v_uv;
  float p = u_progress;
  vec2 cen = uv - 0.5;
  cen.x *= u_resolution.x / u_resolution.y;
  float d = length(cen);
  float radius = p * 0.9;
  float m = smoothstep(radius - 0.01, radius + 0.01, d);
  vec3 from = texture2D(u_from, uv).rgb;
  vec3 to = texture2D(u_to, uv).rgb;
  vec3 col = mix(to, from, m);
  // Concentric glow rings
  float glow = exp(-abs(d - radius) * 25.0);
  glow += exp(-abs(d - radius * 1.05) * 40.0) * 0.6;
  glow += exp(-abs(d - radius * 0.95) * 40.0) * 0.6;
  col += u_accent * glow * sin(p * 3.14159) * 1.5;
  gl_FragColor = vec4(col, 1.0);
}
`,
};

function compileShader(gl: WebGLRenderingContext, type: number, source: string): WebGLShader {
  const sh = gl.createShader(type)!;
  gl.shaderSource(sh, source);
  gl.compileShader(sh);
  if (!gl.getShaderParameter(sh, gl.COMPILE_STATUS)) {
    const log = gl.getShaderInfoLog(sh);
    gl.deleteShader(sh);
    throw new Error(`Shader compile failed: ${log}`);
  }
  return sh;
}

function loadImage(src: string): Promise<HTMLImageElement> {
  return new Promise((resolve, reject) => {
    const img = new Image();
    img.crossOrigin = "anonymous";
    img.onload = () => resolve(img);
    img.onerror = reject;
    img.src = src;
  });
}

export const ShaderTransition: React.FC<ShaderTransitionProps> = ({
  fromSrc,
  toSrc,
  shader,
  accent = [0.31, 0.6, 0.86],
}) => {
  const canvasRef = useRef<HTMLCanvasElement>(null);
  const frame = useCurrentFrame();
  const { durationInFrames, width, height } = useVideoConfig();
  const glRef = useRef<WebGLRenderingContext | null>(null);
  const progRef = useRef<WebGLProgram | null>(null);
  const texFromRef = useRef<WebGLTexture | null>(null);
  const texToRef = useRef<WebGLTexture | null>(null);
  const readyRef = useRef(false);
  const handleRef = useRef<number | null>(null);

  const fromUrl = useMemo(() => staticFile(fromSrc), [fromSrc]);
  const toUrl = useMemo(() => staticFile(toSrc), [toSrc]);

  // Initialize GL + load textures once
  useEffect(() => {
    const canvas = canvasRef.current;
    if (!canvas) return;
    const handle = delayRender(`shader-init-${shader}`);
    handleRef.current = handle;

    const gl = canvas.getContext("webgl", { preserveDrawingBuffer: true });
    if (!gl) {
      continueRender(handle);
      return;
    }
    glRef.current = gl;

    // Compile program
    const vs = compileShader(gl, gl.VERTEX_SHADER, VERT);
    const fs = compileShader(gl, gl.FRAGMENT_SHADER, FRAGS[shader]);
    const prog = gl.createProgram()!;
    gl.attachShader(prog, vs);
    gl.attachShader(prog, fs);
    gl.linkProgram(prog);
    if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) {
      console.error("Program link failed:", gl.getProgramInfoLog(prog));
      continueRender(handle);
      return;
    }
    progRef.current = prog;

    // Quad buffer
    const buf = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, buf);
    gl.bufferData(
      gl.ARRAY_BUFFER,
      new Float32Array([-1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, 1]),
      gl.STATIC_DRAW
    );
    const loc = gl.getAttribLocation(prog, "a_pos");
    gl.enableVertexAttribArray(loc);
    gl.vertexAttribPointer(loc, 2, gl.FLOAT, false, 0, 0);

    // Load textures
    Promise.all([loadImage(fromUrl), loadImage(toUrl)])
      .then(([fromImg, toImg]) => {
        const makeTex = (img: HTMLImageElement) => {
          const tex = gl.createTexture();
          gl.bindTexture(gl.TEXTURE_2D, tex);
          gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, img);
          gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
          gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
          gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
          gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
          return tex;
        };
        texFromRef.current = makeTex(fromImg);
        texToRef.current = makeTex(toImg);
        readyRef.current = true;
        continueRender(handle);
      })
      .catch((err) => {
        console.error("Texture load failed:", err);
        continueRender(handle);
      });

    return () => {
      if (texFromRef.current) gl.deleteTexture(texFromRef.current);
      if (texToRef.current) gl.deleteTexture(texToRef.current);
      if (progRef.current) gl.deleteProgram(progRef.current);
    };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [shader, fromUrl, toUrl]);

  // Render this frame
  useEffect(() => {
    const gl = glRef.current;
    const prog = progRef.current;
    if (!gl || !prog || !readyRef.current) return;
    const progress = Math.max(0, Math.min(1, frame / Math.max(1, durationInFrames - 1)));

    gl.viewport(0, 0, width, height);
    gl.useProgram(prog);

    gl.activeTexture(gl.TEXTURE0);
    gl.bindTexture(gl.TEXTURE_2D, texFromRef.current);
    gl.uniform1i(gl.getUniformLocation(prog, "u_from"), 0);

    gl.activeTexture(gl.TEXTURE1);
    gl.bindTexture(gl.TEXTURE_2D, texToRef.current);
    gl.uniform1i(gl.getUniformLocation(prog, "u_to"), 1);

    gl.uniform1f(gl.getUniformLocation(prog, "u_progress"), progress);
    gl.uniform2f(gl.getUniformLocation(prog, "u_resolution"), width, height);
    gl.uniform3f(gl.getUniformLocation(prog, "u_accent"), accent[0], accent[1], accent[2]);

    gl.drawArrays(gl.TRIANGLES, 0, 6);
  }, [frame, durationInFrames, width, height, accent]);

  return (
    <AbsoluteFill style={{ backgroundColor: "#000" }}>
      <canvas
        ref={canvasRef}
        width={width}
        height={height}
        style={{ width: "100%", height: "100%", display: "block" }}
      />
    </AbsoluteFill>
  );
};

Schema (Zod)

// Auto-extracted from Props interface — see source for authoritative types.
// Component: ShaderTransition
// 4 props detected · 0 port(s)

Shader Transition

shader_transition

Shader Transition — Andy's hand-crafted Remotion scene. 4 props. no ports detected — source-only preview.

Workflow Inputs (0)

This block has no workflow-connected inputs.

Config Fields (4)

  • toSrc
  • accent
  • shader
  • fromSrc

Meta

Updated
4/21/2026