8.7 KiB
name, description, metadata
| name | description | metadata | ||
|---|---|---|---|---|
| effects | Canvas/WebGL visual effects for Remotion using effects arrays and createEffect(). |
|
Use this rule only when the top-level skill lists an effect that matches the requested look, or when the user asks to create a reusable custom effect.
Docs: https://www.remotion.dev/docs/effects Custom effect docs: https://www.remotion.dev/docs/create-effect
Usage
Install the package that provides the chosen effect:
npx remotion add @remotion/effects
Effects are functions passed to the effects prop of canvas-based components such as <Video> from @remotion/media, <Solid>, <CanvasImage>, and <HtmlInCanvas>.
import {Video} from '@remotion/media';
import {blur} from '@remotion/effects/blur';
<Video src="https://remotion.media/video.mp4" effects={[blur({radius: 8})]} />;
Use the effect docs for exact props and imports. Most @remotion/effects imports use @remotion/effects/<effect-slug>; uvTranslate() and xyTranslate() use @remotion/effects/translate.
These effects use WebGL2. During renders, enable WebGL with:
import {Config} from '@remotion/cli/config';
Config.setChromiumOpenGlRenderer('angle');
Available effects
brightness(), contrast(), colorKey(), duotone(), grayscale(), hue(), invert(), saturation(), tint(), linearGradient(), linearGradientTint(), thermalVision(), blur(), linearProgressiveBlur(), radialProgressiveBlur(), zoomBlur(), dropShadow(), glow(), lightTrail(), evolve(), venetianBlinds(), mirror(), scale(), uvTranslate(), xyTranslate(), barrelDistortion(), chromaticAberration(), fisheye(), cornerPin(), wave(), burlap(), emboss(), dotGrid(), halftone(), noise(), noiseDisplacement(), paper(), roughenEdges(), pattern(), pixelate(), pixelDissolve(), scanlines(), speckle(), shine(), shrinkwrap(), vignette(), contourLines(), checkerboard(), halftoneLinearGradient(), gridlines(), whiteNoise(), tvSignalOff(), lines(), rings(), waves(), zigzag(), lightLeak(), starburst().
Example:
import {brightness} from "@remotion/effects";
<Video src="https://remotion.media/video.mp4" effects={[brightness({})]} />;
Custom effects
Use createEffect() from remotion when the user wants a reusable effect factory that works in the same effects array as @remotion/effects.
Prefer a custom effect over <HtmlInCanvas onPaint> when the transformation should be reusable, parameterized, editable in Studio, or stackable with other effects.
For quick project-specific effects, keep the effect next to the composition, for example src/effects/palette-map.ts. For library effects intended for @remotion/effects, follow the repository's add-effect skill instead.
createEffect() expects:
type: stable reverse-DNS identifier, for examplecom.example.paletteMap.label: Studio label, commonlypaletteMap().documentationLink: URL ornull.backend:"2d","webgl2"or"webgpu".calculateKey(params): stable string containing every resolved parameter that changes output.setup(target): create reusable backend state, or returnnull.apply({source, target, width, height, params, state, flipSourceY}): draw the transformed result intotarget.cleanup(state): free resources created bysetup().schema: anInteractivitySchemafor Studio controls.disabledis added automatically.validateParams(params): throw on missing or invalid values.
Use backend: "2d" for simple pixel, filter, drawImage, or image-data effects. Use WebGL2 only when shader math or GPU performance is needed; during renders, enable WebGL as shown above.
import {createEffect, type InteractivitySchema} from 'remotion';
type MyEffectParams = {
readonly amount?: number;
};
const myEffectSchema = {
amount: {
type: 'number',
min: 0,
max: 1,
step: 0.01,
default: 1,
description: 'Amount',
},
} as const satisfies InteractivitySchema;
const resolve = (params: MyEffectParams) => ({
amount: params.amount ?? 1,
});
export const myEffect = createEffect<MyEffectParams, null>({
type: 'com.example.myEffect',
label: 'myEffect()',
documentationLink: null,
backend: '2d',
calculateKey: (params) => {
const {amount} = resolve(params);
return `my-effect-${amount}`;
},
setup: () => null,
apply: ({source, target, width, height, params}) => {
const ctx = target.getContext('2d');
if (!ctx) {
throw new Error('Could not get a 2D context for myEffect().');
}
const {amount} = resolve(params);
ctx.clearRect(0, 0, width, height);
ctx.filter = `opacity(${amount * 100}%)`;
ctx.drawImage(source, 0, 0, width, height);
ctx.filter = 'none';
},
cleanup: () => undefined,
schema: myEffectSchema,
validateParams: ({amount = 1}) => {
if (typeof amount !== 'number' || !Number.isFinite(amount) || amount < 0 || amount > 1) {
throw new TypeError('amount must be a number between 0 and 1');
}
},
});
For a WebGL2 effect, compile/link shaders in setup(), keep the program, fullscreen quad, texture, and uniform locations in state, upload source in apply(), and free GPU resources in cleanup(). Minimal shape:
import {createEffect, type InteractivitySchema} from 'remotion';
type RgbShiftParams = {
readonly amount?: number;
};
type RgbShiftState = {
readonly gl: WebGL2RenderingContext;
readonly program: WebGLProgram;
readonly vao: WebGLVertexArrayObject;
readonly vbo: WebGLBuffer;
readonly texture: WebGLTexture;
readonly uSource: WebGLUniformLocation | null;
readonly uOffset: WebGLUniformLocation | null;
};
const rgbShiftSchema = {
amount: {
type: 'number',
min: 0,
max: 80,
step: 1,
default: 12,
description: 'Amount',
},
} as const satisfies InteractivitySchema;
export const rgbShift = createEffect<RgbShiftParams, RgbShiftState>({
type: 'com.example.rgbShift',
label: 'rgbShift()',
documentationLink: null,
backend: 'webgl2',
calculateKey: ({amount = 12}) => `rgb-shift-${amount}`,
setup: (target) => {
const gl = target.getContext('webgl2', {
premultipliedAlpha: true,
alpha: true,
preserveDrawingBuffer: true,
});
if (!gl) {
throw new Error('Could not get a WebGL2 context for rgbShift().');
}
gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true);
// Compile/link shaders, create a fullscreen quad VAO/VBO, create a
// CLAMP_TO_EDGE RGBA texture, and get uSource/uOffset uniform locations.
return createRgbShiftState(gl);
},
apply: ({source, width, height, params, state, flipSourceY}) => {
const amount = params.amount ?? 12;
const {gl} = state;
gl.viewport(0, 0, width, height);
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, flipSourceY);
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, state.texture);
gl.texImage2D(
gl.TEXTURE_2D,
0,
gl.RGBA,
gl.RGBA,
gl.UNSIGNED_BYTE,
source as TexImageSource,
);
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
gl.useProgram(state.program);
if (state.uSource) gl.uniform1i(state.uSource, 0);
if (state.uOffset) gl.uniform2f(state.uOffset, amount / width, 0);
gl.bindVertexArray(state.vao);
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
},
cleanup: ({gl, program, vao, vbo, texture}) => {
gl.deleteTexture(texture);
gl.deleteBuffer(vbo);
gl.deleteProgram(program);
gl.deleteVertexArray(vao);
},
schema: rgbShiftSchema,
validateParams: ({amount = 12}) => {
if (typeof amount !== 'number' || !Number.isFinite(amount) || amount < 0 || amount > 80) {
throw new TypeError('amount must be a number between 0 and 80');
}
},
});
For a complete 2D and WebGL2 pair, see packages/example/src/EffectsTestbed/sample-posterize-2d.ts and packages/example/src/EffectsTestbed/sample-rgb-shift-webgl.ts.
Use the returned factory in an effects array:
import {CanvasImage, staticFile} from 'remotion';
import {myEffect} from './effects/my-effect';
export const MyComp: React.FC = () => {
return (
<CanvasImage
src={staticFile('image.png')}
effects={[myEffect({amount: 0.8})]}
/>
);
};
When generating a custom effect, also:
- Include
disabled?: booleanonly through the returned factory; do not add it to the custom params type or schema. - Validate required parameters at factory-call time with
validateParams. - Include all defaults in both
schemaand theresolve()helper. - Reset mutable 2D context state such as
filter,globalAlpha, transforms, and compositing after drawing. - Preserve alpha unless the requested effect intentionally changes transparency.