Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "content-skills",
"description": "Customer-facing content and GTM strategy commands plus writing and Remotion video skills.",
"version": "1.3.0",
"version": "1.3.1",
"author": {
"name": "Accelerate Data"
},
Expand Down
2 changes: 1 addition & 1 deletion .codex-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "content-skills",
"description": "Customer-facing content and GTM strategy commands plus writing and Remotion video skills.",
"version": "1.3.0",
"version": "1.3.1",
"author": {
"name": "Accelerate Data"
},
Expand Down
235 changes: 149 additions & 86 deletions skills/humanizer/SKILL.md

Large diffs are not rendered by default.

5 changes: 2 additions & 3 deletions skills/remotion/NOTICE
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ skills/remotion/ — vendored from remotion-dev/remotion

Upstream: https://github.com/remotion-dev/remotion
Source path: packages/skills/skills/remotion/
Vendored from: commit d4061da9d1ad9fb04f3a82fa1c64653d3ed7e93f
Vendored from: commit 67eb129f9ccbaef0f0417b3d35627bc2fd60dde8
License: Remotion License (non-MIT — see https://remotion.pro/license)
Usage posture: Internal Accelerate Data tooling only. Not sold,
sublicensed, or redistributed as a derivative of Remotion.
Expand All @@ -14,8 +14,7 @@ internal plugin is not a derivative work of Remotion.

To refresh from upstream:

git clone --depth=1 --filter=blob:none --sparse \
https://github.com/remotion-dev/remotion.git /tmp/remotion-sparse
git clone --depth=1 --filter=blob:none --sparse https://github.com/remotion-dev/remotion.git /tmp/remotion-sparse
git -C /tmp/remotion-sparse sparse-checkout set packages/skills/skills/remotion
rm -rf skills/remotion
cp -R /tmp/remotion-sparse/packages/skills/skills/remotion skills/remotion
Expand Down
52 changes: 38 additions & 14 deletions skills/remotion/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
---
name: remotion-best-practices
description: Best practices for Remotion - Video creation in React
description: Best practices and domain knowledge for building videos programmatically with Remotion (videos in React/TypeScript, rendered to MP4). Use whenever writing or editing Remotion code — scaffolding a video project, animating with interpolate/spring over useCurrentFrame(), sequencing scenes and transitions, captions/subtitles, audio, GIFs/Lottie/3D, charts, visual effects, dynamic duration via calculateMetadata, or rendering. Consult before reaching for a Remotion API, since its components and props evolve (e.g. @remotion/media).
metadata:
tags: remotion, video, react, animation, composition
---

## When to use

Use this skills whenever you are dealing with Remotion code to obtain the domain-specific knowledge.
Use this skill whenever you are dealing with Remotion code to obtain the domain-specific knowledge.

## New project setup

Expand All @@ -21,10 +21,14 @@ Replace `my-video` with a suitable project name.

## Designing a video

Animate properties using `useCurrentFrame()` and `interpolate()`. Use Easing to customize the timing of the animation.
Before designing visual scenes, layouts, promos, motion graphics, or text-heavy videos, load [rules/video-layout.md](rules/video-layout.md) for video-first layout and text sizing guidance.

Animate properties using `useCurrentFrame()` and `interpolate()`. Prefer `interpolate()` over `spring()` unless physics-based motion is explicitly needed. Use `Easing.bezier()` to customize timing, including jumpy or overshooting motion.

For animations that should be editable in Remotion Studio, keep the `interpolate()` call inline in the `style` prop and use individual CSS transform properties (`scale`, `translate`, `rotate`) instead of composing a `transform` string.

```tsx
import { useCurrentFrame, Easing } from "remotion";
import { useCurrentFrame, Easing, interpolate, useVideoConfig } from "remotion";

export const FadeIn = () => {
const frame = useCurrentFrame();
Expand All @@ -40,6 +44,26 @@ export const FadeIn = () => {
};
```

Prefer:

```tsx
style={{
scale: interpolate(frame, [0, 100], [0, 1]),
translate: interpolate(frame, [0, 100], ["0px 0px", "100px 100px"]),
rotate: interpolate(frame, [0, 100], ["20deg", "90deg"]),
}}
```

Over:

```tsx
const scale = interpolate(frame, [0, 100], [0, 1]);

style={{
transform: `scale(${scale})`,
}}
```

CSS transitions or animations are FORBIDDEN - they will not render correctly.
Tailwind animation class names are FORBIDDEN - they will not render correctly.

Expand Down Expand Up @@ -230,6 +254,14 @@ When needing to visualize audio (spectrum bars, waveforms, bass-reactive effects

When needing to use sound effects, load the [./rules/sfx.md](./rules/sfx.md) file for more information.

## Visual and pixel effects

When creating a visual effect, prefer: 1. normal Remotion/HTML/CSS/SVG/filter/blend/mask animation, 2. a listed effect via [rules/effects.md](rules/effects.md), including on HTML rendered through `<HtmlInCanvas>`, 3. a custom `createEffect()` via [rules/effects.md](rules/effects.md) when the user asks for a reusable/project-specific effect, 4. custom `<HtmlInCanvas onPaint>` via [rules/html-in-canvas.md](rules/html-in-canvas.md) only if no effect fits.

For light leak overlays, see [rules/light-leaks.md](rules/light-leaks.md). Docs: https://www.remotion.dev/docs/effects

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()`, `pattern()`, `pixelate()`, `pixelDissolve()`, `scanlines()`, `speckle()`, `shine()`, `shrinkwrap()`, `vignette()`, `contourLines()`, `checkerboard()`, `halftoneLinearGradient()`, `gridlines()`, `whiteNoise()`, `tvSignalOff()`, `lines()`, `rings()`, `waves()`, `zigzag()`, `lightLeak()`, `starburst()`.

## 3D content

See [rules/3d.md](rules/3d.md) for 3D content in Remotion using Three.js and React Three Fiber.
Expand Down Expand Up @@ -274,18 +306,10 @@ See [rules/gifs.md](rules/gifs.md) for how to display GIFs synchronized with Rem

See [rules/images.md](rules/images.md) for sizing and positioning images, dynamic image paths, and getting image dimensions.

## Light leaks

See [rules/light-leaks.md](rules/light-leaks.md) for light leak overlay effects using `@remotion/light-leaks`.

## Lottie animations

See [rules/lottie.md](rules/lottie.md) for embedding Lottie animations in Remotion.

## HTML in canvas

See [rules/html-in-canvas.md](rules/html-in-canvas.md) if you need to render HTML into a `<canvas>` to apply 2D or WebGL effects via `<HtmlInCanvas>`.

## Measuring DOM nodes

See [rules/measuring-dom-nodes.md](rules/measuring-dom-nodes.md) for measuring DOM element dimensions in Remotion.
Expand Down Expand Up @@ -332,8 +356,8 @@ See [rules/parameters.md](rules/parameters.md) for making a composition parametr

## Maps

For simple maps with little flyovers, consider just using a static images for maps.
For complex maps with many flyovers, consider using Mapbox and animating it. Instructions: [rules/mapbox.md](rules/mapbox.md)
For simple maps with little flyovers, consider using static map images.
For complex maps with animated routes or flyovers, load the maps rule: [rules/maplibre.md](rules/maplibre.md)

## Voiceover

Expand Down
235 changes: 235 additions & 0 deletions skills/remotion/rules/effects.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
---
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
```

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.
Loading