diff --git a/.claude/skills/remotion-video/SKILL.md b/.claude/skills/remotion-video/SKILL.md new file mode 100644 index 0000000..f080402 --- /dev/null +++ b/.claude/skills/remotion-video/SKILL.md @@ -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 && 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 out/video.mp4` + Still frame: `npx remotion still 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 = () => ( + +); +``` + +`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 ( + +

{title}

+
+ ); +}; +``` + +## 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 `` to order scenes; `` 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 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 `` 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 diff --git a/.claude/skills/remotion-video/templates/audio-voiceover.md b/.claude/skills/remotion-video/templates/audio-voiceover.md new file mode 100644 index 0000000..3d9cbfc --- /dev/null +++ b/.claude/skills/remotion-video/templates/audio-voiceover.md @@ -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 = 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 ( + +

{title}

+ + {/* Background music: quiet, fades out over the last second. */} +
+ ); +}; +``` + +Register it (note `calculateMetadata`): +```tsx + +``` + +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. +- `` 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. diff --git a/.claude/skills/remotion-video/templates/captions.md b/.claude/skills/remotion-video/templates/captions.md new file mode 100644 index 0000000..52b9ce1 --- /dev/null +++ b/.claude/skills/remotion-video/templates/captions.md @@ -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 ( + +
+ {visible.map((w, k) => ( + = w.start && t < w.end ? "#ffd400" : "white", marginRight: 16 }}> + {w.text} + + ))} +
+
+ ); +}; +``` + +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: ``. diff --git a/.claude/skills/remotion-video/templates/charts.md b/.claude/skills/remotion-video/templates/charts.md new file mode 100644 index 0000000..9369abb --- /dev/null +++ b/.claude/skills/remotion-video/templates/charts.md @@ -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 ( + +
+ {data.map((d, i) => { + const grow = spring({ frame: frame - i * 6, fps, config: { damping: 15 } }); + return ( +
+
+
{d.label}
+
+ ); + })} +
+ + ); +}; +``` + +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. diff --git a/.claude/skills/remotion-video/templates/intro-outro.md b/.claude/skills/remotion-video/templates/intro-outro.md new file mode 100644 index 0000000..e6089d6 --- /dev/null +++ b/.claude/skills/remotion-video/templates/intro-outro.md @@ -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 ( + +

{title}

+ {subtitle &&

{subtitle}

} +
+ ); +}; +``` + +Chain with the main video using ``: +```tsx + + + + + +``` diff --git a/.claude/skills/remotion-video/templates/vertical.md b/.claude/skills/remotion-video/templates/vertical.md new file mode 100644 index 0000000..a8b008d --- /dev/null +++ b/.claude/skills/remotion-video/templates/vertical.md @@ -0,0 +1,23 @@ +# Template: Vertical clips (Reels / TikTok / Shorts) + +Same code, different size. Register a vertical composition: + +```tsx + +``` + +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).