← Catalog
Edit Props (live)
config
config
config
config
Source
320 linesimport 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_transitionShader 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)
toSrcaccentshaderfromSrc
Meta
- Updated
- 4/21/2026