Learn how pixi.js, fragment shaders (frag files), and screen blending modes can recreate popular video editing effects right in the browser.
A .frag file is a fragment shader that alters pixel colors to create visual effects.
It receives texture coordinates, samples the texture, and outputs modified colors via gl_FragColor.
uniform sampler2D uSampler; // texture sampler (built-in pixi.js)
varying vec2 vTextureCoord; // texture coordinates (built-in)
void main(void) {
vec4 color = texture2D(uSampler, vTextureCoord); // sample texture
gl_FragColor = color; // output color
}
The original image or video frame is passed as a sampler2D uniform named uSampler
GPU runs your GLSL code per pixel, reading from vTextureCoord and applying math
Each pixel's final color is written to gl_FragColor, producing the visual effect
Shader effects are made dynamic through uniform variables. Different input values produce different visual changes.
Common types include float, vec2 (2D vector), and vec4.
precision highp float;
varying highp vec2 vTextureCoord;
uniform sampler2D uSampler;
uniform float scale; // zoom factor
uniform float horzIntensity; // horizontal jitter
uniform float vertIntensity; // vertical jitter
void main() {
// Scale texture coordinates
vec2 uv = vec2(
(scale - 1.0) * 0.5 + vTextureCoord.x / scale,
(scale - 1.0) * 0.5 + vTextureCoord.y / scale
);
vec4 tex = texture2D(uSampler, uv);
// Channel-shift for chromatic jitter
vec4 shift1 = texture2D(uSampler, uv + vec2(
-0.05*(scale-1.0)*horzIntensity*2.,
-0.05*(scale-1.0)*vertIntensity*2.));
vec4 shift2 = texture2D(uSampler, uv + vec2(
-0.1*(scale-1.0)*horzIntensity*2.,
-0.1*(scale-1.0)*vertIntensity*2.));
// RGB channel blend
vec3 blend = vec3(tex.r, tex.g, shift1.b);
vec3 result = vec3(shift2.r, blend.g, blend.b);
gl_FragColor = vec4(result, tex.a);
}
Use PIXI.Filter to wrap a fragment shader and apply it to any display object.
Uniform values are updated each frame to create animation.
const uniforms = {
scale: 1.0,
horzIntensity: 0.5,
vertIntensity: 0.5,
};
// Frame-by-frame scale values (low-cost animation)
const scale = [1.0,1.07,1.1,1.13,1.17,1.2,1.2,1.0,
1.0,1.0,1.0,1.0,1.0,1.0,1.0];
// Create a custom filter from frag source
const filter = new PIXI.Filter(null, frag, uniforms);
container.filters = [filter];
// Animate by updating uniforms each frame
let start = 0;
app.ticker.add(() => {
filter.uniforms.scale = scale[start++ % scale.length];
});
Shaders support various uniform types: float, vec2 (2D vector), and vec4 (4D vector).
The blur effect below uses multiple vec2 uniforms for per-channel directional blur.
const uniforms = {
blurSize: 0.0,
angleR: 0.0,
angleG: 0.0,
angleB: 0.0,
moveR: [0, 0], // vec2 — red channel blur direction
moveG: [0, 0], // vec2 — green channel blur direction
moveB: [0, 0], // vec2 — blue channel blur direction
// vec4 — [x, y, viewWidth, viewHeight]
u_ScreenParams: [0, 0, viewWidth, viewHeight]
};
The screen blending mode is ideal for atmospheric effects like rain, fog, and snow.
Black stays black, white stays white, and other colors become lighter—perfect for overlays on dark or transparent backgrounds.
// Screen blending formula per channel:
// result = 1 - (1 - base) * (1 - blend)
//
// Key properties:
// - Any color screened with BLACK = original color
// - Any color screened with WHITE = white
// - Any color screened with another = lighter
//
// In CSS: mix-blend-mode: screen;
//
// In pixi.js:
const sprite = new PIXI.Sprite(texture);
sprite.blendMode = PIXI.BLEND_MODES.SCREEN;
Rather than loading hundreds of PNG frames, use the WebCodecs API to decode MP4 videos directly in the browser. This is far more efficient—one HTTP request instead of hundreds, and files are ~10x smaller than APNG.
// Using mp4box.js + WebCodecs to decode MP4
const mp4box = MP4Box.createFile();
mp4box.onReady = async (info) => {
const track = info.videoTracks[0];
const decoder = new VideoDecoder({
output(frame) {
// Draw each decoded frame to canvas
ctx.drawImage(frame, 0, 0, width, height);
frame.close();
},
error(e) { console.error(e); }
});
// ... configure and decode
};
// Fetch and append MP4 data
const res = await fetch('./lightning.mp4');
const buffer = await res.arrayBuffer();
mp4box.appendBuffer(buffer);
| Format | File Size | HTTP Requests | Decode Speed |
|---|---|---|---|
| 📷 PNG Sequence (100 frames) | ~50 MB | 100 | Fast |
| 🖼️ APNG (animated) | ~30 MB | 1 | Average |
| 🎬 MP4 + WebCodecs | ~3 MB | 1 | Very Fast |
Video filters like those in CapCut often use ColorMapFilter—a technique that maps source colors
through a lookup table (LUT) image. The result is a consistent, film-like color grade applied to every frame.
LUT files come in formats like .cube or .3dl.
// Algorithmic filter (e.g., Gaussian blur, inversion)
// → Mathematical computation, sometimes lacks refinement
ctx.filter = 'blur(4px) hue-rotate(90deg)';
// ColorMap filter (LUT-based)
// → Color mapping via a lookup table image
// → More refined, film-quality color grading
// File formats: .cube, .3dl, or ColorMap PNG
//
// In pixi.js:
const colorMapFilter = new PIXI.ColorMapFilter(
PIXI.Texture.from('./lut.png'),
PIXI.FORMATS.CUBE
);
container.filters = [colorMapFilter];
CapCut-style effects fall into two main categories: shader-based effects (frag filters for distortion, blur, jitter) and blended atmospheric effects (screen-blended video overlays for rain, fog, snow).
Fragment shaders that modify pixel colors directly
Scale, jitter, wave, swirl via shader math
Directional, radial, and per-channel blur effects
Atmospheric: rain, fog, snow, fireworks
MP4 via WebCodecs for frame sequences
Film-grade color grading via lookup tables