$npx -y skills add remotion-dev/remotion --skill add-effectAdd a new effect to @remotion/effects, including implementation, package exports, docs, demos, preview images, Remotion skill updates, tests, formatting, and builds.
| 1 | # Add a new `@remotion/effects` effect |
| 2 | |
| 3 | Use this skill when adding a new effect to `@remotion/effects`. |
| 4 | |
| 5 | ## 1. Pick the effect shape |
| 6 | |
| 7 | - Prefer the WebGL2 backend for new effects. Use 2D only when WebGL cannot express the effect. |
| 8 | - Use a single file at `packages/effects/src/<effect-name>.ts` for simple effects. |
| 9 | - Use a folder at `packages/effects/src/<effect-name>/` plus a top-level re-export file when the effect needs multiple shaders, runtime helpers, or multiple files. |
| 10 | - Follow naming already used by the package: |
| 11 | - File/subpath: kebab-case (`chromatic-aberration`) |
| 12 | - Function: camelCase (`chromaticAberration`) |
| 13 | - Type: PascalCase params (`ChromaticAberrationParams`) |
| 14 | - Effect type string: `remotion/<kebab-case-name>` |
| 15 | |
| 16 | ## 2. Implement the effect |
| 17 | |
| 18 | In the effect file: |
| 19 | |
| 20 | - Import `SequenceSchema` and `Internals` from `remotion`. |
| 21 | - Use `const {createEffect, createWebGL2ContextError} = Internals;`. |
| 22 | - Define defaults as `const` values. |
| 23 | - Define a schema with `satisfies SequenceSchema`; these fields appear in Studio visual editing. |
| 24 | - Export the params type. |
| 25 | - Resolve defaults in a `resolve()` helper. |
| 26 | - Validate params using helpers from: |
| 27 | - `packages/effects/src/validate-effect-param.ts` |
| 28 | - `packages/effects/src/color-utils.ts` |
| 29 | - Throw `createWebGL2ContextError('<effect name> effect')` if WebGL2 cannot be acquired. |
| 30 | - Set `documentationLink` to `https://www.remotion.dev/docs/effects/<slug>`. |
| 31 | - Include every resolved parameter in `calculateKey()`. |
| 32 | |
| 33 | For WebGL2 effects, use this general structure: |
| 34 | |
| 35 | ```ts |
| 36 | import type {SequenceSchema} from 'remotion'; |
| 37 | import {Internals} from 'remotion'; |
| 38 | import {assertOptionalFiniteNumber, validateUnitInterval} from './color-utils.js'; |
| 39 | import {assertEffectParamsObject} from './validate-effect-param.js'; |
| 40 | |
| 41 | const {createEffect, createWebGL2ContextError} = Internals; |
| 42 | |
| 43 | const DEFAULT_AMOUNT = 1 as const; |
| 44 | |
| 45 | const myEffectSchema = { |
| 46 | amount: { |
| 47 | type: 'number', |
| 48 | min: 0, |
| 49 | max: 1, |
| 50 | step: 0.01, |
| 51 | default: DEFAULT_AMOUNT, |
| 52 | description: 'Amount', |
| 53 | }, |
| 54 | } as const satisfies SequenceSchema; |
| 55 | |
| 56 | export type MyEffectParams = { |
| 57 | readonly amount?: number; |
| 58 | }; |
| 59 | |
| 60 | type MyEffectResolved = { |
| 61 | amount: number; |
| 62 | }; |
| 63 | |
| 64 | const resolve = (p: MyEffectParams): MyEffectResolved => ({ |
| 65 | amount: p.amount ?? DEFAULT_AMOUNT, |
| 66 | }); |
| 67 | |
| 68 | const validateMyEffectParams = (params: MyEffectParams): void => { |
| 69 | assertEffectParamsObject(params, 'My effect'); |
| 70 | assertOptionalFiniteNumber(params.amount, 'amount'); |
| 71 | validateUnitInterval(params.amount ?? DEFAULT_AMOUNT, 'amount'); |
| 72 | }; |
| 73 | |
| 74 | type MyEffectState = { |
| 75 | readonly gl: WebGL2RenderingContext; |
| 76 | readonly program: WebGLProgram; |
| 77 | readonly vao: WebGLVertexArrayObject; |
| 78 | readonly vbo: WebGLBuffer; |
| 79 | readonly texture: WebGLTexture; |
| 80 | readonly uSource: WebGLUniformLocation | null; |
| 81 | readonly uAmount: WebGLUniformLocation | null; |
| 82 | }; |
| 83 | |
| 84 | const VERTEX_SHADER = /* glsl */ `#version 300 es |
| 85 | in vec2 aPos; |
| 86 | in vec2 aUv; |
| 87 | out vec2 vUv; |
| 88 | |
| 89 | void main() { |
| 90 | vUv = aUv; |
| 91 | gl_Position = vec4(aPos, 0.0, 1.0); |
| 92 | } |
| 93 | `; |
| 94 | |
| 95 | const FRAGMENT_SHADER = /* glsl */ `#version 300 es |
| 96 | precision highp float; |
| 97 | |
| 98 | in vec2 vUv; |
| 99 | out vec4 fragColor; |
| 100 | |
| 101 | uniform sampler2D uSource; |
| 102 | uniform float uAmount; |
| 103 | |
| 104 | void main() { |
| 105 | vec4 color = texture(uSource, vUv); |
| 106 | fragColor = vec4(color.rgb * uAmount, color.a); |
| 107 | } |
| 108 | `; |
| 109 | |
| 110 | // Follow existing helpers in halftone.ts or a runtime file for shader |
| 111 | // compilation, program linking, fullscreen-quad setup, and texture setup. |
| 112 | |
| 113 | export const myEffect = createEffect<MyEffectParams, MyEffectState>({ |
| 114 | type: 'remotion/my-effect', |
| 115 | label: 'My Effect', |
| 116 | documentationLink: 'https://www.remotion.dev/docs/effects/my-effect', |
| 117 | backend: 'webgl2', |
| 118 | calculateKey: (params) => { |
| 119 | const r = resolve(params); |
| 120 | return `my-effect-${r.amount}`; |
| 121 | }, |
| 122 | setup: (target) => { |
| 123 | const gl = target.getContext('webgl2', { |
| 124 | premultipliedAlpha: true, |
| 125 | alpha: true, |
| 126 | preserveDrawingBuffer: true, |
| 127 | }); |
| 128 | if (!gl) { |
| 129 | throw createWebGL2ContextError('my effect effect'); |
| 130 | } |
| 131 | |
| 132 | gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true); |
| 133 | |
| 134 | return createMyEffectState(gl, VERTEX_SHADER, FRAGMENT_SHADER); |
| 135 | }, |
| 136 | apply: ({source, width, height, params, state, flipSourceY}) => { |
| 137 | const r = resolve(params); |
| 138 | |
| 139 | state.gl.viewport(0, 0, width, height); |
| 140 | state.gl.bindFramebuffer(state.gl.FRAMEBUFFER, null); |
| 141 | state.gl.activeTexture(state.gl.TEXTURE0); |
| 142 | state.gl.bindTexture(state.gl.TEXTURE_2D, state.texture); |
| 143 | state.gl.pixelStorei(state.gl.UNPACK_FLIP_Y_WEBGL, flipSourceY); |
| 144 | state.gl.texImage2D( |
| 145 | state.gl.TEXTURE_2D, |
| 146 | 0, |
| 147 | state.gl.RGBA, |
| 148 | state.gl.RGBA, |
| 149 | state.gl.UNSIGNED_BYTE, |
| 150 | source as TexImageSource, |
| 151 | ); |
| 152 | |
| 153 | state.gl.useProgram(state.program); |
| 154 | if (state.uSource) state.gl.uniform1i(state.uSource, 0); |
| 155 | if (state.uAmount) state.gl.uniform1f(state.uAmount, r.amount); |
| 156 | state.gl.bindVertexArray(state.vao); |
| 157 | state.gl.drawArrays(state.gl.TRIANGLE_STRIP, 0, 4); |
| 158 | }, |