Back to Remotion

Effects

packages/skills/skills/remotion-markup/effects.md

4.0.4987.7 KB
Original Source

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

Use npx remotion add @remotion/light-leaks for lightLeak() and npx remotion add @remotion/starburst for starburst().

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; lightLeak() uses @remotion/light-leaks; starburst() uses @remotion/starburst.

These effects use WebGL2. During renders, enable WebGL with:

ts
import {Config} from '@remotion/cli/config';

Config.setChromiumOpenGlRenderer('angle');

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.