with one click
add-effect
// Add a new effect to @remotion/effects, including implementation, package exports, docs, demos, preview images, tests, formatting, and builds.
// Add a new effect to @remotion/effects, including implementation, package exports, docs, demos, preview images, tests, formatting, and builds.
| name | add-effect |
| description | Add a new effect to @remotion/effects, including implementation, package exports, docs, demos, preview images, tests, formatting, and builds. |
@remotion/effects effectUse this skill when adding a new effect to @remotion/effects.
packages/effects/src/<effect-name>.ts for simple effects.packages/effects/src/<effect-name>/ plus a top-level re-export file when the effect needs multiple shaders, runtime helpers, or multiple files.chromatic-aberration)chromaticAberration)ChromaticAberrationParams)remotion/<kebab-case-name>In the effect file:
SequenceSchema and Internals from remotion.const {createEffect, createWebGL2ContextError} = Internals;.const values.satisfies SequenceSchema; these fields appear in Studio visual editing.resolve() helper.packages/effects/src/validate-effect-param.tspackages/effects/src/color-utils.tscreateWebGL2ContextError('<effect name> effect') if WebGL2 cannot be acquired.documentationLink to https://www.remotion.dev/docs/effects/<slug>.calculateKey().For WebGL2 effects, use this general structure:
import type {SequenceSchema} from 'remotion';
import {Internals} from 'remotion';
import {assertOptionalFiniteNumber, validateUnitInterval} from './color-utils.js';
import {assertEffectParamsObject} from './validate-effect-param.js';
const {createEffect, createWebGL2ContextError} = Internals;
const DEFAULT_AMOUNT = 1 as const;
const myEffectSchema = {
amount: {
type: 'number',
min: 0,
max: 1,
step: 0.01,
default: DEFAULT_AMOUNT,
description: 'Amount',
},
} as const satisfies SequenceSchema;
export type MyEffectParams = {
readonly amount?: number;
};
type MyEffectResolved = {
amount: number;
};
const resolve = (p: MyEffectParams): MyEffectResolved => ({
amount: p.amount ?? DEFAULT_AMOUNT,
});
const validateMyEffectParams = (params: MyEffectParams): void => {
assertEffectParamsObject(params, 'My effect');
assertOptionalFiniteNumber(params.amount, 'amount');
validateUnitInterval(params.amount ?? DEFAULT_AMOUNT, 'amount');
};
type MyEffectState = {
readonly gl: WebGL2RenderingContext;
readonly program: WebGLProgram;
readonly vao: WebGLVertexArrayObject;
readonly vbo: WebGLBuffer;
readonly texture: WebGLTexture;
readonly uSource: WebGLUniformLocation | null;
readonly uAmount: WebGLUniformLocation | null;
};
const VERTEX_SHADER = /* glsl */ `#version 300 es
in vec2 aPos;
in vec2 aUv;
out vec2 vUv;
void main() {
vUv = aUv;
gl_Position = vec4(aPos, 0.0, 1.0);
}
`;
const FRAGMENT_SHADER = /* glsl */ `#version 300 es
precision highp float;
in vec2 vUv;
out vec4 fragColor;
uniform sampler2D uSource;
uniform float uAmount;
void main() {
vec4 color = texture(uSource, vUv);
fragColor = vec4(color.rgb * uAmount, color.a);
}
`;
// Follow existing helpers in halftone.ts or a runtime file for shader
// compilation, program linking, fullscreen-quad setup, and texture setup.
export const myEffect = createEffect<MyEffectParams, MyEffectState>({
type: 'remotion/my-effect',
label: 'My Effect',
documentationLink: 'https://www.remotion.dev/docs/effects/my-effect',
backend: 'webgl2',
calculateKey: (params) => {
const r = resolve(params);
return `my-effect-${r.amount}`;
},
setup: (target) => {
const gl = target.getContext('webgl2', {
premultipliedAlpha: true,
alpha: true,
preserveDrawingBuffer: true,
});
if (!gl) {
throw createWebGL2ContextError('my effect effect');
}
gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true);
return createMyEffectState(gl, VERTEX_SHADER, FRAGMENT_SHADER);
},
apply: ({source, width, height, params, state, flipSourceY}) => {
const r = resolve(params);
state.gl.viewport(0, 0, width, height);
state.gl.bindFramebuffer(state.gl.FRAMEBUFFER, null);
state.gl.activeTexture(state.gl.TEXTURE0);
state.gl.bindTexture(state.gl.TEXTURE_2D, state.texture);
state.gl.pixelStorei(state.gl.UNPACK_FLIP_Y_WEBGL, flipSourceY);
state.gl.texImage2D(
state.gl.TEXTURE_2D,
0,
state.gl.RGBA,
state.gl.RGBA,
state.gl.UNSIGNED_BYTE,
source as TexImageSource,
);
state.gl.useProgram(state.program);
if (state.uSource) state.gl.uniform1i(state.uSource, 0);
if (state.uAmount) state.gl.uniform1f(state.uAmount, r.amount);
state.gl.bindVertexArray(state.vao);
state.gl.drawArrays(state.gl.TRIANGLE_STRIP, 0, 4);
},
cleanup: ({gl, program, vao, vbo, texture}) => {
gl.deleteTexture(texture);
gl.deleteBuffer(vbo);
gl.deleteProgram(program);
gl.deleteVertexArray(vao);
},
schema: myEffectSchema,
validateParams: validateMyEffectParams,
});
Look at existing WebGL2 effects such as halftone.ts,
blur/blur-runtime.ts, chromatic-aberration/chromatic-aberration-runtime.ts,
and wave/wave-runtime.ts before adding new helpers. In the template above,
createMyEffectState() stands for the shader compilation, program linking,
fullscreen-quad, texture, and uniform-location setup used by those files.
Update:
packages/effects/bundle.ts — add the new src/<effect-name>.ts entrypoint.packages/effects/package.json:
exports["./<effect-name>"].typesVersions entry.If using a folder implementation, add a top-level file that re-exports from the folder:
export {myEffect, type MyEffectParams} from './my-effect/index.js';
Update packages/effects/src/test/effect-params.test.ts:
effectKey values.Run:
cd packages/effects
bun test src/test
bunx turbo make --filter="@remotion/effects"
Create packages/docs/docs/effects/<effect-name>.mdx.
Follow existing effect pages:
slug, title, sidebar_label, crumb: '@remotion/effects'.image: only after running bun render-cards.ts.# effectName()<AvailableFrom v="..." />._Part of the [@remotion/effects](/docs/effects/api) package._.<Demo type="effects-<effect-name>" />.title="MyComp.tsx".### heading, using ? for optional parameters.disabled? section.Update:
packages/docs/sidebars.ts — add 'effects/<effect-name>' in alphabetical order.packages/docs/docs/effects/table-of-contents.tsx — add a card in the right category.packages/docs/src/data/articles.ts by running the card generator, not by hand.Use the writing-docs skill for documentation wording.
Create packages/docs/components/effects/effects-<effect-name>-preview.tsx.
Use the same preview source as other effects:
import {myEffect} from '@remotion/effects/my-effect';
import React from 'react';
import {CanvasImage} from 'remotion';
import {EFFECTS_PREVIEW_IMAGE_SRC} from './effects-preview-image';
export const EffectsMyEffectPreview: React.FC<{
readonly amount: number;
}> = ({amount}) => {
return (
<CanvasImage
src={EFFECTS_PREVIEW_IMAGE_SRC}
width={1280}
height={720}
fit="cover"
effects={[myEffect({amount})]}
/>
);
};
Use fit="cover" for docs effect previews so the shared preview image fills
the 16:9 canvas and does not leave transparent bars.
Register the demo:
packages/docs/components/demos/types.ts
effectsMyEffectDemo.id: 'effects-<effect-name>'.packages/docs/components/demos/index.tsx
demos array.Use the docs-demo skill for demo details.
The TOC card must come from a real Remotion composition in packages/docs, not a hand-written asset.
Always render preview assets as PNG files.
Add a Still to packages/docs/src/remotion/Root.tsx under the effect-previews folder:
<Still
id="effects-my-effect-preview"
component={EffectsMyEffectPreview}
width={1280}
height={720}
defaultProps={{
amount: 1,
}}
/>
Use the same width and height as the preview component's CanvasImage.
If the preview component uses the shared docs preview image, keep
fit="cover" on CanvasImage. Rendering a 16:9 preview component into a
different aspect ratio can leave black bars in the generated TOC image.
Then render from packages/docs:
bunx remotion still src/remotion/entry.ts effects-my-effect-preview static/img/effects-my-effect-preview.png --overwrite --image-format=png
Commit both:
packages/docs/src/remotion/Root.tsxpackages/docs/static/img/Run:
cd packages/docs
bun render-cards.ts
Commit the generated packages/docs/static/generated/articles-docs-effects-<effect-name>.png and the new image: frontmatter line.
If render-cards.ts opportunistically generates unrelated missing cards, remove those unrelated files unless they belong to the current change.
Run:
cd packages/effects
bunx oxfmt src --write
cd ../..
bun run build
bun run formatting
If the change touches docs source, bun run formatting covers packages/docs/src. For MDX-only edits, do not run formatters on docs pages.
Before committing, check:
git diff --check
git status --short
package.json exports and typesVersions; subpath imports like @remotion/effects/my-effect depend on them.bundle.ts; otherwise the ESM subpath will not be built.packages/docs/src/remotion.Update `AvailableFrom` in a PR to the next Remotion patch version (`main` + `0.0.1`).
Open a pull request for the current feature
Add a new Remotion CLI or config option by creating an AnyRemotionOption, registering CLI parsing, wiring config setters, and updating documentation. Use when adding or converting command-line flags or Remotion options.
Add a new package to the Remotion monorepo, including package scaffolding, monorepo registration, documentation, build scripts, tests, and release checklist updates. Use when creating a new @remotion package.
Guidance for working on Remotion Studio Visual Mode, sequence identity, node paths, symbolicated stacks, override IDs, and hot reload behavior. Use when implementing or debugging visual editing of Sequences.
Best practices for Remotion - Video creation in React