Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4399c3ac75 | ||
|
|
20022ff1f9 | ||
|
|
bc7a50dff3 | ||
|
|
801b855148 | ||
|
|
3010881048 |
No files matched your search
@@ -0,0 +1,102 @@
|
|||||||
|
---
|
||||||
|
name: remotion-video
|
||||||
|
description: Create videos from React code with Remotion. Use when the user asks to make, animate, preview, or render a video, motion graphic, intro, caption, or social clip using code.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Remotion Video
|
||||||
|
|
||||||
|
Remotion makes videos out of React components. Each frame is a web page. Render them in order and you get an MP4.
|
||||||
|
|
||||||
|
## Core ideas
|
||||||
|
- **Composition**: one video. Has `width`, `height`, `fps`, `durationInFrames`.
|
||||||
|
- **Frame**: `useCurrentFrame()` returns the current frame number. All animation is a function of the frame.
|
||||||
|
- **`interpolate`**: maps a frame range to a value range (fade, slide).
|
||||||
|
- **`spring`**: natural bouncy motion.
|
||||||
|
- **`Sequence`**: shows children only during a frame range.
|
||||||
|
- **`AbsoluteFill`**: full-screen layer; stack them for backgrounds.
|
||||||
|
- **`Audio` / `OffthreadVideo` / `Img`**: media. Put files in `public/` and load with `staticFile("name.mp3")`.
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
1. **Plan first.** Ask or decide: length (seconds), size (1920x1080, or 1080x1920 for vertical), fps (30), and the scenes. Keep scenes to a short list.
|
||||||
|
2. **Set up** (skip if the project exists):
|
||||||
|
```
|
||||||
|
npx create-video@latest
|
||||||
|
cd <project> && npm install
|
||||||
|
```
|
||||||
|
3. **Write** compositions in `src/`, register them in `src/Root.tsx`.
|
||||||
|
4. **Preview**: `npm run dev` (opens Remotion Studio).
|
||||||
|
5. **Render**: `npx remotion render <CompositionId> out/video.mp4`
|
||||||
|
Still frame: `npx remotion still <CompositionId> out/frame.png --frame=30`
|
||||||
|
6. **Verify**: render one still first, check it looks right, then render the full video.
|
||||||
|
|
||||||
|
## Starter example
|
||||||
|
`src/Root.tsx`
|
||||||
|
```tsx
|
||||||
|
import { Composition } from "remotion";
|
||||||
|
import { Hello } from "./Hello";
|
||||||
|
|
||||||
|
export const RemotionRoot = () => (
|
||||||
|
<Composition
|
||||||
|
id="Hello"
|
||||||
|
component={Hello}
|
||||||
|
durationInFrames={150}
|
||||||
|
fps={30}
|
||||||
|
width={1920}
|
||||||
|
height={1080}
|
||||||
|
defaultProps={{ title: "Hello, world" }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
`src/Hello.tsx`
|
||||||
|
```tsx
|
||||||
|
import { AbsoluteFill, interpolate, spring, useCurrentFrame, useVideoConfig } from "remotion";
|
||||||
|
|
||||||
|
export const Hello = ({ title }: { title: string }) => {
|
||||||
|
const frame = useCurrentFrame();
|
||||||
|
const { fps } = useVideoConfig();
|
||||||
|
const opacity = interpolate(frame, [0, 30], [0, 1], { extrapolateRight: "clamp" });
|
||||||
|
const scale = spring({ frame, fps, config: { damping: 12 } });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AbsoluteFill style={{ backgroundColor: "#0b1020", justifyContent: "center", alignItems: "center" }}>
|
||||||
|
<h1 style={{ color: "white", fontSize: 120, opacity, transform: `scale(${scale})` }}>{title}</h1>
|
||||||
|
</AbsoluteFill>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Rules of thumb
|
||||||
|
- Never animate with CSS transitions, `setTimeout`, or CSS animations. They don't render per frame. Drive everything from `useCurrentFrame()`.
|
||||||
|
- Always pass `extrapolateRight: "clamp"` to `interpolate` unless you want values to keep growing.
|
||||||
|
- Seconds to frames: `seconds * fps`.
|
||||||
|
- Use `<Sequence from={..} durationInFrames={..}>` to order scenes; `<Series>` to chain them.
|
||||||
|
- Load fonts with `@remotion/google-fonts` so they are ready before rendering.
|
||||||
|
- Keep props in `defaultProps` so the user can edit text in the Studio.
|
||||||
|
- Rendering needs Chromium and ffmpeg (Remotion installs/handles these); first render can be slow.
|
||||||
|
|
||||||
|
## Fonts
|
||||||
|
Google Fonts: `npm i @remotion/google-fonts`, then
|
||||||
|
```tsx
|
||||||
|
import { loadFont } from "@remotion/google-fonts/Inter";
|
||||||
|
const { fontFamily } = loadFont(); // use as style={{ fontFamily }}
|
||||||
|
```
|
||||||
|
It downloads the font while rendering, so the machine needs internet access to fonts.googleapis.com. If the render fails with "Failed to load font", the network is blocking it: use a local font file in `public/` with `@remotion/fonts` (`loadFont({ family, url: staticFile("x.woff2") })`) or a system font. (Not verified in the sandbox where this skill was tested, which had no access to Google.)
|
||||||
|
|
||||||
|
## Common problems
|
||||||
|
- **Can't download the browser (no internet / locked-down machine)**: point Remotion at an installed Chromium:
|
||||||
|
`npx remotion render <Id> out/video.mp4 --browser-executable=/path/to/chromium --chrome-mode=chrome-for-testing`
|
||||||
|
- **Blank/white frames**: an asset path is wrong; use `staticFile()`.
|
||||||
|
- **Flicker**: an image or font not loaded; use `<Img>` and `@remotion/google-fonts`.
|
||||||
|
- **Slow render**: lower resolution/fps while testing, or use `--concurrency`.
|
||||||
|
|
||||||
|
## Templates
|
||||||
|
Copy-paste starting points. Read the one that fits:
|
||||||
|
- `templates/captions.md` - words highlighted in time with speech
|
||||||
|
- `templates/charts.md` - animated bar chart
|
||||||
|
- `templates/intro-outro.md` - title card + chaining scenes with `Series`
|
||||||
|
- `templates/audio-voiceover.md` - background music, voiceover, auto video length
|
||||||
|
- `templates/vertical.md` - 1080x1920 clips for Reels/TikTok/Shorts
|
||||||
|
|
||||||
|
## Docs
|
||||||
|
https://www.remotion.dev/docs
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
# Template: Audio, music and voiceover
|
||||||
|
|
||||||
|
Put sound files in `public/` (mp3 or wav). Load them with `staticFile()`.
|
||||||
|
|
||||||
|
`src/Narrated.tsx`
|
||||||
|
```tsx
|
||||||
|
import { AbsoluteFill, Audio, Sequence, interpolate, staticFile, useVideoConfig } from "remotion";
|
||||||
|
import { getAudioDurationInSeconds } from "@remotion/media-utils";
|
||||||
|
import type { CalculateMetadataFunction } from "remotion";
|
||||||
|
|
||||||
|
type Props = { title: string; voice: string; music: string };
|
||||||
|
|
||||||
|
// Make the video exactly as long as the voiceover (+1 second of padding).
|
||||||
|
export const calcNarratedMetadata: CalculateMetadataFunction<Props> = async ({ props }) => {
|
||||||
|
const seconds = await getAudioDurationInSeconds(staticFile(props.voice));
|
||||||
|
return { durationInFrames: Math.ceil((seconds + 1) * 30) };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Narrated = ({ title, voice, music }: Props) => {
|
||||||
|
const { fps, durationInFrames } = useVideoConfig();
|
||||||
|
return (
|
||||||
|
<AbsoluteFill style={{ background: "#0b1020", justifyContent: "center", alignItems: "center" }}>
|
||||||
|
<h1 style={{ color: "white", fontSize: 110 }}>{title}</h1>
|
||||||
|
|
||||||
|
{/* Background music: quiet, fades out over the last second. */}
|
||||||
|
<Audio
|
||||||
|
src={staticFile(music)}
|
||||||
|
volume={(f) => interpolate(f, [durationInFrames - fps, durationInFrames], [0.15, 0], { extrapolateLeft: "clamp", extrapolateRight: "clamp" })}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Voiceover starts half a second in, full volume. */}
|
||||||
|
<Sequence from={Math.round(fps * 0.5)}>
|
||||||
|
<Audio src={staticFile(voice)} volume={1} />
|
||||||
|
</Sequence>
|
||||||
|
</AbsoluteFill>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
Register it (note `calculateMetadata`):
|
||||||
|
```tsx
|
||||||
|
<Composition
|
||||||
|
id="Narrated"
|
||||||
|
component={Narrated}
|
||||||
|
calculateMetadata={calcNarratedMetadata}
|
||||||
|
durationInFrames={300} // placeholder, replaced by calculateMetadata
|
||||||
|
fps={30} width={1920} height={1080}
|
||||||
|
defaultProps={{ title: "My story", voice: "voice.mp3", music: "music.mp3" }}
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
Install: `npm i @remotion/media-utils`
|
||||||
|
|
||||||
|
Getting a voiceover
|
||||||
|
- Record yourself, or generate speech with a text-to-speech service, and save as `public/voice.mp3`. Never commit API keys; keep them in environment variables.
|
||||||
|
- Pair with `templates/captions.md`: feed it word timings from a transcription of the same file.
|
||||||
|
|
||||||
|
Rules
|
||||||
|
- Music volume 0.1-0.2 so the voice stays clear.
|
||||||
|
- `<Sequence from={...}>` delays when a sound starts.
|
||||||
|
- Audio is only in the final render (mp4). Stills (`remotion still`) have no sound.
|
||||||
|
- Check the sound made it in: `ffprobe out/video.mp4` should list an `Audio:` stream.
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
# Template: Captions
|
||||||
|
|
||||||
|
Words appear in time with speech. Give each word a start time in seconds.
|
||||||
|
|
||||||
|
`src/Captions.tsx`
|
||||||
|
```tsx
|
||||||
|
import { AbsoluteFill, useCurrentFrame, useVideoConfig } from "remotion";
|
||||||
|
|
||||||
|
type Word = { text: string; start: number; end: number }; // seconds
|
||||||
|
|
||||||
|
export const Captions = ({ words, background = "#111" }: { words: Word[]; background?: string }) => {
|
||||||
|
const frame = useCurrentFrame();
|
||||||
|
const { fps } = useVideoConfig();
|
||||||
|
const t = frame / fps;
|
||||||
|
|
||||||
|
// Show a window of up to 5 words around the current one.
|
||||||
|
const current = words.findIndex((w) => t >= w.start && t < w.end);
|
||||||
|
const i = current === -1 ? 0 : current;
|
||||||
|
const visible = words.slice(Math.floor(i / 5) * 5, Math.floor(i / 5) * 5 + 5);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AbsoluteFill style={{ backgroundColor: background, justifyContent: "flex-end", alignItems: "center", paddingBottom: 200 }}>
|
||||||
|
<div style={{ fontSize: 72, fontWeight: 800, color: "white", textShadow: "0 4px 12px black", textAlign: "center", maxWidth: "80%" }}>
|
||||||
|
{visible.map((w, k) => (
|
||||||
|
<span key={k} style={{ color: t >= w.start && t < w.end ? "#ffd400" : "white", marginRight: 16 }}>
|
||||||
|
{w.text}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</AbsoluteFill>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
Tips
|
||||||
|
- Get word times from a transcription tool (e.g. `@remotion/captions` or Whisper) and save them as JSON in `public/`.
|
||||||
|
- `background` is the backdrop when there is no video. Pass `"transparent"` when layering over a video.
|
||||||
|
- Layer it over the video: `<AbsoluteFill><OffthreadVideo src={staticFile("clip.mp4")} /><Captions words={words} background="transparent" /></AbsoluteFill>`.
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
# Template: Animated bar chart
|
||||||
|
|
||||||
|
Bars grow one after another.
|
||||||
|
|
||||||
|
`src/BarChart.tsx`
|
||||||
|
```tsx
|
||||||
|
import { AbsoluteFill, spring, useCurrentFrame, useVideoConfig } from "remotion";
|
||||||
|
|
||||||
|
type Bar = { label: string; value: number };
|
||||||
|
|
||||||
|
export const BarChart = ({ data, color = "#4f8cff" }: { data: Bar[]; color?: string }) => {
|
||||||
|
const frame = useCurrentFrame();
|
||||||
|
const { fps } = useVideoConfig();
|
||||||
|
const max = Math.max(...data.map((d) => d.value));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AbsoluteFill style={{ backgroundColor: "#0b1020", padding: 120, justifyContent: "flex-end" }}>
|
||||||
|
<div style={{ display: "flex", alignItems: "flex-end", gap: 40, height: 700 }}>
|
||||||
|
{data.map((d, i) => {
|
||||||
|
const grow = spring({ frame: frame - i * 6, fps, config: { damping: 15 } });
|
||||||
|
return (
|
||||||
|
<div key={d.label} style={{ flex: 1, textAlign: "center", color: "white", fontSize: 36 }}>
|
||||||
|
<div style={{ height: (d.value / max) * 600 * grow, background: color, borderRadius: 8 }} />
|
||||||
|
<div style={{ marginTop: 16 }}>{d.label}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</AbsoluteFill>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
Tips
|
||||||
|
- Delay each bar with `frame - i * 6` (6 frames apart).
|
||||||
|
- For lines, animate `strokeDashoffset` on an SVG path using `interpolate`.
|
||||||
|
- Keep numbers readable: show the value as text, rounded.
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
# Template: Title intro / outro card
|
||||||
|
|
||||||
|
A short card (2-3 seconds) with a title and subtitle. Use at the start or end.
|
||||||
|
|
||||||
|
`src/TitleCard.tsx`
|
||||||
|
```tsx
|
||||||
|
import { AbsoluteFill, interpolate, spring, useCurrentFrame, useVideoConfig } from "remotion";
|
||||||
|
|
||||||
|
export const TitleCard = ({ title, subtitle }: { title: string; subtitle?: string }) => {
|
||||||
|
const frame = useCurrentFrame();
|
||||||
|
const { fps, durationInFrames } = useVideoConfig();
|
||||||
|
const rise = spring({ frame, fps, config: { damping: 14 } });
|
||||||
|
const fadeOut = interpolate(frame, [durationInFrames - 15, durationInFrames], [1, 0], {
|
||||||
|
extrapolateLeft: "clamp",
|
||||||
|
extrapolateRight: "clamp",
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AbsoluteFill style={{ background: "linear-gradient(135deg,#1b2a6b,#0b1020)", justifyContent: "center", alignItems: "center", opacity: fadeOut }}>
|
||||||
|
<h1 style={{ color: "white", fontSize: 130, margin: 0, transform: `translateY(${(1 - rise) * 60}px)`, opacity: rise }}>{title}</h1>
|
||||||
|
{subtitle && <p style={{ color: "#9fb3ff", fontSize: 48, opacity: rise }}>{subtitle}</p>}
|
||||||
|
</AbsoluteFill>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
Chain with the main video using `<Series>`:
|
||||||
|
```tsx
|
||||||
|
<Series>
|
||||||
|
<Series.Sequence durationInFrames={90}><TitleCard title="My Video" /></Series.Sequence>
|
||||||
|
<Series.Sequence durationInFrames={600}><MainScene /></Series.Sequence>
|
||||||
|
<Series.Sequence durationInFrames={90}><TitleCard title="Thanks for watching" /></Series.Sequence>
|
||||||
|
</Series>
|
||||||
|
```
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# Template: Vertical clips (Reels / TikTok / Shorts)
|
||||||
|
|
||||||
|
Same code, different size. Register a vertical composition:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<Composition
|
||||||
|
id="Vertical"
|
||||||
|
component={MyScene}
|
||||||
|
width={1080}
|
||||||
|
height={1920}
|
||||||
|
fps={30}
|
||||||
|
durationInFrames={30 * 30} // 30 seconds
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
Render: `npx remotion render Vertical out/vertical.mp4`
|
||||||
|
|
||||||
|
Rules
|
||||||
|
- Keep text inside the **safe zone**: about 150px from the top, 350px from the bottom (apps cover those parts with buttons and captions), and 60px from the sides.
|
||||||
|
- Use big text (60px+). Phone screens are small.
|
||||||
|
- Keep clips under 60 seconds (Shorts limit); 15-30 seconds does best.
|
||||||
|
- Use `useVideoConfig()` for width/height instead of hard-coding, so the same scene works wide and tall.
|
||||||
|
- Put captions in the lower-middle, above the bottom safe zone (see captions.md).
|
||||||
Reference in new issue
Block a user