add-effect

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Add a new
@remotion/effects
effect

@remotion/effects
添加新特效

Use this skill when adding a new effect to
@remotion/effects
.
当你需要为
@remotion/effects
添加新特效时,请遵循此流程。

1. Pick the effect shape

1. 确定特效结构

  • Prefer the WebGL2 backend for new effects. Use 2D only when WebGL cannot express the effect.
  • Use a single file at
    packages/effects/src/<effect-name>.ts
    for simple effects.
  • 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.
  • Follow naming already used by the package:
    • File/subpath: kebab-case (
      chromatic-aberration
      )
    • Function: camelCase (
      chromaticAberration
      )
    • Type: PascalCase params (
      ChromaticAberrationParams
      )
    • Effect type string:
      remotion/<kebab-case-name>
  • 新特效优先使用WebGL2后端。仅当WebGL无法实现该特效时,才使用2D方案。
  • 简单特效直接在
    packages/effects/src/<effect-name>.ts
    创建单个文件。
  • 若特效需要多个着色器、运行时工具或多文件支持,则在
    packages/effects/src/<effect-name>/
    创建文件夹,并添加一个顶级重导出文件。
  • 遵循包内已有的命名规范:
    • 文件/子路径:短横线分隔命名(kebab-case),例如
      chromatic-aberration
    • 函数:小驼峰命名(camelCase),例如
      chromaticAberration
    • 类型:大驼峰命名参数(PascalCase),例如
      ChromaticAberrationParams
    • 特效类型字符串:
      remotion/<短横线分隔名称>

2. Implement the effect

2. 实现特效

In the effect file:
  • Import
    SequenceSchema
    and
    Internals
    from
    remotion
    .
  • Use
    const {createEffect, createWebGL2ContextError} = Internals;
    .
  • Define defaults as
    const
    values.
  • Define a schema with
    satisfies SequenceSchema
    ; these fields appear in Studio visual editing.
  • Export the params type.
  • Resolve defaults in a
    resolve()
    helper.
  • Validate params using helpers from:
    • packages/effects/src/validate-effect-param.ts
    • packages/effects/src/color-utils.ts
  • Throw
    createWebGL2ContextError('<effect name> effect')
    if WebGL2 cannot be acquired.
  • Set
    documentationLink
    to
    https://www.remotion.dev/docs/effects/<slug>
    .
  • Include every resolved parameter in
    calculateKey()
    .
For WebGL2 effects, use this general structure:
ts
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.
在特效文件中:
  • remotion
    导入
    SequenceSchema
    Internals
  • 使用
    const {createEffect, createWebGL2ContextError} = Internals;
    解构方法。
  • 将默认值定义为
    const
    常量。
  • 使用
    satisfies SequenceSchema
    定义Schema;这些字段会在Studio可视化编辑中展示。
  • 导出参数类型。
  • resolve()
    工具函数中处理默认值。
  • 使用以下工具函数验证参数:
    • packages/effects/src/validate-effect-param.ts
    • packages/effects/src/color-utils.ts
  • 若无法获取WebGL2上下文,抛出
    createWebGL2ContextError('<effect name> effect')
    错误。
  • documentationLink
    设置为
    https://www.remotion.dev/docs/effects/<slug>
  • calculateKey()
    中包含所有已处理的参数。
WebGL2特效可遵循以下通用结构:
ts
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,
});
在添加新工具函数前,可参考已有的WebGL2特效实现,例如
halftone.ts
blur/blur-runtime.ts
chromatic-aberration/chromatic-aberration-runtime.ts
wave/wave-runtime.ts
。在上述模板中,
createMyEffectState()
代表这些文件中用到的着色器编译、程序链接、全屏四边形、纹理以及统一变量位置的初始化逻辑。

3. Register package entry points

3. 注册包入口

Update:
  • packages/effects/bundle.ts
    — add the new
    src/<effect-name>.ts
    entrypoint.
  • packages/effects/package.json
    :
    • Add
      exports["./<effect-name>"]
      .
    • Add the
      typesVersions
      entry.
If using a folder implementation, add a top-level file that re-exports from the folder:
ts
export {myEffect, type MyEffectParams} from './my-effect/index.js';
更新以下文件:
  • packages/effects/bundle.ts
    — 添加新的
    src/<effect-name>.ts
    入口。
  • packages/effects/package.json
    • 添加
      exports["./<effect-name>"]
      配置。
    • 添加
      typesVersions
      条目。
如果使用文件夹结构实现,添加一个顶级文件来重导出文件夹内的内容:
ts
export {myEffect, type MyEffectParams} from './my-effect/index.js';

4. Add tests

4. 添加测试

Update
packages/effects/src/test/effect-params.test.ts
:
  • Import the new effect.
  • Add it to the documentation link test.
  • Test default params when all fields are optional.
  • Test required params if any are required.
  • Test invalid values and exact error substrings.
  • Test that meaningful params produce distinct
    effectKey
    values.
Run:
bash
cd packages/effects
bun test src/test
bunx turbo make --filter="@remotion/effects"
更新
packages/effects/src/test/effect-params.test.ts
  • 导入新特效。
  • 将其添加到文档链接测试中。
  • 测试所有字段为可选时的默认参数。
  • 若存在必填参数,测试必填参数逻辑。
  • 测试无效值及精确的错误子串。
  • 测试不同有效参数能否生成不同的
    effectKey
    值。
运行:
bash
cd packages/effects
bun test src/test
bunx turbo make --filter="@remotion/effects"

5. Add docs

5. 添加文档

Create
packages/docs/docs/effects/<effect-name>.mdx
.
Follow existing effect pages:
  • Frontmatter:
    slug
    ,
    title
    ,
    sidebar_label
    ,
    crumb: '@remotion/effects'
    .
  • Add
    image:
    only after running
    bun render-cards.ts
    .
  • H1:
    # effectName()<AvailableFrom v="..." />
    .
  • Include
    _Part of the [@remotion/effects](/docs/effects/api) package._
    .
  • Add a short description.
  • Add
    <Demo type="effects-<effect-name>" />
    .
  • Add a twoslash example with
    title="MyComp.tsx"
    .
  • Document each option as its own
    ###
    heading, using
    ?
    for optional parameters.
  • Add a
    disabled?
    section.
  • Add a See also section.
Update:
  • packages/docs/sidebars.ts
    — add
    'effects/<effect-name>'
    .
  • 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.
创建
packages/docs/docs/effects/<effect-name>.mdx
文件。
参考已有特效页面的结构:
  • 前置元数据:
    slug
    title
    sidebar_label
    crumb: '@remotion/effects'
  • 仅在运行
    bun render-cards.ts
    后添加
    image:
    字段。
  • 一级标题:
    # effectName()<AvailableFrom v="..." />
  • 添加
    _Part of the [@remotion/effects](/docs/effects/api) package._
    说明。
  • 添加简短描述。
  • 添加
    <Demo type="effects-<effect-name>" />
    组件。
  • 添加带有
    title="MyComp.tsx"
    的twoslash示例。
  • 将每个选项作为独立的
    ###
    标题进行文档说明,可选参数标注
    ?
  • 添加
    disabled?
    章节。
  • 添加“另请参阅”章节。
更新以下文件:
  • packages/docs/sidebars.ts
    — 添加
    'effects/<effect-name>'
    到侧边栏。
  • packages/docs/docs/effects/table-of-contents.tsx
    — 在对应分类下添加卡片。
  • 通过运行卡片生成器更新
    packages/docs/src/data/articles.ts
    ,请勿手动修改。
文档措辞可参考
writing-docs
技能规范。

6. Add the interactive docs demo

6. 添加交互式文档演示

Create
packages/docs/components/effects/effects-<effect-name>-preview.tsx
.
Use the same preview source as other effects:
tsx
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
    • Import the preview component.
    • Export
      effectsMyEffectDemo
      .
    • Use
      id: 'effects-<effect-name>'
      .
    • Add controls matching the effect schema.
  • packages/docs/components/demos/index.tsx
    • Import and add the demo to the
      demos
      array.
Use the
docs-demo
skill for demo details.
创建
packages/docs/components/effects/effects-<effect-name>-preview.tsx
文件。
参考其他特效的预览代码结构:
tsx
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})]}
		/>
	);
};
文档特效预览请使用
fit="cover"
,确保共享预览图填满16:9画布,避免出现透明边栏。
注册演示组件:
  • packages/docs/components/demos/types.ts
    • 导入预览组件。
    • 导出
      effectsMyEffectDemo
    • 设置
      id: 'effects-<effect-name>'
    • 添加与特效Schema匹配的控件。
  • packages/docs/components/demos/index.tsx
    • 导入并将演示添加到
      demos
      数组中。
演示细节可参考
docs-demo
技能规范。

7. Render the table-of-contents preview image

7. 渲染目录预览图

The TOC card should use a rendered image from the same preview component, not a hand-written SVG.
Create a temporary Remotion entry point for the still render and delete it before committing:
tsx
import React from 'react';
import {Composition, registerRoot} from 'remotion';
import {EffectsMyEffectPreview} from '../../components/effects/effects-my-effect-preview';

const Root: React.FC = () => {
	return (
		<Composition
			id="effects-my-effect-preview"
			component={EffectsMyEffectPreview}
			width={1080}
			height={720}
			fps={30}
			durationInFrames={1}
			defaultProps={{
				amount: 1,
			}}
		/>
	);
};

registerRoot(Root);
Then render from
packages/docs
:
bash
npx --no-install --package @remotion/cli remotion still src/remotion/effects-preview-entry.tsx effects-my-effect-preview static/img/effects-my-effect-preview.jpg --overwrite --image-format=jpeg
Add the rendered image to
packages/docs/static/img/
, reference it from
table-of-contents.tsx
, and delete the temporary entry point before committing.
目录卡片应使用预览组件渲染出的图片,而非手写SVG。
创建一个临时Remotion入口点用于渲染静态图,提交前删除该文件:
tsx
import React from 'react';
import {Composition, registerRoot} from 'remotion';
import {EffectsMyEffectPreview} from '../../components/effects/effects-my-effect-preview';

const Root: React.FC = () => {
	return (
		<Composition
			id="effects-my-effect-preview"
			component={EffectsMyEffectPreview}
			width={1080}
			height={720}
			fps={30}
			durationInFrames={1}
			defaultProps={{
				amount: 1,
			}}
		/>
	);
};

registerRoot(Root);
然后在
packages/docs
目录下运行渲染命令:
bash
npx --no-install --package @remotion/cli remotion still src/remotion/effects-preview-entry.tsx effects-my-effect-preview static/img/effects-my-effect-preview.jpg --overwrite --image-format=jpeg
将渲染后的图片添加到
packages/docs/static/img/
,在
table-of-contents.tsx
中引用该图片,并在提交前删除临时入口点。

8. Generate docs card

8. 生成文档卡片

Run:
bash
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.
运行:
bash
cd packages/docs
bun render-cards.ts
提交生成的
packages/docs/static/generated/articles-docs-effects-<effect-name>.png
文件及新增的
image:
前置元数据行。
如果
render-cards.ts
顺带生成了无关的缺失卡片,除非这些卡片属于当前变更内容,否则请删除这些无关文件。

9. Format, build, and verify

9. 格式化、构建与验证

Run:
bash
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:
bash
git diff --check
git status --short
运行:
bash
cd packages/effects
bunx oxfmt src --write
cd ../..
bun run build
bun run formatting
若变更涉及文档源码,
bun run formatting
会处理
packages/docs/src
目录。仅编辑MDX文件时,请勿对文档页面运行格式化工具。
提交前,请检查:
bash
git diff --check
git status --short

Common pitfalls

常见陷阱

  • Do not forget
    package.json
    exports
    and
    typesVersions
    ; subpath imports like
    @remotion/effects/my-effect
    depend on them.
  • Do not forget
    bundle.ts
    ; otherwise the ESM subpath will not be built.
  • Do not leave temporary render entry points in
    packages/docs/src/remotion
    .
  • Do not use a hand-written SVG for the effect TOC preview.
  • Preserve alpha unless the effect intentionally changes it.
  • For pixel math, be aware canvases store premultiplied alpha.
  • WebGL color math often needs to unpremultiply the sampled RGB before luminance or threshold calculations, then premultiply the output RGB again.
  • 不要忘记配置
    package.json
    中的
    exports
    typesVersions
    ;像
    @remotion/effects/my-effect
    这样的子路径导入依赖这些配置。
  • 不要忘记更新
    bundle.ts
    ;否则ESM子路径将无法构建。
  • 不要在
    packages/docs/src/remotion
    目录下遗留临时渲染入口点。
  • 不要为特效目录预览使用手写SVG。
  • 除非特效有意修改,否则请保留透明度通道。
  • 进行像素计算时,注意画布存储的是预乘透明度(premultiplied alpha)。
  • WebGL颜色计算通常需要在亮度或阈值计算前对采样的RGB值进行去预乘处理,然后再对输出的RGB值进行预乘处理。