246 lines
8.7 KiB
Markdown
246 lines
8.7 KiB
Markdown
---
|
|
name: effects
|
|
description: Canvas/WebGL visual effects for Remotion using effects arrays and createEffect().
|
|
metadata:
|
|
tags: effects, visual-effects, webgl, canvas, video, create-effect
|
|
---
|
|
|
|
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:
|
|
|
|
```bash
|
|
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>`.
|
|
|
|
```tsx
|
|
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:
|
|
|
|
```ts
|
|
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:
|
|
|
|
```tsx
|
|
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 example `com.example.paletteMap`.
|
|
- `label`: Studio label, commonly `paletteMap()`.
|
|
- `documentationLink`: URL or `null`.
|
|
- `backend`: `"2d"`, `"webgl2"` or `"webgpu"`.
|
|
- `calculateKey(params)`: stable string containing every resolved parameter that changes output.
|
|
- `setup(target)`: create reusable backend state, or return `null`.
|
|
- `apply({source, target, width, height, params, state, flipSourceY})`: draw the transformed result into `target`.
|
|
- `cleanup(state)`: free resources created by `setup()`.
|
|
- `schema`: an `InteractivitySchema` for Studio controls. `disabled` is 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.
|
|
|
|
```ts
|
|
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:
|
|
|
|
```ts
|
|
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:
|
|
|
|
```tsx
|
|
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?: boolean` only 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 `schema` and the `resolve()` helper.
|
|
- Reset mutable 2D context state such as `filter`, `globalAlpha`, transforms, and compositing after drawing.
|
|
- Preserve alpha unless the requested effect intentionally changes transparency.
|