上传文件至「rules」
This commit is contained in:
86
rules/3d.md
Normal file
86
rules/3d.md
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
---
|
||||||
|
name: 3d
|
||||||
|
description: 3D content in Remotion using Three.js and React Three Fiber.
|
||||||
|
metadata:
|
||||||
|
tags: 3d, three, threejs
|
||||||
|
---
|
||||||
|
|
||||||
|
# Using Three.js and React Three Fiber in Remotion
|
||||||
|
|
||||||
|
Follow React Three Fiber and Three.js best practices.
|
||||||
|
Only the following Remotion-specific rules need to be followed:
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
First, the `@remotion/three` package needs to be installed.
|
||||||
|
If it is not, use the following command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx remotion add @remotion/three # If project uses npm
|
||||||
|
bunx remotion add @remotion/three # If project uses bun
|
||||||
|
yarn remotion add @remotion/three # If project uses yarn
|
||||||
|
pnpm exec remotion add @remotion/three # If project uses pnpm
|
||||||
|
```
|
||||||
|
|
||||||
|
## Using ThreeCanvas
|
||||||
|
|
||||||
|
You MUST wrap 3D content in `<ThreeCanvas>` and include proper lighting.
|
||||||
|
`<ThreeCanvas>` MUST have a `width` and `height` prop.
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { ThreeCanvas } from "@remotion/three";
|
||||||
|
import { useVideoConfig } from "remotion";
|
||||||
|
|
||||||
|
const { width, height } = useVideoConfig();
|
||||||
|
|
||||||
|
<ThreeCanvas width={width} height={height}>
|
||||||
|
<ambientLight intensity={0.4} />
|
||||||
|
<directionalLight position={[5, 5, 5]} intensity={0.8} />
|
||||||
|
<mesh>
|
||||||
|
<sphereGeometry args={[1, 32, 32]} />
|
||||||
|
<meshStandardMaterial color="red" />
|
||||||
|
</mesh>
|
||||||
|
</ThreeCanvas>
|
||||||
|
```
|
||||||
|
|
||||||
|
## No animations not driven by `useCurrentFrame()`
|
||||||
|
|
||||||
|
Shaders, models etc MUST NOT animate by themselves.
|
||||||
|
No animations are allowed unless they are driven by `useCurrentFrame()`.
|
||||||
|
Otherwise, it will cause flickering during rendering.
|
||||||
|
|
||||||
|
Using `useFrame()` from `@react-three/fiber` is forbidden.
|
||||||
|
|
||||||
|
## Animate using `useCurrentFrame()`
|
||||||
|
|
||||||
|
Use `useCurrentFrame()` to perform animations.
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
const frame = useCurrentFrame();
|
||||||
|
const rotationY = frame * 0.02;
|
||||||
|
|
||||||
|
<mesh rotation={[0, rotationY, 0]}>
|
||||||
|
<boxGeometry args={[2, 2, 2]} />
|
||||||
|
<meshStandardMaterial color="#4a9eff" />
|
||||||
|
</mesh>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Using `<Sequence>` inside `<ThreeCanvas>`
|
||||||
|
|
||||||
|
The `layout` prop of any `<Sequence>` inside a `<ThreeCanvas>` must be set to `none`.
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { Sequence } from "remotion";
|
||||||
|
import { ThreeCanvas } from "@remotion/three";
|
||||||
|
|
||||||
|
const { width, height } = useVideoConfig();
|
||||||
|
|
||||||
|
<ThreeCanvas width={width} height={height}>
|
||||||
|
<Sequence layout="none">
|
||||||
|
<mesh>
|
||||||
|
<boxGeometry args={[2, 2, 2]} />
|
||||||
|
<meshStandardMaterial color="#4a9eff" />
|
||||||
|
</mesh>
|
||||||
|
</Sequence>
|
||||||
|
</ThreeCanvas>
|
||||||
|
```
|
||||||
29
rules/animations.md
Normal file
29
rules/animations.md
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
---
|
||||||
|
name: animations
|
||||||
|
description: Fundamental animation skills for Remotion
|
||||||
|
metadata:
|
||||||
|
tags: animations, transitions, frames, useCurrentFrame
|
||||||
|
---
|
||||||
|
|
||||||
|
All animations MUST be driven by the `useCurrentFrame()` hook.
|
||||||
|
Write animations in seconds and multiply them by the `fps` value from `useVideoConfig()`.
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { useCurrentFrame } from "remotion";
|
||||||
|
|
||||||
|
export const FadeIn = () => {
|
||||||
|
const frame = useCurrentFrame();
|
||||||
|
const { fps } = useVideoConfig();
|
||||||
|
|
||||||
|
const opacity = interpolate(frame, [0, 2 * fps], [0, 1], {
|
||||||
|
extrapolateRight: 'clamp',
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ opacity }}>Hello World!</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
CSS transitions or animations are FORBIDDEN - they will not render correctly.
|
||||||
|
Tailwind animation class names are FORBIDDEN - they will not render correctly.
|
||||||
78
rules/assets.md
Normal file
78
rules/assets.md
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
---
|
||||||
|
name: assets
|
||||||
|
description: Importing images, videos, audio, and fonts into Remotion
|
||||||
|
metadata:
|
||||||
|
tags: assets, staticFile, images, fonts, public
|
||||||
|
---
|
||||||
|
|
||||||
|
# Importing assets in Remotion
|
||||||
|
|
||||||
|
## The public folder
|
||||||
|
|
||||||
|
Place assets in the `public/` folder at your project root.
|
||||||
|
|
||||||
|
## Using staticFile()
|
||||||
|
|
||||||
|
You MUST use `staticFile()` to reference files from the `public/` folder:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import {Img, staticFile} from 'remotion';
|
||||||
|
|
||||||
|
export const MyComposition = () => {
|
||||||
|
return <Img src={staticFile('logo.png')} />;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
The function returns an encoded URL that works correctly when deploying to subdirectories.
|
||||||
|
|
||||||
|
## Using with components
|
||||||
|
|
||||||
|
**Images:**
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import {Img, staticFile} from 'remotion';
|
||||||
|
|
||||||
|
<Img src={staticFile('photo.png')} />;
|
||||||
|
```
|
||||||
|
|
||||||
|
**Videos:**
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import {Video} from '@remotion/media';
|
||||||
|
import {staticFile} from 'remotion';
|
||||||
|
|
||||||
|
<Video src={staticFile('clip.mp4')} />;
|
||||||
|
```
|
||||||
|
|
||||||
|
**Audio:**
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import {Audio} from '@remotion/media';
|
||||||
|
import {staticFile} from 'remotion';
|
||||||
|
|
||||||
|
<Audio src={staticFile('music.mp3')} />;
|
||||||
|
```
|
||||||
|
|
||||||
|
**Fonts:**
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import {staticFile} from 'remotion';
|
||||||
|
|
||||||
|
const fontFamily = new FontFace('MyFont', `url(${staticFile('font.woff2')})`);
|
||||||
|
await fontFamily.load();
|
||||||
|
document.fonts.add(fontFamily);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Remote URLs
|
||||||
|
|
||||||
|
Remote URLs can be used directly without `staticFile()`:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<Img src="https://example.com/image.png" />
|
||||||
|
<Video src="https://remotion.media/video.mp4" />
|
||||||
|
```
|
||||||
|
|
||||||
|
## Important notes
|
||||||
|
|
||||||
|
- Remotion components (`<Img>`, `<Video>`, `<Audio>`) ensure assets are fully loaded before rendering
|
||||||
|
- Special characters in filenames (`#`, `?`, `&`) are automatically encoded
|
||||||
169
rules/audio.md
Normal file
169
rules/audio.md
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
---
|
||||||
|
name: audio
|
||||||
|
description: Using audio and sound in Remotion - importing, trimming, volume, speed, pitch
|
||||||
|
metadata:
|
||||||
|
tags: audio, media, trim, volume, speed, loop, pitch, mute, sound, sfx
|
||||||
|
---
|
||||||
|
|
||||||
|
# Using audio in Remotion
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
First, the @remotion/media package needs to be installed.
|
||||||
|
If it is not installed, use the following command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx remotion add @remotion/media
|
||||||
|
```
|
||||||
|
|
||||||
|
## Importing Audio
|
||||||
|
|
||||||
|
Use `<Audio>` from `@remotion/media` to add audio to your composition.
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { Audio } from "@remotion/media";
|
||||||
|
import { staticFile } from "remotion";
|
||||||
|
|
||||||
|
export const MyComposition = () => {
|
||||||
|
return <Audio src={staticFile("audio.mp3")} />;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
Remote URLs are also supported:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<Audio src="https://remotion.media/audio.mp3" />
|
||||||
|
```
|
||||||
|
|
||||||
|
By default, audio plays from the start, at full volume and full length.
|
||||||
|
Multiple audio tracks can be layered by adding multiple `<Audio>` components.
|
||||||
|
|
||||||
|
## Trimming
|
||||||
|
|
||||||
|
Use `trimBefore` and `trimAfter` to remove portions of the audio. Values are in frames.
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
const { fps } = useVideoConfig();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Audio
|
||||||
|
src={staticFile("audio.mp3")}
|
||||||
|
trimBefore={2 * fps} // Skip the first 2 seconds
|
||||||
|
trimAfter={10 * fps} // End at the 10 second mark
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
The audio still starts playing at the beginning of the composition - only the specified portion is played.
|
||||||
|
|
||||||
|
## Delaying
|
||||||
|
|
||||||
|
Wrap the audio in a `<Sequence>` to delay when it starts:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { Sequence, staticFile } from "remotion";
|
||||||
|
import { Audio } from "@remotion/media";
|
||||||
|
|
||||||
|
const { fps } = useVideoConfig();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Sequence from={1 * fps}>
|
||||||
|
<Audio src={staticFile("audio.mp3")} />
|
||||||
|
</Sequence>
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
The audio will start playing after 1 second.
|
||||||
|
|
||||||
|
## Volume
|
||||||
|
|
||||||
|
Set a static volume (0 to 1):
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<Audio src={staticFile("audio.mp3")} volume={0.5} />
|
||||||
|
```
|
||||||
|
|
||||||
|
Or use a callback for dynamic volume based on the current frame:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { interpolate } from "remotion";
|
||||||
|
|
||||||
|
const { fps } = useVideoConfig();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Audio
|
||||||
|
src={staticFile("audio.mp3")}
|
||||||
|
volume={(f) =>
|
||||||
|
interpolate(f, [0, 1 * fps], [0, 1], { extrapolateRight: "clamp" })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
The value of `f` starts at 0 when the audio begins to play, not the composition frame.
|
||||||
|
|
||||||
|
## Muting
|
||||||
|
|
||||||
|
Use `muted` to silence the audio. It can be set dynamically:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
const frame = useCurrentFrame();
|
||||||
|
const { fps } = useVideoConfig();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Audio
|
||||||
|
src={staticFile("audio.mp3")}
|
||||||
|
muted={frame >= 2 * fps && frame <= 4 * fps} // Mute between 2s and 4s
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Speed
|
||||||
|
|
||||||
|
Use `playbackRate` to change the playback speed:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<Audio src={staticFile("audio.mp3")} playbackRate={2} /> {/* 2x speed */}
|
||||||
|
<Audio src={staticFile("audio.mp3")} playbackRate={0.5} /> {/* Half speed */}
|
||||||
|
```
|
||||||
|
|
||||||
|
Reverse playback is not supported.
|
||||||
|
|
||||||
|
## Looping
|
||||||
|
|
||||||
|
Use `loop` to loop the audio indefinitely:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<Audio src={staticFile("audio.mp3")} loop />
|
||||||
|
```
|
||||||
|
|
||||||
|
Use `loopVolumeCurveBehavior` to control how the frame count behaves when looping:
|
||||||
|
|
||||||
|
- `"repeat"`: Frame count resets to 0 each loop (default)
|
||||||
|
- `"extend"`: Frame count continues incrementing
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<Audio
|
||||||
|
src={staticFile("audio.mp3")}
|
||||||
|
loop
|
||||||
|
loopVolumeCurveBehavior="extend"
|
||||||
|
volume={(f) => interpolate(f, [0, 300], [1, 0])} // Fade out over multiple loops
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Pitch
|
||||||
|
|
||||||
|
Use `toneFrequency` to adjust the pitch without affecting speed. Values range from 0.01 to 2:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<Audio
|
||||||
|
src={staticFile("audio.mp3")}
|
||||||
|
toneFrequency={1.5} // Higher pitch
|
||||||
|
/>
|
||||||
|
<Audio
|
||||||
|
src={staticFile("audio.mp3")}
|
||||||
|
toneFrequency={0.8} // Lower pitch
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
Pitch shifting only works during server-side rendering, not in the Remotion Studio preview or in the `<Player />`.
|
||||||
104
rules/calculate-metadata.md
Normal file
104
rules/calculate-metadata.md
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
---
|
||||||
|
name: calculate-metadata
|
||||||
|
description: Dynamically set composition duration, dimensions, and props
|
||||||
|
metadata:
|
||||||
|
tags: calculateMetadata, duration, dimensions, props, dynamic
|
||||||
|
---
|
||||||
|
|
||||||
|
# Using calculateMetadata
|
||||||
|
|
||||||
|
Use `calculateMetadata` on a `<Composition>` to dynamically set duration, dimensions, and transform props before rendering.
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<Composition id="MyComp" component={MyComponent} durationInFrames={300} fps={30} width={1920} height={1080} defaultProps={{videoSrc: 'https://remotion.media/video.mp4'}} calculateMetadata={calculateMetadata} />
|
||||||
|
```
|
||||||
|
|
||||||
|
## Setting duration based on a video
|
||||||
|
|
||||||
|
Use the `getMediaMetadata()` function from the mediabunny/metadata skill to get the video duration:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import {CalculateMetadataFunction} from 'remotion';
|
||||||
|
import {getMediaMetadata} from '../get-media-metadata';
|
||||||
|
|
||||||
|
const calculateMetadata: CalculateMetadataFunction<Props> = async ({props}) => {
|
||||||
|
const {durationInSeconds} = await getMediaMetadata(props.videoSrc);
|
||||||
|
|
||||||
|
return {
|
||||||
|
durationInFrames: Math.ceil(durationInSeconds * 30),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Matching dimensions of a video
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
const calculateMetadata: CalculateMetadataFunction<Props> = async ({props}) => {
|
||||||
|
const {durationInSeconds, dimensions} = await getMediaMetadata(props.videoSrc);
|
||||||
|
|
||||||
|
return {
|
||||||
|
durationInFrames: Math.ceil(durationInSeconds * 30),
|
||||||
|
width: dimensions?.width ?? 1920,
|
||||||
|
height: dimensions?.height ?? 1080,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Setting duration based on multiple videos
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
const calculateMetadata: CalculateMetadataFunction<Props> = async ({props}) => {
|
||||||
|
const metadataPromises = props.videos.map((video) => getMediaMetadata(video.src));
|
||||||
|
const allMetadata = await Promise.all(metadataPromises);
|
||||||
|
|
||||||
|
const totalDuration = allMetadata.reduce((sum, meta) => sum + meta.durationInSeconds, 0);
|
||||||
|
|
||||||
|
return {
|
||||||
|
durationInFrames: Math.ceil(totalDuration * 30),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Setting a default outName
|
||||||
|
|
||||||
|
Set the default output filename based on props:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
const calculateMetadata: CalculateMetadataFunction<Props> = async ({props}) => {
|
||||||
|
return {
|
||||||
|
defaultOutName: `video-${props.id}.mp4`,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Transforming props
|
||||||
|
|
||||||
|
Fetch data or transform props before rendering:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
const calculateMetadata: CalculateMetadataFunction<Props> = async ({props, abortSignal}) => {
|
||||||
|
const response = await fetch(props.dataUrl, {signal: abortSignal});
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
...props,
|
||||||
|
fetchedData: data,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
The `abortSignal` cancels stale requests when props change in the Studio.
|
||||||
|
|
||||||
|
## Return value
|
||||||
|
|
||||||
|
All fields are optional. Returned values override the `<Composition>` props:
|
||||||
|
|
||||||
|
- `durationInFrames`: Number of frames
|
||||||
|
- `width`: Composition width in pixels
|
||||||
|
- `height`: Composition height in pixels
|
||||||
|
- `fps`: Frames per second
|
||||||
|
- `props`: Transformed props passed to the component
|
||||||
|
- `defaultOutName`: Default output filename
|
||||||
|
- `defaultCodec`: Default codec for rendering
|
||||||
75
rules/can-decode.md
Normal file
75
rules/can-decode.md
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
---
|
||||||
|
name: can-decode
|
||||||
|
description: Check if a video can be decoded by the browser using Mediabunny
|
||||||
|
metadata:
|
||||||
|
tags: decode, validation, video, audio, compatibility, browser
|
||||||
|
---
|
||||||
|
|
||||||
|
# Checking if a video can be decoded
|
||||||
|
|
||||||
|
Use Mediabunny to check if a video can be decoded by the browser before attempting to play it.
|
||||||
|
|
||||||
|
## The `canDecode()` function
|
||||||
|
|
||||||
|
This function can be copy-pasted into any project.
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { Input, ALL_FORMATS, UrlSource } from "mediabunny";
|
||||||
|
|
||||||
|
export const canDecode = async (src: string) => {
|
||||||
|
const input = new Input({
|
||||||
|
formats: ALL_FORMATS,
|
||||||
|
source: new UrlSource(src, {
|
||||||
|
getRetryDelay: () => null,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await input.getFormat();
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const videoTrack = await input.getPrimaryVideoTrack();
|
||||||
|
if (videoTrack && !(await videoTrack.canDecode())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const audioTrack = await input.getPrimaryAudioTrack();
|
||||||
|
if (audioTrack && !(await audioTrack.canDecode())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
const src = "https://remotion.media/video.mp4";
|
||||||
|
const isDecodable = await canDecode(src);
|
||||||
|
|
||||||
|
if (isDecodable) {
|
||||||
|
console.log("Video can be decoded");
|
||||||
|
} else {
|
||||||
|
console.log("Video cannot be decoded by this browser");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Using with Blob
|
||||||
|
|
||||||
|
For file uploads or drag-and-drop, use `BlobSource`:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { Input, ALL_FORMATS, BlobSource } from "mediabunny";
|
||||||
|
|
||||||
|
export const canDecodeBlob = async (blob: Blob) => {
|
||||||
|
const input = new Input({
|
||||||
|
formats: ALL_FORMATS,
|
||||||
|
source: new BlobSource(blob),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Same validation logic as above
|
||||||
|
};
|
||||||
|
```
|
||||||
120
rules/charts.md
Normal file
120
rules/charts.md
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
---
|
||||||
|
name: charts
|
||||||
|
description: Chart and data visualization patterns for Remotion. Use when creating bar charts, pie charts, line charts, stock graphs, or any data-driven animations.
|
||||||
|
metadata:
|
||||||
|
tags: charts, data, visualization, bar-chart, pie-chart, line-chart, stock-chart, svg-paths, graphs
|
||||||
|
---
|
||||||
|
|
||||||
|
# Charts in Remotion
|
||||||
|
|
||||||
|
Create charts using React code - HTML, SVG, and D3.js are all supported.
|
||||||
|
|
||||||
|
Disable all animations from third party libraries - they cause flickering.
|
||||||
|
Drive all animations from `useCurrentFrame()`.
|
||||||
|
|
||||||
|
## Bar Chart
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
const STAGGER_DELAY = 5;
|
||||||
|
const frame = useCurrentFrame();
|
||||||
|
const { fps } = useVideoConfig();
|
||||||
|
|
||||||
|
const bars = data.map((item, i) => {
|
||||||
|
const height = spring({
|
||||||
|
frame,
|
||||||
|
fps,
|
||||||
|
delay: i * STAGGER_DELAY,
|
||||||
|
config: { damping: 200 },
|
||||||
|
});
|
||||||
|
return <div style={{ height: height * item.value }} />;
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Pie Chart
|
||||||
|
|
||||||
|
Animate segments using stroke-dashoffset, starting from 12 o'clock:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
const progress = interpolate(frame, [0, 100], [0, 1]);
|
||||||
|
const circumference = 2 * Math.PI * radius;
|
||||||
|
const segmentLength = (value / total) * circumference;
|
||||||
|
const offset = interpolate(progress, [0, 1], [segmentLength, 0]);
|
||||||
|
|
||||||
|
<circle
|
||||||
|
r={radius}
|
||||||
|
cx={center}
|
||||||
|
cy={center}
|
||||||
|
fill="none"
|
||||||
|
stroke={color}
|
||||||
|
strokeWidth={strokeWidth}
|
||||||
|
strokeDasharray={`${segmentLength} ${circumference}`}
|
||||||
|
strokeDashoffset={offset}
|
||||||
|
transform={`rotate(-90 ${center} ${center})`}
|
||||||
|
/>;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Line Chart / Path Animation
|
||||||
|
|
||||||
|
Use `@remotion/paths` for animating SVG paths (line charts, stock graphs, signatures).
|
||||||
|
|
||||||
|
Install: `npx remotion add @remotion/paths`
|
||||||
|
Docs: https://remotion.dev/docs/paths.md
|
||||||
|
|
||||||
|
### Convert data points to SVG path
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
type Point = { x: number; y: number };
|
||||||
|
|
||||||
|
const generateLinePath = (points: Point[]): string => {
|
||||||
|
if (points.length < 2) return "";
|
||||||
|
return points.map((p, i) => `${i === 0 ? "M" : "L"} ${p.x} ${p.y}`).join(" ");
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### Draw path with animation
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { evolvePath } from "@remotion/paths";
|
||||||
|
|
||||||
|
const path = "M 100 200 L 200 150 L 300 180 L 400 100";
|
||||||
|
const progress = interpolate(frame, [0, 2 * fps], [0, 1], {
|
||||||
|
extrapolateLeft: "clamp",
|
||||||
|
extrapolateRight: "clamp",
|
||||||
|
easing: Easing.out(Easing.quad),
|
||||||
|
});
|
||||||
|
|
||||||
|
const { strokeDasharray, strokeDashoffset } = evolvePath(progress, path);
|
||||||
|
|
||||||
|
<path
|
||||||
|
d={path}
|
||||||
|
fill="none"
|
||||||
|
stroke="#FF3232"
|
||||||
|
strokeWidth={4}
|
||||||
|
strokeDasharray={strokeDasharray}
|
||||||
|
strokeDashoffset={strokeDashoffset}
|
||||||
|
/>;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Follow path with marker/arrow
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import {
|
||||||
|
getLength,
|
||||||
|
getPointAtLength,
|
||||||
|
getTangentAtLength,
|
||||||
|
} from "@remotion/paths";
|
||||||
|
|
||||||
|
const pathLength = getLength(path);
|
||||||
|
const point = getPointAtLength(path, progress * pathLength);
|
||||||
|
const tangent = getTangentAtLength(path, progress * pathLength);
|
||||||
|
const angle = Math.atan2(tangent.y, tangent.x);
|
||||||
|
|
||||||
|
<g
|
||||||
|
style={{
|
||||||
|
transform: `translate(${point.x}px, ${point.y}px) rotate(${angle}rad)`,
|
||||||
|
transformOrigin: "0 0",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<polygon points="0,0 -20,-10 -20,10" fill="#FF3232" />
|
||||||
|
</g>;
|
||||||
|
```
|
||||||
141
rules/compositions.md
Normal file
141
rules/compositions.md
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
---
|
||||||
|
name: compositions
|
||||||
|
description: Defining compositions, stills, folders, default props and dynamic metadata
|
||||||
|
metadata:
|
||||||
|
tags: composition, still, folder, props, metadata
|
||||||
|
---
|
||||||
|
|
||||||
|
A `<Composition>` defines the component, width, height, fps and duration of a renderable video.
|
||||||
|
|
||||||
|
It normally is placed in the `src/Root.tsx` file.
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import {Composition} from 'remotion';
|
||||||
|
import {MyComposition} from './MyComposition';
|
||||||
|
|
||||||
|
export const RemotionRoot = () => {
|
||||||
|
return <Composition id="MyComposition" component={MyComposition} durationInFrames={100} fps={30} width={1080} height={1080} />;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Default Props
|
||||||
|
|
||||||
|
Pass `defaultProps` to provide initial values for your component.
|
||||||
|
Values must be JSON-serializable (`Date`, `Map`, `Set`, and `staticFile()` are supported).
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import {Composition} from 'remotion';
|
||||||
|
import {MyComposition, MyCompositionProps} from './MyComposition';
|
||||||
|
|
||||||
|
export const RemotionRoot = () => {
|
||||||
|
return (
|
||||||
|
<Composition
|
||||||
|
id="MyComposition"
|
||||||
|
component={MyComposition}
|
||||||
|
durationInFrames={100}
|
||||||
|
fps={30}
|
||||||
|
width={1080}
|
||||||
|
height={1080}
|
||||||
|
defaultProps={
|
||||||
|
{
|
||||||
|
title: 'Hello World',
|
||||||
|
color: '#ff0000',
|
||||||
|
} satisfies MyCompositionProps
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
Use `type` declarations for props rather than `interface` to ensure `defaultProps` type safety.
|
||||||
|
|
||||||
|
## Folders
|
||||||
|
|
||||||
|
Use `<Folder>` to organize compositions in the sidebar.
|
||||||
|
Folder names can only contain letters, numbers, and hyphens.
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import {Composition, Folder} from 'remotion';
|
||||||
|
|
||||||
|
export const RemotionRoot = () => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Folder name="Marketing">
|
||||||
|
<Composition id="Promo" /* ... */ />
|
||||||
|
<Composition id="Ad" /* ... */ />
|
||||||
|
</Folder>
|
||||||
|
<Folder name="Social">
|
||||||
|
<Folder name="Instagram">
|
||||||
|
<Composition id="Story" /* ... */ />
|
||||||
|
<Composition id="Reel" /* ... */ />
|
||||||
|
</Folder>
|
||||||
|
</Folder>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Stills
|
||||||
|
|
||||||
|
Use `<Still>` for single-frame images. It does not require `durationInFrames` or `fps`.
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import {Still} from 'remotion';
|
||||||
|
import {Thumbnail} from './Thumbnail';
|
||||||
|
|
||||||
|
export const RemotionRoot = () => {
|
||||||
|
return <Still id="Thumbnail" component={Thumbnail} width={1280} height={720} />;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Calculate Metadata
|
||||||
|
|
||||||
|
Use `calculateMetadata` to make dimensions, duration, or props dynamic based on data.
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import {Composition, CalculateMetadataFunction} from 'remotion';
|
||||||
|
import {MyComposition, MyCompositionProps} from './MyComposition';
|
||||||
|
|
||||||
|
const calculateMetadata: CalculateMetadataFunction<MyCompositionProps> = async ({props, abortSignal}) => {
|
||||||
|
const data = await fetch(`https://api.example.com/video/${props.videoId}`, {
|
||||||
|
signal: abortSignal,
|
||||||
|
}).then((res) => res.json());
|
||||||
|
|
||||||
|
return {
|
||||||
|
durationInFrames: Math.ceil(data.duration * 30),
|
||||||
|
props: {
|
||||||
|
...props,
|
||||||
|
videoUrl: data.url,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const RemotionRoot = () => {
|
||||||
|
return (
|
||||||
|
<Composition
|
||||||
|
id="MyComposition"
|
||||||
|
component={MyComposition}
|
||||||
|
durationInFrames={100} // Placeholder, will be overridden
|
||||||
|
fps={30}
|
||||||
|
width={1080}
|
||||||
|
height={1080}
|
||||||
|
defaultProps={{videoId: 'abc123'}}
|
||||||
|
calculateMetadata={calculateMetadata}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
The function can return `props`, `durationInFrames`, `width`, `height`, `fps`, and codec-related defaults. It runs once before rendering begins.
|
||||||
|
|
||||||
|
## Nesting compositions within another
|
||||||
|
|
||||||
|
To add a composition within another composition, you can use the `<Sequence>` component with a `width` and `height` prop to specify the size of the composition.
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<AbsoluteFill>
|
||||||
|
<Sequence width={COMPOSITION_WIDTH} height={COMPOSITION_HEIGHT}>
|
||||||
|
<CompositionComponent />
|
||||||
|
</Sequence>
|
||||||
|
</AbsoluteFill>
|
||||||
|
```
|
||||||
Reference in New Issue
Block a user