feat: adiciona 12 plugins Descomplicar ao marketplace
Plugins: automacao, crm-ops, design-media, dev-tools, gestao, infraestrutura, marketing, negocio, perfex-dev, project-manager, wordpress + hello-plugin (existente). Totais: 83 skills, 44 agents, 12 datasets.json Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
739
marketing/skills/video/SKILL.md
Normal file
739
marketing/skills/video/SKILL.md
Normal file
@@ -0,0 +1,739 @@
|
||||
---
|
||||
name: video
|
||||
description: Video content strategy and production guidance. Plans video content,
|
||||
scripts, and production workflows. Use when user mentions "video content", "conteúdo
|
||||
vídeo", "youtube strategy", "video production", "video script".
|
||||
author: Descomplicar® Crescimento Digital
|
||||
version: 2.0.0
|
||||
quality_score: 75
|
||||
user_invocable: true
|
||||
desk_task: 1486
|
||||
allowed-tools: Edit
|
||||
---
|
||||
|
||||
# /video - Criação de Vídeos com Remotion
|
||||
|
||||
Cria vídeos profissionais usando **Remotion** (React) a partir de descrições em linguagem natural.
|
||||
|
||||
## Arquitectura
|
||||
|
||||
```
|
||||
Prompt Natural → Claude Code → Código React/Remotion → Vídeo MP4/GIF/WebM
|
||||
```
|
||||
|
||||
| Componente | Função |
|
||||
|------------|--------|
|
||||
| **Remotion** | Framework React para vídeos programáticos |
|
||||
| **Esta Skill** | Ensina Claude a gerar código Remotion correcto |
|
||||
| **Output** | MP4, GIF, WebM em qualquer resolução |
|
||||
|
||||
---
|
||||
|
||||
## Projecto Base
|
||||
|
||||
```
|
||||
/media/ealmeida/Dados/Dev/remotion-demo/
|
||||
```
|
||||
|
||||
Este projecto já está configurado com:
|
||||
- Remotion 4.x instalado
|
||||
- Agent Skills do Remotion
|
||||
- Composição de exemplo funcional
|
||||
- Scripts de renderização
|
||||
|
||||
---
|
||||
|
||||
## Uso Rápido
|
||||
|
||||
### Criar Novo Vídeo
|
||||
|
||||
```bash
|
||||
# 1. Navegar ao projecto
|
||||
cd /media/ealmeida/Dados/Dev/remotion-demo
|
||||
|
||||
# 2. Descrever o vídeo pretendido ao Claude
|
||||
"Cria um vídeo de 10 segundos para [empresa] com logo animado e tagline"
|
||||
|
||||
# 3. Renderizar
|
||||
npm run render
|
||||
# ou
|
||||
npx remotion render [CompositionName] out/video.mp4
|
||||
```
|
||||
|
||||
### Comandos Disponíveis
|
||||
|
||||
| Comando | Descrição |
|
||||
|---------|-----------|
|
||||
| `npm run dev` | Abrir Remotion Studio (preview) |
|
||||
| `npm run render` | Renderizar DemoVideo para MP4 |
|
||||
| `npm run render:gif` | Renderizar para GIF |
|
||||
|
||||
---
|
||||
|
||||
## Estrutura de Prompt Recomendada
|
||||
|
||||
```
|
||||
Cria um vídeo de [duração] segundos para [empresa/propósito] com:
|
||||
- Resolução: [1920x1080 / 1080x1920 / 1080x1080]
|
||||
- FPS: [30 / 60]
|
||||
- Fundo: [cor sólida / gradiente / imagem]
|
||||
- Sequência de animação:
|
||||
1. [Elemento] [animação] (frames X-Y)
|
||||
2. [Elemento] [animação] (frames X-Y)
|
||||
3. ...
|
||||
- Texto: [título, subtítulo, CTA]
|
||||
- Estilo: [minimalista / corporativo / energético / elegante]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Exemplos de Prompts
|
||||
|
||||
### Intro Corporativa
|
||||
```
|
||||
Cria um vídeo intro de 5 segundos para "Descomplicar" com:
|
||||
- Resolução: 1920x1080, 30fps
|
||||
- Fundo: gradiente de #1e3a8a para #7c3aed
|
||||
- Animação:
|
||||
1. Logo fade in com bounce (0-45 frames)
|
||||
2. Tagline "Crescimento Digital" slide up (45-90 frames)
|
||||
3. Linha decorativa expande (60-120 frames)
|
||||
- Estilo: profissional e moderno
|
||||
```
|
||||
|
||||
### Reel/Short Vertical
|
||||
```
|
||||
Cria um vídeo vertical de 15 segundos para Instagram Reels com:
|
||||
- Resolução: 1080x1920, 30fps
|
||||
- Tema: "5 Dicas de SEO"
|
||||
- Sequência:
|
||||
1. Título impactante com zoom (0-30 frames)
|
||||
2. Dica 1 slide in (30-90 frames)
|
||||
3. Dica 2 slide in (90-150 frames)
|
||||
...
|
||||
- CTA final: "Segue para mais!"
|
||||
- Cores: marca Descomplicar
|
||||
```
|
||||
|
||||
### Promo Produto
|
||||
```
|
||||
Cria um vídeo promocional de 20 segundos com:
|
||||
- Resolução: 1920x1080, 30fps
|
||||
- Produto: [Nome]
|
||||
- Features a destacar: [lista]
|
||||
- CTA: "Experimenta grátis"
|
||||
- Incluir: preço com animação de destaque
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Conceitos Remotion Essenciais
|
||||
|
||||
### Timing
|
||||
- **Frames**: Unidade base (30fps = 30 frames/segundo)
|
||||
- **useCurrentFrame()**: Frame actual da animação
|
||||
- **interpolate()**: Mapear frames para valores (opacity, position, scale)
|
||||
- **spring()**: Animações com física (bounce, elastic)
|
||||
|
||||
### Componentes Core
|
||||
```tsx
|
||||
import {
|
||||
AbsoluteFill, // Container full-screen
|
||||
Sequence, // Sequenciar elementos no tempo
|
||||
useCurrentFrame, // Frame actual
|
||||
useVideoConfig, // fps, width, height, duration
|
||||
interpolate, // Interpolação linear
|
||||
spring, // Animação spring
|
||||
Img, // Imagens
|
||||
Audio, // Áudio
|
||||
Video, // Vídeo embebido
|
||||
} from "remotion";
|
||||
```
|
||||
|
||||
### Estrutura de Composição
|
||||
```tsx
|
||||
// src/Root.tsx
|
||||
<Composition
|
||||
id="NomeVideo"
|
||||
component={MeuComponente}
|
||||
durationInFrames={150} // 5s a 30fps
|
||||
fps={30}
|
||||
width={1920}
|
||||
height={1080}
|
||||
defaultProps={{ titulo: "Exemplo" }}
|
||||
/>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Opções de Renderização
|
||||
|
||||
### Qualidade
|
||||
```bash
|
||||
# Alta qualidade (lento)
|
||||
npx remotion render Video out.mp4 --crf=18
|
||||
|
||||
# Qualidade média (rápido)
|
||||
npx remotion render Video out.mp4 --crf=23
|
||||
|
||||
# Baixa qualidade (muito rápido)
|
||||
npx remotion render Video out.mp4 --crf=28
|
||||
```
|
||||
|
||||
### Formatos
|
||||
```bash
|
||||
# MP4 (H.264)
|
||||
npx remotion render Video out.mp4
|
||||
|
||||
# GIF
|
||||
npx remotion render Video out.gif --codec=gif
|
||||
|
||||
# WebM (VP8)
|
||||
npx remotion render Video out.webm --codec=vp8
|
||||
|
||||
# ProRes (alta qualidade, ficheiro grande)
|
||||
npx remotion render Video out.mov --codec=prores
|
||||
```
|
||||
|
||||
### Props Dinâmicas
|
||||
```bash
|
||||
npx remotion render Video out.mp4 \
|
||||
--props='{"titulo":"Descomplicar","cor":"#1e3a8a"}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Resoluções Comuns
|
||||
|
||||
| Formato | Resolução | Uso |
|
||||
|---------|-----------|-----|
|
||||
| 16:9 HD | 1920x1080 | YouTube, Website |
|
||||
| 16:9 4K | 3840x2160 | YouTube 4K |
|
||||
| 9:16 Vertical | 1080x1920 | Reels, TikTok, Shorts |
|
||||
| 1:1 Quadrado | 1080x1080 | Instagram Feed |
|
||||
| 4:5 Portrait | 1080x1350 | Instagram Feed optimal |
|
||||
|
||||
---
|
||||
|
||||
## Boas Práticas
|
||||
|
||||
### Performance
|
||||
- Usar `React.memo()` para componentes pesados
|
||||
- Evitar re-renders desnecessários
|
||||
- Pré-carregar assets com `staticFile()`
|
||||
|
||||
### Animações
|
||||
- Começar com `interpolate()` para animações simples
|
||||
- Usar `spring()` para movimentos naturais
|
||||
- Combinar `Sequence` para organizar timeline
|
||||
|
||||
### Assets
|
||||
- Colocar imagens/áudio em `public/`
|
||||
- Usar `staticFile("nome.png")` para referenciar
|
||||
- Formatos recomendados: PNG (imagens), MP3/WAV (áudio)
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Problema | Solução |
|
||||
|----------|---------|
|
||||
| Vídeo preto | Verificar `AbsoluteFill` tem background |
|
||||
| Fontes não carregam | Usar `@remotion/google-fonts` |
|
||||
| Animação não funciona | Verificar range de frames no `interpolate` |
|
||||
| Erro de módulo | `rm -rf node_modules && npm install` |
|
||||
| Render lento | Reduzir `--concurrency` ou usar `--crf` maior |
|
||||
|
||||
---
|
||||
|
||||
## Regras Detalhadas
|
||||
|
||||
Para instruções específicas, consultar:
|
||||
|
||||
- [rules/animations.md](rules/animations.md) - Animações fundamentais
|
||||
- [rules/timing.md](rules/timing.md) - Interpolação e easing
|
||||
- [rules/transitions.md](rules/transitions.md) - Transições entre cenas
|
||||
- [rules/sequencing.md](rules/sequencing.md) - Organização temporal
|
||||
- [rules/fonts.md](rules/fonts.md) - Carregamento de fontes
|
||||
- [rules/images.md](rules/images.md) - Trabalhar com imagens
|
||||
- [rules/audio.md](rules/audio.md) - Áudio e som
|
||||
- [rules/tailwind.md](rules/tailwind.md) - TailwindCSS no Remotion
|
||||
|
||||
---
|
||||
|
||||
## Workflow Completo
|
||||
|
||||
```
|
||||
1. DEFINIR → Duração, resolução, estilo
|
||||
2. DESCREVER → Prompt detalhado ao Claude
|
||||
3. GERAR → Claude cria componentes React
|
||||
4. PREVIEW → npm run dev (Remotion Studio)
|
||||
5. AJUSTAR → Refinar animações/timing
|
||||
6. RENDER → npx remotion render
|
||||
7. EXPORTAR → MP4/GIF/WebM conforme necessidade
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Casos de Uso
|
||||
|
||||
| Tipo | Duração | Resolução |
|
||||
|------|---------|-----------|
|
||||
| Logo Intro | 3-5s | 1920x1080 |
|
||||
| Promo Rápida | 15-30s | 1080x1920 |
|
||||
| Explainer | 60-120s | 1920x1080 |
|
||||
| Social Post | 5-15s | 1080x1080 |
|
||||
| YouTube Intro | 5-10s | 1920x1080 |
|
||||
| Stories | 15s | 1080x1920 |
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
## Datasets Dify (Consulta Obrigatória)
|
||||
|
||||
Em caso de dúvidas ou para aprofundar conhecimento, consultar os seguintes datasets via MCP:
|
||||
|
||||
| Dataset | ID | Prioridade |
|
||||
|---------|----|-----------:|
|
||||
| **Canva** | `7efc5db4-05b1-408a-9e41-b612188ee877` | 2 |
|
||||
| **Criatividade** | `39818f77-8c70-4729-9b5c-6f92d3a2b418` | 2 |
|
||||
| **Youtube Marketing** | `baa1b3e6-ebf0-4413-84b2-63d1164867ea` | 2 |
|
||||
| **Marketing Redes Sociais** | `66117552-348f-455d-9aca-2da722567693` | 3 |
|
||||
|
||||
### Como Consultar
|
||||
|
||||
```javascript
|
||||
// Princípios de design para vídeo
|
||||
mcp__dify-kb__dify_kb_retrieve_segments({
|
||||
dataset_id: "7efc5db4-05b1-408a-9e41-b612188ee877",
|
||||
query: "animacao motion design"
|
||||
})
|
||||
|
||||
// Técnicas criativas
|
||||
mcp__dify-kb__dify_kb_retrieve_segments({
|
||||
dataset_id: "39818f77-8c70-4729-9b5c-6f92d3a2b418",
|
||||
query: "storytelling visual criativo"
|
||||
})
|
||||
|
||||
// Optimização para YouTube
|
||||
mcp__dify-kb__dify_kb_retrieve_segments({
|
||||
dataset_id: "baa1b3e6-ebf0-4413-84b2-63d1164867ea",
|
||||
query: "thumbnail intro retention"
|
||||
})
|
||||
```
|
||||
|
||||
### Quando Consultar
|
||||
|
||||
- Criar animações e motion graphics
|
||||
- Princípios de design visual
|
||||
- Optimizar vídeos para redes sociais
|
||||
- Storytelling visual
|
||||
|
||||
---
|
||||
|
||||
## Paletas de Cores para Vídeo
|
||||
|
||||
### Corporativo Profissional
|
||||
```tsx
|
||||
const colors = {
|
||||
background: "#0f172a", // Slate 900
|
||||
primary: "#3b82f6", // Blue 500
|
||||
secondary: "#8b5cf6", // Violet 500
|
||||
text: "#f8fafc", // Slate 50
|
||||
accent: "#10b981", // Emerald 500
|
||||
}
|
||||
```
|
||||
|
||||
### Energético/Marketing
|
||||
```tsx
|
||||
const colors = {
|
||||
background: "#ff006e", // Pink forte
|
||||
primary: "#ffbe0b", // Amarelo vibrante
|
||||
secondary: "#fb5607", // Laranja
|
||||
text: "#ffffff",
|
||||
accent: "#8338ec", // Roxo
|
||||
}
|
||||
```
|
||||
|
||||
### Minimalista/Elegante
|
||||
```tsx
|
||||
const colors = {
|
||||
background: "#fafafa", // Off-white
|
||||
primary: "#18181b", // Zinc 900
|
||||
secondary: "#71717a", // Zinc 500
|
||||
text: "#18181b",
|
||||
accent: "#a855f7", // Purple 500
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Animações Prontas (Copy-Paste)
|
||||
|
||||
### Fade In com Scale
|
||||
```tsx
|
||||
import { interpolate, useCurrentFrame } from "remotion"
|
||||
|
||||
export function FadeInScale({ children }: { children: React.ReactNode }) {
|
||||
const frame = useCurrentFrame()
|
||||
|
||||
const opacity = interpolate(frame, [0, 30], [0, 1], { extrapolateRight: "clamp" })
|
||||
const scale = interpolate(frame, [0, 30], [0.8, 1], { extrapolateRight: "clamp" })
|
||||
|
||||
return (
|
||||
<div style={{ opacity, transform: `scale(${scale})` }}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Slide Up
|
||||
```tsx
|
||||
export function SlideUp({ children, delay = 0 }: { children: React.ReactNode; delay?: number }) {
|
||||
const frame = useCurrentFrame()
|
||||
|
||||
const y = interpolate(frame, [delay, delay + 20], [50, 0], { extrapolateRight: "clamp" })
|
||||
const opacity = interpolate(frame, [delay, delay + 20], [0, 1], { extrapolateRight: "clamp" })
|
||||
|
||||
return (
|
||||
<div style={{ opacity, transform: `translateY(${y}px)` }}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Bounce In (Spring)
|
||||
```tsx
|
||||
import { spring, useCurrentFrame, useVideoConfig } from "remotion"
|
||||
|
||||
export function BounceIn({ children }: { children: React.ReactNode }) {
|
||||
const frame = useCurrentFrame()
|
||||
const { fps } = useVideoConfig()
|
||||
|
||||
const scale = spring({
|
||||
frame,
|
||||
fps,
|
||||
config: {
|
||||
damping: 10,
|
||||
stiffness: 200,
|
||||
mass: 0.5,
|
||||
},
|
||||
})
|
||||
|
||||
return (
|
||||
<div style={{ transform: `scale(${scale})` }}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Text Reveal (Linha por Linha)
|
||||
```tsx
|
||||
export function TextReveal({ lines }: { lines: string[] }) {
|
||||
const frame = useCurrentFrame()
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
{lines.map((line, i) => {
|
||||
const delay = i * 15
|
||||
const opacity = interpolate(
|
||||
frame,
|
||||
[delay, delay + 10],
|
||||
[0, 1],
|
||||
{ extrapolateRight: "clamp" }
|
||||
)
|
||||
|
||||
return (
|
||||
<div key={i} style={{ opacity }}>
|
||||
{line}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Tipografia para Vídeo
|
||||
|
||||
### Hierarquia de Tamanhos
|
||||
|
||||
```tsx
|
||||
// Remotion 1920x1080
|
||||
const textSizes = {
|
||||
hero: "120px", // Título principal
|
||||
h1: "80px", // Secções
|
||||
h2: "60px", // Sub-títulos
|
||||
h3: "40px", // Destaques
|
||||
body: "32px", // Texto normal
|
||||
caption: "24px", // Legendas
|
||||
}
|
||||
|
||||
// Remotion 1080x1920 (Vertical)
|
||||
const textSizesVertical = {
|
||||
hero: "80px",
|
||||
h1: "56px",
|
||||
h2: "40px",
|
||||
h3: "32px",
|
||||
body: "24px",
|
||||
caption: "18px",
|
||||
}
|
||||
```
|
||||
|
||||
### Font Pairings para Vídeo
|
||||
|
||||
```tsx
|
||||
// Moderno Tech
|
||||
<style>{`
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@700;900&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@500&display=swap');
|
||||
`}</style>
|
||||
|
||||
// Editorial Elegante
|
||||
<style>{`
|
||||
@import url('https://fonts.googleapis.com/css2?family=Playfair+Display:wght@700;900&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=Source+Sans+Pro:wght@400;600&display=swap');
|
||||
`}</style>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Templates Prontos
|
||||
|
||||
### Logo Intro (5s)
|
||||
```tsx
|
||||
import { AbsoluteFill, Img, interpolate, spring, useCurrentFrame, useVideoConfig } from "remotion"
|
||||
|
||||
export function LogoIntro({ logoSrc }: { logoSrc: string }) {
|
||||
const frame = useCurrentFrame()
|
||||
const { fps } = useVideoConfig()
|
||||
|
||||
// Logo scale com spring
|
||||
const logoScale = spring({
|
||||
frame: frame - 10,
|
||||
fps,
|
||||
config: { damping: 12, stiffness: 200 }
|
||||
})
|
||||
|
||||
// Tagline slide up
|
||||
const taglineY = interpolate(frame, [60, 90], [50, 0], { extrapolateRight: "clamp" })
|
||||
const taglineOpacity = interpolate(frame, [60, 90], [0, 1], { extrapolateRight: "clamp" })
|
||||
|
||||
return (
|
||||
<AbsoluteFill style={{ backgroundColor: "#0f172a", justifyContent: "center", alignItems: "center" }}>
|
||||
{/* Logo */}
|
||||
<div style={{ transform: `scale(${logoScale})` }}>
|
||||
<Img src={logoSrc} style={{ width: "300px" }} />
|
||||
</div>
|
||||
|
||||
{/* Tagline */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
bottom: "150px",
|
||||
fontSize: "48px",
|
||||
color: "#f8fafc",
|
||||
fontWeight: 600,
|
||||
transform: `translateY(${taglineY}px)`,
|
||||
opacity: taglineOpacity,
|
||||
}}
|
||||
>
|
||||
Crescimento Digital
|
||||
</div>
|
||||
</AbsoluteFill>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Stats Highlight (3s)
|
||||
```tsx
|
||||
export function StatsHighlight({ stat, label }: { stat: string; label: string }) {
|
||||
const frame = useCurrentFrame()
|
||||
const { fps } = useVideoConfig()
|
||||
|
||||
// Número escala com bounce
|
||||
const scale = spring({
|
||||
frame,
|
||||
fps,
|
||||
config: { damping: 10, mass: 0.5, stiffness: 200 }
|
||||
})
|
||||
|
||||
// Label fade in
|
||||
const labelOpacity = interpolate(frame, [30, 45], [0, 1], { extrapolateRight: "clamp" })
|
||||
|
||||
return (
|
||||
<AbsoluteFill style={{ backgroundColor: "#3b82f6", justifyContent: "center", alignItems: "center" }}>
|
||||
{/* Número */}
|
||||
<div
|
||||
style={{
|
||||
fontSize: "180px",
|
||||
fontWeight: 900,
|
||||
color: "#ffffff",
|
||||
transform: `scale(${scale})`,
|
||||
}}
|
||||
>
|
||||
{stat}
|
||||
</div>
|
||||
|
||||
{/* Label */}
|
||||
<div
|
||||
style={{
|
||||
fontSize: "40px",
|
||||
color: "#dbeafe",
|
||||
marginTop: "20px",
|
||||
opacity: labelOpacity,
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</div>
|
||||
</AbsoluteFill>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Motion Design Guidelines
|
||||
|
||||
### Duração de Animações
|
||||
|
||||
| Tipo | Frames (30fps) | Duração |
|
||||
|------|----------------|---------|
|
||||
| Micro (hover, toggle) | 3-6 | 100-200ms |
|
||||
| Rápida (fade, slide) | 12-18 | 400-600ms |
|
||||
| Normal (entrada cena) | 24-30 | 800-1000ms |
|
||||
| Lenta (transição cena) | 45-60 | 1.5-2s |
|
||||
|
||||
### Easing Curves
|
||||
|
||||
```tsx
|
||||
import { Easing } from "remotion"
|
||||
|
||||
// Entrada suave
|
||||
const easeOut = Easing.bezier(0, 0, 0.2, 1)
|
||||
|
||||
// Saída suave
|
||||
const easeIn = Easing.bezier(0.4, 0, 1, 1)
|
||||
|
||||
// Entrada e saída suave
|
||||
const easeInOut = Easing.bezier(0.4, 0, 0.2, 1)
|
||||
|
||||
// Spring natural
|
||||
const spring = Easing.bezier(0.34, 1.56, 0.64, 1)
|
||||
|
||||
// Uso
|
||||
const y = interpolate(frame, [0, 30], [100, 0], {
|
||||
easing: easeOut,
|
||||
extrapolateRight: "clamp"
|
||||
})
|
||||
```
|
||||
|
||||
### Princípios de Motion
|
||||
|
||||
1. **Staging** - Um movimento de cada vez
|
||||
2. **Anticipation** - Preparação antes do movimento
|
||||
3. **Follow Through** - Overshooting ligeiro
|
||||
4. **Timing** - Velocidade comunica peso/importância
|
||||
5. **Exaggeration** - Amplificar para ênfase
|
||||
|
||||
---
|
||||
|
||||
## Acessibilidade em Vídeo
|
||||
|
||||
### Contraste de Texto
|
||||
|
||||
```tsx
|
||||
// NUNCA usar contraste baixo
|
||||
// BAD: texto cinza em fundo branco
|
||||
const bad = { color: "#9ca3af", background: "#ffffff" } // Ratio 2.8:1
|
||||
|
||||
// GOOD: contraste mínimo 4.5:1
|
||||
const good = { color: "#1f2937", background: "#ffffff" } // Ratio 15.8:1
|
||||
```
|
||||
|
||||
### Tamanho Mínimo de Texto
|
||||
|
||||
```tsx
|
||||
// 1080p (1920x1080)
|
||||
const minSize = "28px" // Legível em mobile
|
||||
|
||||
// 4K (3840x2160)
|
||||
const minSize4k = "56px"
|
||||
```
|
||||
|
||||
### Reduced Motion
|
||||
|
||||
```tsx
|
||||
import { useVideoConfig } from "remotion"
|
||||
|
||||
export function RespectMotion({ children }: { children: React.ReactNode }) {
|
||||
const { fps } = useVideoConfig()
|
||||
const prefersReducedMotion = false // Remotion não tem acesso a media queries
|
||||
|
||||
// Para export, criar versão alternativa sem animações complexas
|
||||
return children
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Datasets Dify (Consulta Obrigatória)
|
||||
|
||||
Em caso de dúvidas ou para aprofundar conhecimento, consultar os seguintes datasets via MCP:
|
||||
|
||||
| Dataset | ID | Prioridade |
|
||||
|---------|----|-----------:|
|
||||
| **Canva** | `7efc5db4-05b1-408a-9e41-b612188ee877` | 2 |
|
||||
| **Criatividade** | `39818f77-8c70-4729-9b5c-6f92d3a2b418` | 2 |
|
||||
| **Youtube Marketing** | `baa1b3e6-ebf0-4413-84b2-63d1164867ea` | 2 |
|
||||
| **Marketing Redes Sociais** | `66117552-348f-455d-9aca-2da722567693` | 3 |
|
||||
|
||||
### Como Consultar
|
||||
|
||||
```javascript
|
||||
// Princípios de design para vídeo
|
||||
mcp__dify-kb__dify_kb_retrieve_segments({
|
||||
dataset_id: "7efc5db4-05b1-408a-9e41-b612188ee877",
|
||||
query: "animacao motion design"
|
||||
})
|
||||
|
||||
// Técnicas criativas
|
||||
mcp__dify-kb__dify_kb_retrieve_segments({
|
||||
dataset_id: "39818f77-8c70-4729-9b5c-6f92d3a2b418",
|
||||
query: "storytelling visual criativo"
|
||||
})
|
||||
|
||||
// Optimização para YouTube
|
||||
mcp__dify-kb__dify_kb_retrieve_segments({
|
||||
dataset_id: "baa1b3e6-ebf0-4413-84b2-63d1164867ea",
|
||||
query: "thumbnail intro retention"
|
||||
})
|
||||
```
|
||||
|
||||
### Quando Consultar
|
||||
|
||||
- Criar animações e motion graphics
|
||||
- Princípios de design visual
|
||||
- Optimizar vídeos para redes sociais
|
||||
- Storytelling visual
|
||||
|
||||
---
|
||||
|
||||
**Versão**: 2.0.0 | **Data**: 2026-02-03 | **Autor**: Descomplicar®
|
||||
**Projecto Base**: `/media/ealmeida/Dados/Dev/remotion-demo/`
|
||||
**Documentação**: [remotion.dev/docs](https://remotion.dev/docs)
|
||||
|
||||
---
|
||||
|
||||
|
||||
## Quando NÃO Usar
|
||||
|
||||
- Para tarefas fora do domínio de especialização desta skill
|
||||
- Quando outra skill mais específica está disponível
|
||||
- Para operações que requerem confirmação manual do utilizador
|
||||
86
marketing/skills/video/rules/3d.md
Normal file
86
marketing/skills/video/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
marketing/skills/video/rules/animations.md
Normal file
29
marketing/skills/video/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
marketing/skills/video/rules/assets.md
Normal file
78
marketing/skills/video/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
|
||||
172
marketing/skills/video/rules/audio.md
Normal file
172
marketing/skills/video/rules/audio.md
Normal file
@@ -0,0 +1,172 @@
|
||||
---
|
||||
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 # If project uses npm
|
||||
bunx remotion add @remotion/media # If project uses bun
|
||||
yarn remotion add @remotion/media # If project uses yarn
|
||||
pnpm exec remotion add @remotion/media # If project uses pnpm
|
||||
```
|
||||
|
||||
## 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
marketing/skills/video/rules/calculate-metadata.md
Normal file
104
marketing/skills/video/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
marketing/skills/video/rules/can-decode.md
Normal file
75
marketing/skills/video/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
|
||||
};
|
||||
```
|
||||
58
marketing/skills/video/rules/charts.md
Normal file
58
marketing/skills/video/rules/charts.md
Normal file
@@ -0,0 +1,58 @@
|
||||
---
|
||||
name: charts
|
||||
description: Chart and data visualization patterns for Remotion. Use when creating bar charts, pie charts, histograms, progress bars, or any data-driven animations.
|
||||
metadata:
|
||||
tags: charts, data, visualization, bar-chart, pie-chart, graphs
|
||||
---
|
||||
|
||||
# Charts in Remotion
|
||||
|
||||
You can create bar charts in Remotion by using regular React code - HTML and SVG is allowed, as well as D3.js.
|
||||
|
||||
## No animations not powered by `useCurrentFrame()`
|
||||
|
||||
Disable all animations by third party libraries.
|
||||
They will cause flickering during rendering.
|
||||
Instead, drive all animations from `useCurrentFrame()`.
|
||||
|
||||
## Bar Chart Animations
|
||||
|
||||
See [Bar Chart Example](assets/charts/bar-chart.tsx) for a basic example implmentation.
|
||||
|
||||
### Staggered Bars
|
||||
|
||||
You can animate the height of the bars and stagger them like this:
|
||||
|
||||
```tsx
|
||||
const STAGGER_DELAY = 5;
|
||||
const frame = useCurrentFrame();
|
||||
const {fps} = useVideoConfig();
|
||||
|
||||
const bars = data.map((item, i) => {
|
||||
const delay = i * STAGGER_DELAY;
|
||||
const height = spring({
|
||||
frame,
|
||||
fps,
|
||||
delay,
|
||||
config: {damping: 200},
|
||||
});
|
||||
return <div style={{height: height * item.value}} />;
|
||||
});
|
||||
```
|
||||
|
||||
## Pie Chart Animation
|
||||
|
||||
Animate segments using stroke-dashoffset, starting from 12 o'clock.
|
||||
|
||||
```tsx
|
||||
const frame = useCurrentFrame();
|
||||
const {fps} = useVideoConfig();
|
||||
|
||||
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})`} />;
|
||||
```
|
||||
141
marketing/skills/video/rules/compositions.md
Normal file
141
marketing/skills/video/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>
|
||||
```
|
||||
160
marketing/skills/video/rules/descomplicar-brand.md
Normal file
160
marketing/skills/video/rules/descomplicar-brand.md
Normal file
@@ -0,0 +1,160 @@
|
||||
# Descomplicar Brand Guidelines para Vídeos
|
||||
|
||||
## Cores da Marca
|
||||
|
||||
```tsx
|
||||
const DESCOMPLICAR_COLORS = {
|
||||
primary: "#1e3a8a", // Azul escuro
|
||||
secondary: "#7c3aed", // Roxo
|
||||
accent: "#06b6d4", // Cyan
|
||||
dark: "#0f172a", // Quase preto
|
||||
light: "#f8fafc", // Quase branco
|
||||
gradient: "linear-gradient(135deg, #1e3a8a 0%, #7c3aed 100%)",
|
||||
};
|
||||
```
|
||||
|
||||
## Tipografia
|
||||
|
||||
```tsx
|
||||
// Google Fonts recomendadas
|
||||
import { loadFont as loadInter } from "@remotion/google-fonts/Inter";
|
||||
import { loadFont as loadPoppins } from "@remotion/google-fonts/Poppins";
|
||||
|
||||
const { fontFamily: inter } = loadInter();
|
||||
const { fontFamily: poppins } = loadPoppins();
|
||||
|
||||
// Uso
|
||||
const FONTS = {
|
||||
heading: poppins, // Títulos
|
||||
body: inter, // Texto corrido
|
||||
};
|
||||
```
|
||||
|
||||
## Estilos Padrão
|
||||
|
||||
### Título Principal
|
||||
```tsx
|
||||
const titleStyle = {
|
||||
fontFamily: poppins,
|
||||
fontSize: 80,
|
||||
fontWeight: "bold",
|
||||
color: "white",
|
||||
textShadow: "0 4px 20px rgba(0,0,0,0.3)",
|
||||
};
|
||||
```
|
||||
|
||||
### Subtítulo
|
||||
```tsx
|
||||
const subtitleStyle = {
|
||||
fontFamily: inter,
|
||||
fontSize: 36,
|
||||
fontWeight: 500,
|
||||
color: "rgba(255,255,255,0.9)",
|
||||
letterSpacing: 2,
|
||||
};
|
||||
```
|
||||
|
||||
### CTA (Call-to-Action)
|
||||
```tsx
|
||||
const ctaStyle = {
|
||||
fontFamily: inter,
|
||||
fontSize: 28,
|
||||
fontWeight: 600,
|
||||
color: "#06b6d4",
|
||||
textTransform: "uppercase",
|
||||
};
|
||||
```
|
||||
|
||||
## Animações Recomendadas
|
||||
|
||||
### Entrada de Título
|
||||
```tsx
|
||||
const titleOpacity = interpolate(frame, [0, 30], [0, 1], {
|
||||
extrapolateRight: "clamp",
|
||||
});
|
||||
|
||||
const titleScale = spring({
|
||||
frame,
|
||||
fps,
|
||||
config: { damping: 12, stiffness: 100, mass: 0.5 },
|
||||
});
|
||||
```
|
||||
|
||||
### Entrada de Subtítulo (após título)
|
||||
```tsx
|
||||
const subtitleOpacity = interpolate(frame, [30, 60], [0, 1], {
|
||||
extrapolateLeft: "clamp",
|
||||
extrapolateRight: "clamp",
|
||||
});
|
||||
|
||||
const subtitleY = interpolate(frame, [30, 60], [20, 0], {
|
||||
extrapolateLeft: "clamp",
|
||||
extrapolateRight: "clamp",
|
||||
});
|
||||
```
|
||||
|
||||
### Linha Decorativa
|
||||
```tsx
|
||||
const lineWidth = interpolate(frame, [45, 90], [0, 300], {
|
||||
extrapolateLeft: "clamp",
|
||||
extrapolateRight: "clamp",
|
||||
});
|
||||
```
|
||||
|
||||
## Estrutura de Composição Padrão
|
||||
|
||||
```tsx
|
||||
export const DescomplicarVideo: React.FC<Props> = ({ title, subtitle }) => {
|
||||
const frame = useCurrentFrame();
|
||||
const { fps } = useVideoConfig();
|
||||
|
||||
return (
|
||||
<AbsoluteFill
|
||||
style={{
|
||||
background: DESCOMPLICAR_COLORS.gradient,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
{/* Logo/Título */}
|
||||
<div style={{ ...titleStyle, opacity: titleOpacity }}>
|
||||
{title}
|
||||
</div>
|
||||
|
||||
{/* Subtítulo */}
|
||||
<div style={{ ...subtitleStyle, opacity: subtitleOpacity }}>
|
||||
{subtitle}
|
||||
</div>
|
||||
|
||||
{/* Elemento decorativo */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
bottom: 80,
|
||||
width: lineWidth,
|
||||
height: 3,
|
||||
background: "rgba(255,255,255,0.7)",
|
||||
borderRadius: 2,
|
||||
}}
|
||||
/>
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
## Assinatura
|
||||
|
||||
Incluir no final de vídeos promocionais:
|
||||
```
|
||||
Descomplicar® - Crescimento Digital
|
||||
descomplicar.pt
|
||||
```
|
||||
|
||||
## Durações Recomendadas
|
||||
|
||||
| Tipo | Frames (30fps) | Segundos |
|
||||
|------|----------------|----------|
|
||||
| Logo intro | 90-150 | 3-5s |
|
||||
| Social post | 150-450 | 5-15s |
|
||||
| Promo curta | 450-900 | 15-30s |
|
||||
| Explainer | 1800+ | 60s+ |
|
||||
126
marketing/skills/video/rules/display-captions.md
Normal file
126
marketing/skills/video/rules/display-captions.md
Normal file
@@ -0,0 +1,126 @@
|
||||
---
|
||||
name: display-captions
|
||||
description: Displaying captions in Remotion with TikTok-style pages and word highlighting
|
||||
metadata:
|
||||
tags: captions, subtitles, display, tiktok, highlight
|
||||
---
|
||||
|
||||
# Displaying captions in Remotion
|
||||
|
||||
This guide explains how to display captions in Remotion, assuming you already have captions in the `Caption` format.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
First, the @remotion/captions package needs to be installed.
|
||||
If it is not installed, use the following command:
|
||||
|
||||
```bash
|
||||
npx remotion add @remotion/captions # If project uses npm
|
||||
bunx remotion add @remotion/captions # If project uses bun
|
||||
yarn remotion add @remotion/captions # If project uses yarn
|
||||
pnpm exec remotion add @remotion/captions # If project uses pnpm
|
||||
```
|
||||
|
||||
## Creating pages
|
||||
|
||||
Use `createTikTokStyleCaptions()` to group captions into pages. The `combineTokensWithinMilliseconds` option controls how many words appear at once:
|
||||
|
||||
```tsx
|
||||
import {useMemo} from 'react';
|
||||
import {createTikTokStyleCaptions} from '@remotion/captions';
|
||||
import type {Caption} from '@remotion/captions';
|
||||
|
||||
// How often captions should switch (in milliseconds)
|
||||
// Higher values = more words per page
|
||||
// Lower values = fewer words (more word-by-word)
|
||||
const SWITCH_CAPTIONS_EVERY_MS = 1200;
|
||||
|
||||
const {pages} = useMemo(() => {
|
||||
return createTikTokStyleCaptions({
|
||||
captions,
|
||||
combineTokensWithinMilliseconds: SWITCH_CAPTIONS_EVERY_MS,
|
||||
});
|
||||
}, [captions]);
|
||||
```
|
||||
|
||||
## Rendering with Sequences
|
||||
|
||||
Map over the pages and render each one in a `<Sequence>`. Calculate the start frame and duration from the page timing:
|
||||
|
||||
```tsx
|
||||
import {Sequence, useVideoConfig, AbsoluteFill} from 'remotion';
|
||||
import type {TikTokPage} from '@remotion/captions';
|
||||
|
||||
const CaptionedContent: React.FC = () => {
|
||||
const {fps} = useVideoConfig();
|
||||
|
||||
return (
|
||||
<AbsoluteFill>
|
||||
{pages.map((page, index) => {
|
||||
const nextPage = pages[index + 1] ?? null;
|
||||
const startFrame = (page.startMs / 1000) * fps;
|
||||
const endFrame = Math.min(
|
||||
nextPage ? (nextPage.startMs / 1000) * fps : Infinity,
|
||||
startFrame + (SWITCH_CAPTIONS_EVERY_MS / 1000) * fps,
|
||||
);
|
||||
const durationInFrames = endFrame - startFrame;
|
||||
|
||||
if (durationInFrames <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Sequence
|
||||
key={index}
|
||||
from={startFrame}
|
||||
durationInFrames={durationInFrames}
|
||||
>
|
||||
<CaptionPage page={page} />
|
||||
</Sequence>
|
||||
);
|
||||
})}
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
## Word highlighting
|
||||
|
||||
A caption page contains `tokens` which you can use to highlight the currently spoken word:
|
||||
|
||||
```tsx
|
||||
import {AbsoluteFill, useCurrentFrame, useVideoConfig} from 'remotion';
|
||||
import type {TikTokPage} from '@remotion/captions';
|
||||
|
||||
const HIGHLIGHT_COLOR = '#39E508';
|
||||
|
||||
const CaptionPage: React.FC<{page: TikTokPage}> = ({page}) => {
|
||||
const frame = useCurrentFrame();
|
||||
const {fps} = useVideoConfig();
|
||||
|
||||
// Current time relative to the start of the sequence
|
||||
const currentTimeMs = (frame / fps) * 1000;
|
||||
// Convert to absolute time by adding the page start
|
||||
const absoluteTimeMs = page.startMs + currentTimeMs;
|
||||
|
||||
return (
|
||||
<AbsoluteFill style={{justifyContent: 'center', alignItems: 'center'}}>
|
||||
<div style={{fontSize: 80, fontWeight: 'bold', whiteSpace: 'pre'}}>
|
||||
{page.tokens.map((token) => {
|
||||
const isActive =
|
||||
token.fromMs <= absoluteTimeMs && token.toMs > absoluteTimeMs;
|
||||
|
||||
return (
|
||||
<span
|
||||
key={token.fromMs}
|
||||
style={{color: isActive ? HIGHLIGHT_COLOR : 'white'}}
|
||||
>
|
||||
{token.text}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
```
|
||||
229
marketing/skills/video/rules/extract-frames.md
Normal file
229
marketing/skills/video/rules/extract-frames.md
Normal file
@@ -0,0 +1,229 @@
|
||||
---
|
||||
name: extract-frames
|
||||
description: Extract frames from videos at specific timestamps using Mediabunny
|
||||
metadata:
|
||||
tags: frames, extract, video, thumbnail, filmstrip, canvas
|
||||
---
|
||||
|
||||
# Extracting frames from videos
|
||||
|
||||
Use Mediabunny to extract frames from videos at specific timestamps. This is useful for generating thumbnails, filmstrips, or processing individual frames.
|
||||
|
||||
## The `extractFrames()` function
|
||||
|
||||
This function can be copy-pasted into any project.
|
||||
|
||||
```tsx
|
||||
import {
|
||||
ALL_FORMATS,
|
||||
Input,
|
||||
UrlSource,
|
||||
VideoSample,
|
||||
VideoSampleSink,
|
||||
} from "mediabunny";
|
||||
|
||||
type Options = {
|
||||
track: { width: number; height: number };
|
||||
container: string;
|
||||
durationInSeconds: number | null;
|
||||
};
|
||||
|
||||
export type ExtractFramesTimestampsInSecondsFn = (
|
||||
options: Options
|
||||
) => Promise<number[]> | number[];
|
||||
|
||||
export type ExtractFramesProps = {
|
||||
src: string;
|
||||
timestampsInSeconds: number[] | ExtractFramesTimestampsInSecondsFn;
|
||||
onVideoSample: (sample: VideoSample) => void;
|
||||
signal?: AbortSignal;
|
||||
};
|
||||
|
||||
export async function extractFrames({
|
||||
src,
|
||||
timestampsInSeconds,
|
||||
onVideoSample,
|
||||
signal,
|
||||
}: ExtractFramesProps): Promise<void> {
|
||||
using input = new Input({
|
||||
formats: ALL_FORMATS,
|
||||
source: new UrlSource(src),
|
||||
});
|
||||
|
||||
const [durationInSeconds, format, videoTrack] = await Promise.all([
|
||||
input.computeDuration(),
|
||||
input.getFormat(),
|
||||
input.getPrimaryVideoTrack(),
|
||||
]);
|
||||
|
||||
if (!videoTrack) {
|
||||
throw new Error("No video track found in the input");
|
||||
}
|
||||
|
||||
if (signal?.aborted) {
|
||||
throw new Error("Aborted");
|
||||
}
|
||||
|
||||
const timestamps =
|
||||
typeof timestampsInSeconds === "function"
|
||||
? await timestampsInSeconds({
|
||||
track: {
|
||||
width: videoTrack.displayWidth,
|
||||
height: videoTrack.displayHeight,
|
||||
},
|
||||
container: format.name,
|
||||
durationInSeconds,
|
||||
})
|
||||
: timestampsInSeconds;
|
||||
|
||||
if (timestamps.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (signal?.aborted) {
|
||||
throw new Error("Aborted");
|
||||
}
|
||||
|
||||
const sink = new VideoSampleSink(videoTrack);
|
||||
|
||||
for await (using videoSample of sink.samplesAtTimestamps(timestamps)) {
|
||||
if (signal?.aborted) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (!videoSample) {
|
||||
continue;
|
||||
}
|
||||
|
||||
onVideoSample(videoSample);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Basic usage
|
||||
|
||||
Extract frames at specific timestamps:
|
||||
|
||||
```tsx
|
||||
await extractFrames({
|
||||
src: "https://remotion.media/video.mp4",
|
||||
timestampsInSeconds: [0, 1, 2, 3, 4],
|
||||
onVideoSample: (sample) => {
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = sample.displayWidth;
|
||||
canvas.height = sample.displayHeight;
|
||||
const ctx = canvas.getContext("2d");
|
||||
sample.draw(ctx!, 0, 0);
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## Creating a filmstrip
|
||||
|
||||
Use a callback function to dynamically calculate timestamps based on video metadata:
|
||||
|
||||
```tsx
|
||||
const canvasWidth = 500;
|
||||
const canvasHeight = 80;
|
||||
const fromSeconds = 0;
|
||||
const toSeconds = 10;
|
||||
|
||||
await extractFrames({
|
||||
src: "https://remotion.media/video.mp4",
|
||||
timestampsInSeconds: async ({ track, durationInSeconds }) => {
|
||||
const aspectRatio = track.width / track.height;
|
||||
const amountOfFramesFit = Math.ceil(
|
||||
canvasWidth / (canvasHeight * aspectRatio)
|
||||
);
|
||||
const segmentDuration = toSeconds - fromSeconds;
|
||||
const timestamps: number[] = [];
|
||||
|
||||
for (let i = 0; i < amountOfFramesFit; i++) {
|
||||
timestamps.push(
|
||||
fromSeconds + (segmentDuration / amountOfFramesFit) * (i + 0.5)
|
||||
);
|
||||
}
|
||||
|
||||
return timestamps;
|
||||
},
|
||||
onVideoSample: (sample) => {
|
||||
console.log(`Frame at ${sample.timestamp}s`);
|
||||
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = sample.displayWidth;
|
||||
canvas.height = sample.displayHeight;
|
||||
const ctx = canvas.getContext("2d");
|
||||
sample.draw(ctx!, 0, 0);
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## Cancellation with AbortSignal
|
||||
|
||||
Cancel frame extraction after a timeout:
|
||||
|
||||
```tsx
|
||||
const controller = new AbortController();
|
||||
|
||||
setTimeout(() => controller.abort(), 5000);
|
||||
|
||||
try {
|
||||
await extractFrames({
|
||||
src: "https://remotion.media/video.mp4",
|
||||
timestampsInSeconds: [0, 1, 2, 3, 4],
|
||||
onVideoSample: (sample) => {
|
||||
using frame = sample;
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = frame.displayWidth;
|
||||
canvas.height = frame.displayHeight;
|
||||
const ctx = canvas.getContext("2d");
|
||||
frame.draw(ctx!, 0, 0);
|
||||
},
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
||||
console.log("Frame extraction complete!");
|
||||
} catch (error) {
|
||||
console.error("Frame extraction was aborted or failed:", error);
|
||||
}
|
||||
```
|
||||
|
||||
## Timeout with Promise.race
|
||||
|
||||
```tsx
|
||||
const controller = new AbortController();
|
||||
|
||||
const timeoutPromise = new Promise<never>((_, reject) => {
|
||||
const timeoutId = setTimeout(() => {
|
||||
controller.abort();
|
||||
reject(new Error("Frame extraction timed out after 10 seconds"));
|
||||
}, 10000);
|
||||
|
||||
controller.signal.addEventListener("abort", () => clearTimeout(timeoutId), {
|
||||
once: true,
|
||||
});
|
||||
});
|
||||
|
||||
try {
|
||||
await Promise.race([
|
||||
extractFrames({
|
||||
src: "https://remotion.media/video.mp4",
|
||||
timestampsInSeconds: [0, 1, 2, 3, 4],
|
||||
onVideoSample: (sample) => {
|
||||
using frame = sample;
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = frame.displayWidth;
|
||||
canvas.height = frame.displayHeight;
|
||||
const ctx = canvas.getContext("2d");
|
||||
frame.draw(ctx!, 0, 0);
|
||||
},
|
||||
signal: controller.signal,
|
||||
}),
|
||||
timeoutPromise,
|
||||
]);
|
||||
|
||||
console.log("Frame extraction complete!");
|
||||
} catch (error) {
|
||||
console.error("Frame extraction was aborted or failed:", error);
|
||||
}
|
||||
```
|
||||
152
marketing/skills/video/rules/fonts.md
Normal file
152
marketing/skills/video/rules/fonts.md
Normal file
@@ -0,0 +1,152 @@
|
||||
---
|
||||
name: fonts
|
||||
description: Loading Google Fonts and local fonts in Remotion
|
||||
metadata:
|
||||
tags: fonts, google-fonts, typography, text
|
||||
---
|
||||
|
||||
# Using fonts in Remotion
|
||||
|
||||
## Google Fonts with @remotion/google-fonts
|
||||
|
||||
The recommended way to use Google Fonts. It's type-safe and automatically blocks rendering until the font is ready.
|
||||
|
||||
### Prerequisites
|
||||
|
||||
First, the @remotion/google-fonts package needs to be installed.
|
||||
If it is not installed, use the following command:
|
||||
|
||||
```bash
|
||||
npx remotion add @remotion/google-fonts # If project uses npm
|
||||
bunx remotion add @remotion/google-fonts # If project uses bun
|
||||
yarn remotion add @remotion/google-fonts # If project uses yarn
|
||||
pnpm exec remotion add @remotion/google-fonts # If project uses pnpm
|
||||
```
|
||||
|
||||
```tsx
|
||||
import { loadFont } from "@remotion/google-fonts/Lobster";
|
||||
|
||||
const { fontFamily } = loadFont();
|
||||
|
||||
export const MyComposition = () => {
|
||||
return <div style={{ fontFamily }}>Hello World</div>;
|
||||
};
|
||||
```
|
||||
|
||||
Preferrably, specify only needed weights and subsets to reduce file size:
|
||||
|
||||
```tsx
|
||||
import { loadFont } from "@remotion/google-fonts/Roboto";
|
||||
|
||||
const { fontFamily } = loadFont("normal", {
|
||||
weights: ["400", "700"],
|
||||
subsets: ["latin"],
|
||||
});
|
||||
```
|
||||
|
||||
### Waiting for font to load
|
||||
|
||||
Use `waitUntilDone()` if you need to know when the font is ready:
|
||||
|
||||
```tsx
|
||||
import { loadFont } from "@remotion/google-fonts/Lobster";
|
||||
|
||||
const { fontFamily, waitUntilDone } = loadFont();
|
||||
|
||||
await waitUntilDone();
|
||||
```
|
||||
|
||||
## Local fonts with @remotion/fonts
|
||||
|
||||
For local font files, use the `@remotion/fonts` package.
|
||||
|
||||
### Prerequisites
|
||||
|
||||
First, install @remotion/fonts:
|
||||
|
||||
```bash
|
||||
npx remotion add @remotion/fonts # If project uses npm
|
||||
bunx remotion add @remotion/fonts # If project uses bun
|
||||
yarn remotion add @remotion/fonts # If project uses yarn
|
||||
pnpm exec remotion add @remotion/fonts # If project uses pnpm
|
||||
```
|
||||
|
||||
### Loading a local font
|
||||
|
||||
Place your font file in the `public/` folder and use `loadFont()`:
|
||||
|
||||
```tsx
|
||||
import { loadFont } from "@remotion/fonts";
|
||||
import { staticFile } from "remotion";
|
||||
|
||||
await loadFont({
|
||||
family: "MyFont",
|
||||
url: staticFile("MyFont-Regular.woff2"),
|
||||
});
|
||||
|
||||
export const MyComposition = () => {
|
||||
return <div style={{ fontFamily: "MyFont" }}>Hello World</div>;
|
||||
};
|
||||
```
|
||||
|
||||
### Loading multiple weights
|
||||
|
||||
Load each weight separately with the same family name:
|
||||
|
||||
```tsx
|
||||
import { loadFont } from "@remotion/fonts";
|
||||
import { staticFile } from "remotion";
|
||||
|
||||
await Promise.all([
|
||||
loadFont({
|
||||
family: "Inter",
|
||||
url: staticFile("Inter-Regular.woff2"),
|
||||
weight: "400",
|
||||
}),
|
||||
loadFont({
|
||||
family: "Inter",
|
||||
url: staticFile("Inter-Bold.woff2"),
|
||||
weight: "700",
|
||||
}),
|
||||
]);
|
||||
```
|
||||
|
||||
### Available options
|
||||
|
||||
```tsx
|
||||
loadFont({
|
||||
family: "MyFont", // Required: name to use in CSS
|
||||
url: staticFile("font.woff2"), // Required: font file URL
|
||||
format: "woff2", // Optional: auto-detected from extension
|
||||
weight: "400", // Optional: font weight
|
||||
style: "normal", // Optional: normal or italic
|
||||
display: "block", // Optional: font-display behavior
|
||||
});
|
||||
```
|
||||
|
||||
## Using in components
|
||||
|
||||
Call `loadFont()` at the top level of your component or in a separate file that's imported early:
|
||||
|
||||
```tsx
|
||||
import { loadFont } from "@remotion/google-fonts/Montserrat";
|
||||
|
||||
const { fontFamily } = loadFont("normal", {
|
||||
weights: ["400", "700"],
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
export const Title: React.FC<{ text: string }> = ({ text }) => {
|
||||
return (
|
||||
<h1
|
||||
style={{
|
||||
fontFamily,
|
||||
fontSize: 80,
|
||||
fontWeight: "bold",
|
||||
}}
|
||||
>
|
||||
{text}
|
||||
</h1>
|
||||
);
|
||||
};
|
||||
```
|
||||
58
marketing/skills/video/rules/get-audio-duration.md
Normal file
58
marketing/skills/video/rules/get-audio-duration.md
Normal file
@@ -0,0 +1,58 @@
|
||||
---
|
||||
name: get-audio-duration
|
||||
description: Getting the duration of an audio file in seconds with Mediabunny
|
||||
metadata:
|
||||
tags: duration, audio, length, time, seconds, mp3, wav
|
||||
---
|
||||
|
||||
# Getting audio duration with Mediabunny
|
||||
|
||||
Mediabunny can extract the duration of an audio file. It works in browser, Node.js, and Bun environments.
|
||||
|
||||
## Getting audio duration
|
||||
|
||||
```tsx
|
||||
import { Input, ALL_FORMATS, UrlSource } from "mediabunny";
|
||||
|
||||
export const getAudioDuration = async (src: string) => {
|
||||
const input = new Input({
|
||||
formats: ALL_FORMATS,
|
||||
source: new UrlSource(src, {
|
||||
getRetryDelay: () => null,
|
||||
}),
|
||||
});
|
||||
|
||||
const durationInSeconds = await input.computeDuration();
|
||||
return durationInSeconds;
|
||||
};
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```tsx
|
||||
const duration = await getAudioDuration("https://remotion.media/audio.mp3");
|
||||
console.log(duration); // e.g. 180.5 (seconds)
|
||||
```
|
||||
|
||||
## Using with local files
|
||||
|
||||
For local files, use `FileSource` instead of `UrlSource`:
|
||||
|
||||
```tsx
|
||||
import { Input, ALL_FORMATS, FileSource } from "mediabunny";
|
||||
|
||||
const input = new Input({
|
||||
formats: ALL_FORMATS,
|
||||
source: new FileSource(file), // File object from input or drag-drop
|
||||
});
|
||||
|
||||
const durationInSeconds = await input.computeDuration();
|
||||
```
|
||||
|
||||
## Using with staticFile in Remotion
|
||||
|
||||
```tsx
|
||||
import { staticFile } from "remotion";
|
||||
|
||||
const duration = await getAudioDuration(staticFile("audio.mp3"));
|
||||
```
|
||||
68
marketing/skills/video/rules/get-video-dimensions.md
Normal file
68
marketing/skills/video/rules/get-video-dimensions.md
Normal file
@@ -0,0 +1,68 @@
|
||||
---
|
||||
name: get-video-dimensions
|
||||
description: Getting the width and height of a video file with Mediabunny
|
||||
metadata:
|
||||
tags: dimensions, width, height, resolution, size, video
|
||||
---
|
||||
|
||||
# Getting video dimensions with Mediabunny
|
||||
|
||||
Mediabunny can extract the width and height of a video file. It works in browser, Node.js, and Bun environments.
|
||||
|
||||
## Getting video dimensions
|
||||
|
||||
```tsx
|
||||
import { Input, ALL_FORMATS, UrlSource } from "mediabunny";
|
||||
|
||||
export const getVideoDimensions = async (src: string) => {
|
||||
const input = new Input({
|
||||
formats: ALL_FORMATS,
|
||||
source: new UrlSource(src, {
|
||||
getRetryDelay: () => null,
|
||||
}),
|
||||
});
|
||||
|
||||
const videoTrack = await input.getPrimaryVideoTrack();
|
||||
if (!videoTrack) {
|
||||
throw new Error("No video track found");
|
||||
}
|
||||
|
||||
return {
|
||||
width: videoTrack.displayWidth,
|
||||
height: videoTrack.displayHeight,
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```tsx
|
||||
const dimensions = await getVideoDimensions("https://remotion.media/video.mp4");
|
||||
console.log(dimensions.width); // e.g. 1920
|
||||
console.log(dimensions.height); // e.g. 1080
|
||||
```
|
||||
|
||||
## Using with local files
|
||||
|
||||
For local files, use `FileSource` instead of `UrlSource`:
|
||||
|
||||
```tsx
|
||||
import { Input, ALL_FORMATS, FileSource } from "mediabunny";
|
||||
|
||||
const input = new Input({
|
||||
formats: ALL_FORMATS,
|
||||
source: new FileSource(file), // File object from input or drag-drop
|
||||
});
|
||||
|
||||
const videoTrack = await input.getPrimaryVideoTrack();
|
||||
const width = videoTrack.displayWidth;
|
||||
const height = videoTrack.displayHeight;
|
||||
```
|
||||
|
||||
## Using with staticFile in Remotion
|
||||
|
||||
```tsx
|
||||
import { staticFile } from "remotion";
|
||||
|
||||
const dimensions = await getVideoDimensions(staticFile("video.mp4"));
|
||||
```
|
||||
58
marketing/skills/video/rules/get-video-duration.md
Normal file
58
marketing/skills/video/rules/get-video-duration.md
Normal file
@@ -0,0 +1,58 @@
|
||||
---
|
||||
name: get-video-duration
|
||||
description: Getting the duration of a video file in seconds with Mediabunny
|
||||
metadata:
|
||||
tags: duration, video, length, time, seconds
|
||||
---
|
||||
|
||||
# Getting video duration with Mediabunny
|
||||
|
||||
Mediabunny can extract the duration of a video file. It works in browser, Node.js, and Bun environments.
|
||||
|
||||
## Getting video duration
|
||||
|
||||
```tsx
|
||||
import { Input, ALL_FORMATS, UrlSource } from "mediabunny";
|
||||
|
||||
export const getVideoDuration = async (src: string) => {
|
||||
const input = new Input({
|
||||
formats: ALL_FORMATS,
|
||||
source: new UrlSource(src, {
|
||||
getRetryDelay: () => null,
|
||||
}),
|
||||
});
|
||||
|
||||
const durationInSeconds = await input.computeDuration();
|
||||
return durationInSeconds;
|
||||
};
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```tsx
|
||||
const duration = await getVideoDuration("https://remotion.media/video.mp4");
|
||||
console.log(duration); // e.g. 10.5 (seconds)
|
||||
```
|
||||
|
||||
## Using with local files
|
||||
|
||||
For local files, use `FileSource` instead of `UrlSource`:
|
||||
|
||||
```tsx
|
||||
import { Input, ALL_FORMATS, FileSource } from "mediabunny";
|
||||
|
||||
const input = new Input({
|
||||
formats: ALL_FORMATS,
|
||||
source: new FileSource(file), // File object from input or drag-drop
|
||||
});
|
||||
|
||||
const durationInSeconds = await input.computeDuration();
|
||||
```
|
||||
|
||||
## Using with staticFile in Remotion
|
||||
|
||||
```tsx
|
||||
import { staticFile } from "remotion";
|
||||
|
||||
const duration = await getVideoDuration(staticFile("video.mp4"));
|
||||
```
|
||||
138
marketing/skills/video/rules/gifs.md
Normal file
138
marketing/skills/video/rules/gifs.md
Normal file
@@ -0,0 +1,138 @@
|
||||
---
|
||||
name: gif
|
||||
description: Displaying GIFs, APNG, AVIF and WebP in Remotion
|
||||
metadata:
|
||||
tags: gif, animation, images, animated, apng, avif, webp
|
||||
---
|
||||
|
||||
# Using Animated images in Remotion
|
||||
|
||||
## Basic usage
|
||||
|
||||
Use `<AnimatedImage>` to display a GIF, APNG, AVIF or WebP image synchronized with Remotion's timeline:
|
||||
|
||||
```tsx
|
||||
import {AnimatedImage, staticFile} from 'remotion';
|
||||
|
||||
export const MyComposition = () => {
|
||||
return <AnimatedImage src={staticFile('animation.gif')} width={500} height={500} />;
|
||||
};
|
||||
```
|
||||
|
||||
Remote URLs are also supported (must have CORS enabled):
|
||||
|
||||
```tsx
|
||||
<AnimatedImage src="https://example.com/animation.gif" width={500} height={500} />
|
||||
```
|
||||
|
||||
## Sizing and fit
|
||||
|
||||
Control how the image fills its container with the `fit` prop:
|
||||
|
||||
```tsx
|
||||
// Stretch to fill (default)
|
||||
<AnimatedImage src={staticFile("animation.gif")} width={500} height={300} fit="fill" />
|
||||
|
||||
// Maintain aspect ratio, fit inside container
|
||||
<AnimatedImage src={staticFile("animation.gif")} width={500} height={300} fit="contain" />
|
||||
|
||||
// Fill container, crop if needed
|
||||
<AnimatedImage src={staticFile("animation.gif")} width={500} height={300} fit="cover" />
|
||||
```
|
||||
|
||||
## Playback speed
|
||||
|
||||
Use `playbackRate` to control the animation speed:
|
||||
|
||||
```tsx
|
||||
<AnimatedImage src={staticFile("animation.gif")} width={500} height={500} playbackRate={2} /> {/* 2x speed */}
|
||||
<AnimatedImage src={staticFile("animation.gif")} width={500} height={500} playbackRate={0.5} /> {/* Half speed */}
|
||||
```
|
||||
|
||||
## Looping behavior
|
||||
|
||||
Control what happens when the animation finishes:
|
||||
|
||||
```tsx
|
||||
// Loop indefinitely (default)
|
||||
<AnimatedImage src={staticFile("animation.gif")} width={500} height={500} loopBehavior="loop" />
|
||||
|
||||
// Play once, show final frame
|
||||
<AnimatedImage src={staticFile("animation.gif")} width={500} height={500} loopBehavior="pause-after-finish" />
|
||||
|
||||
// Play once, then clear canvas
|
||||
<AnimatedImage src={staticFile("animation.gif")} width={500} height={500} loopBehavior="clear-after-finish" />
|
||||
```
|
||||
|
||||
## Styling
|
||||
|
||||
Use the `style` prop for additional CSS (use `width` and `height` props for sizing):
|
||||
|
||||
```tsx
|
||||
<AnimatedImage
|
||||
src={staticFile('animation.gif')}
|
||||
width={500}
|
||||
height={500}
|
||||
style={{
|
||||
borderRadius: 20,
|
||||
position: 'absolute',
|
||||
top: 100,
|
||||
left: 50,
|
||||
}}
|
||||
/>
|
||||
```
|
||||
|
||||
## Getting GIF duration
|
||||
|
||||
Use `getGifDurationInSeconds()` from `@remotion/gif` to get the duration of a GIF.
|
||||
|
||||
```bash
|
||||
npx remotion add @remotion/gif # If project uses npm
|
||||
bunx remotion add @remotion/gif # If project uses bun
|
||||
yarn remotion add @remotion/gif # If project uses yarn
|
||||
pnpm exec remotion add @remotion/gif # If project uses pnpm
|
||||
```
|
||||
|
||||
```tsx
|
||||
import {getGifDurationInSeconds} from '@remotion/gif';
|
||||
import {staticFile} from 'remotion';
|
||||
|
||||
const duration = await getGifDurationInSeconds(staticFile('animation.gif'));
|
||||
console.log(duration); // e.g. 2.5
|
||||
```
|
||||
|
||||
This is useful for setting the composition duration to match the GIF:
|
||||
|
||||
```tsx
|
||||
import {getGifDurationInSeconds} from '@remotion/gif';
|
||||
import {staticFile, CalculateMetadataFunction} from 'remotion';
|
||||
|
||||
const calculateMetadata: CalculateMetadataFunction = async () => {
|
||||
const duration = await getGifDurationInSeconds(staticFile('animation.gif'));
|
||||
return {
|
||||
durationInFrames: Math.ceil(duration * 30),
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
## Alternative
|
||||
|
||||
If `<AnimatedImage>` does not work (only supported in Chrome and Firefox), you can use `<Gif>` from `@remotion/gif` instead.
|
||||
|
||||
```bash
|
||||
npx remotion add @remotion/gif # If project uses npm
|
||||
bunx remotion add @remotion/gif # If project uses bun
|
||||
yarn remotion add @remotion/gif # If project uses yarn
|
||||
pnpm exec remotion add @remotion/gif # If project uses pnpm
|
||||
```
|
||||
|
||||
```tsx
|
||||
import {Gif} from '@remotion/gif';
|
||||
import {staticFile} from 'remotion';
|
||||
|
||||
export const MyComposition = () => {
|
||||
return <Gif src={staticFile('animation.gif')} width={500} height={500} />;
|
||||
};
|
||||
```
|
||||
|
||||
The `<Gif>` component has the same props as `<AnimatedImage>` but only supports GIF files.
|
||||
130
marketing/skills/video/rules/images.md
Normal file
130
marketing/skills/video/rules/images.md
Normal file
@@ -0,0 +1,130 @@
|
||||
---
|
||||
name: images
|
||||
description: Embedding images in Remotion using the <Img> component
|
||||
metadata:
|
||||
tags: images, img, staticFile, png, jpg, svg, webp
|
||||
---
|
||||
|
||||
# Using images in Remotion
|
||||
|
||||
## The `<Img>` component
|
||||
|
||||
Always use the `<Img>` component from `remotion` to display images:
|
||||
|
||||
```tsx
|
||||
import { Img, staticFile } from "remotion";
|
||||
|
||||
export const MyComposition = () => {
|
||||
return <Img src={staticFile("photo.png")} />;
|
||||
};
|
||||
```
|
||||
|
||||
## Important restrictions
|
||||
|
||||
**You MUST use the `<Img>` component from `remotion`.** Do not use:
|
||||
|
||||
- Native HTML `<img>` elements
|
||||
- Next.js `<Image>` component
|
||||
- CSS `background-image`
|
||||
|
||||
The `<Img>` component ensures images are fully loaded before rendering, preventing flickering and blank frames during video export.
|
||||
|
||||
## Local images with staticFile()
|
||||
|
||||
Place images in the `public/` folder and use `staticFile()` to reference them:
|
||||
|
||||
```
|
||||
my-video/
|
||||
├─ public/
|
||||
│ ├─ logo.png
|
||||
│ ├─ avatar.jpg
|
||||
│ └─ icon.svg
|
||||
├─ src/
|
||||
├─ package.json
|
||||
```
|
||||
|
||||
```tsx
|
||||
import { Img, staticFile } from "remotion";
|
||||
|
||||
<Img src={staticFile("logo.png")} />
|
||||
```
|
||||
|
||||
## Remote images
|
||||
|
||||
Remote URLs can be used directly without `staticFile()`:
|
||||
|
||||
```tsx
|
||||
<Img src="https://example.com/image.png" />
|
||||
```
|
||||
|
||||
Ensure remote images have CORS enabled.
|
||||
|
||||
For animated GIFs, use the `<Gif>` component from `@remotion/gif` instead.
|
||||
|
||||
## Sizing and positioning
|
||||
|
||||
Use the `style` prop to control size and position:
|
||||
|
||||
```tsx
|
||||
<Img
|
||||
src={staticFile("photo.png")}
|
||||
style={{
|
||||
width: 500,
|
||||
height: 300,
|
||||
position: "absolute",
|
||||
top: 100,
|
||||
left: 50,
|
||||
objectFit: "cover",
|
||||
}}
|
||||
/>
|
||||
```
|
||||
|
||||
## Dynamic image paths
|
||||
|
||||
Use template literals for dynamic file references:
|
||||
|
||||
```tsx
|
||||
import { Img, staticFile, useCurrentFrame } from "remotion";
|
||||
|
||||
const frame = useCurrentFrame();
|
||||
|
||||
// Image sequence
|
||||
<Img src={staticFile(`frames/frame${frame}.png`)} />
|
||||
|
||||
// Selecting based on props
|
||||
<Img src={staticFile(`avatars/${props.userId}.png`)} />
|
||||
|
||||
// Conditional images
|
||||
<Img src={staticFile(`icons/${isActive ? "active" : "inactive"}.svg`)} />
|
||||
```
|
||||
|
||||
This pattern is useful for:
|
||||
|
||||
- Image sequences (frame-by-frame animations)
|
||||
- User-specific avatars or profile images
|
||||
- Theme-based icons
|
||||
- State-dependent graphics
|
||||
|
||||
## Getting image dimensions
|
||||
|
||||
Use `getImageDimensions()` to get the dimensions of an image:
|
||||
|
||||
```tsx
|
||||
import { getImageDimensions, staticFile } from "remotion";
|
||||
|
||||
const { width, height } = await getImageDimensions(staticFile("photo.png"));
|
||||
```
|
||||
|
||||
This is useful for calculating aspect ratios or sizing compositions:
|
||||
|
||||
```tsx
|
||||
import { getImageDimensions, staticFile, CalculateMetadataFunction } from "remotion";
|
||||
|
||||
const calculateMetadata: CalculateMetadataFunction = async () => {
|
||||
const { width, height } = await getImageDimensions(staticFile("photo.png"));
|
||||
return {
|
||||
width,
|
||||
height,
|
||||
};
|
||||
};
|
||||
```
|
||||
67
marketing/skills/video/rules/import-srt-captions.md
Normal file
67
marketing/skills/video/rules/import-srt-captions.md
Normal file
@@ -0,0 +1,67 @@
|
||||
---
|
||||
name: import-srt-captions
|
||||
description: Importing .srt subtitle files into Remotion using @remotion/captions
|
||||
metadata:
|
||||
tags: captions, subtitles, srt, import, parse
|
||||
---
|
||||
|
||||
# Importing .srt subtitles into Remotion
|
||||
|
||||
If you have an existing `.srt` subtitle file, you can import it into Remotion using `parseSrt()` from `@remotion/captions`.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
First, the @remotion/captions package needs to be installed.
|
||||
If it is not installed, use the following command:
|
||||
|
||||
```bash
|
||||
npx remotion add @remotion/captions # If project uses npm
|
||||
bunx remotion add @remotion/captions # If project uses bun
|
||||
yarn remotion add @remotion/captions # If project uses yarn
|
||||
pnpm exec remotion add @remotion/captions # If project uses pnpm
|
||||
```
|
||||
|
||||
## Reading an .srt file
|
||||
|
||||
Use `staticFile()` to reference an `.srt` file in your `public` folder, then fetch and parse it:
|
||||
|
||||
```tsx
|
||||
import {useState, useEffect, useCallback} from 'react';
|
||||
import {AbsoluteFill, staticFile, useDelayRender} from 'remotion';
|
||||
import {parseSrt} from '@remotion/captions';
|
||||
import type {Caption} from '@remotion/captions';
|
||||
|
||||
export const MyComponent: React.FC = () => {
|
||||
const [captions, setCaptions] = useState<Caption[] | null>(null);
|
||||
const {delayRender, continueRender, cancelRender} = useDelayRender();
|
||||
const [handle] = useState(() => delayRender());
|
||||
|
||||
const fetchCaptions = useCallback(async () => {
|
||||
try {
|
||||
const response = await fetch(staticFile('subtitles.srt'));
|
||||
const text = await response.text();
|
||||
const {captions: parsed} = parseSrt({input: text});
|
||||
setCaptions(parsed);
|
||||
continueRender(handle);
|
||||
} catch (e) {
|
||||
cancelRender(e);
|
||||
}
|
||||
}, [continueRender, cancelRender, handle]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchCaptions();
|
||||
}, [fetchCaptions]);
|
||||
|
||||
if (!captions) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <AbsoluteFill>{/* Use captions here */}</AbsoluteFill>;
|
||||
};
|
||||
```
|
||||
|
||||
Remote URLs are also supported - you can `fetch()` a remote file via URL instead of using `staticFile()`.
|
||||
|
||||
## Using imported captions
|
||||
|
||||
Once parsed, the captions are in the `Caption` format and can be used with all `@remotion/captions` utilities.
|
||||
68
marketing/skills/video/rules/lottie.md
Normal file
68
marketing/skills/video/rules/lottie.md
Normal file
@@ -0,0 +1,68 @@
|
||||
---
|
||||
name: lottie
|
||||
description: Embedding Lottie animations in Remotion.
|
||||
metadata:
|
||||
category: Animation
|
||||
---
|
||||
|
||||
# Using Lottie Animations in Remotion
|
||||
|
||||
## Prerequisites
|
||||
|
||||
First, the @remotion/lottie package needs to be installed.
|
||||
If it is not, use the following command:
|
||||
|
||||
```bash
|
||||
npx remotion add @remotion/lottie # If project uses npm
|
||||
bunx remotion add @remotion/lottie # If project uses bun
|
||||
yarn remotion add @remotion/lottie # If project uses yarn
|
||||
pnpm exec remotion add @remotion/lottie # If project uses pnpm
|
||||
```
|
||||
|
||||
## Displaying a Lottie file
|
||||
|
||||
To import a Lottie animation:
|
||||
|
||||
- Fetch the Lottie asset
|
||||
- Wrap the loading process in `delayRender()` and `continueRender()`
|
||||
- Save the animation data in a state
|
||||
- Render the Lottie animation using the `Lottie` component from the `@remotion/lottie` package
|
||||
|
||||
```tsx
|
||||
import {Lottie, LottieAnimationData} from '@remotion/lottie';
|
||||
import {useEffect, useState} from 'react';
|
||||
import {cancelRender, continueRender, delayRender} from 'remotion';
|
||||
|
||||
export const MyAnimation = () => {
|
||||
const [handle] = useState(() => delayRender('Loading Lottie animation'));
|
||||
|
||||
const [animationData, setAnimationData] = useState<LottieAnimationData | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
fetch('https://assets4.lottiefiles.com/packages/lf20_zyquagfl.json')
|
||||
.then((data) => data.json())
|
||||
.then((json) => {
|
||||
setAnimationData(json);
|
||||
continueRender(handle);
|
||||
})
|
||||
.catch((err) => {
|
||||
cancelRender(err);
|
||||
});
|
||||
}, [handle]);
|
||||
|
||||
if (!animationData) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <Lottie animationData={animationData} />;
|
||||
};
|
||||
```
|
||||
|
||||
## Styling and animating
|
||||
|
||||
Lottie supports the `style` prop to allow styles and animations:
|
||||
|
||||
```tsx
|
||||
return <Lottie animationData={animationData} style={{width: 400, height: 400}} />;
|
||||
```
|
||||
|
||||
403
marketing/skills/video/rules/maps.md
Normal file
403
marketing/skills/video/rules/maps.md
Normal file
@@ -0,0 +1,403 @@
|
||||
---
|
||||
name: maps
|
||||
description: Make map animations with Mapbox
|
||||
metadata:
|
||||
tags: map, map animation, mapbox
|
||||
---
|
||||
|
||||
Maps can be added to a Remotion video with Mapbox.
|
||||
The [Mapbox documentation](https://docs.mapbox.com/mapbox-gl-js/api/) has the API reference.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Mapbox and `@turf/turf` need to be installed.
|
||||
|
||||
Search the project for lockfiles and run the correct command depending on the package manager:
|
||||
|
||||
If `package-lock.json` is found, use the following command:
|
||||
|
||||
```bash
|
||||
npm i mapbox-gl @turf/turf @types/mapbox-gl
|
||||
```
|
||||
|
||||
If `bun.lock` is found, use the following command:
|
||||
|
||||
```bash
|
||||
bun i mapbox-gl @turf/turf @types/mapbox-gl
|
||||
```
|
||||
|
||||
If `yarn.lock` is found, use the following command:
|
||||
|
||||
```bash
|
||||
yarn add mapbox-gl @turf/turf @types/mapbox-gl
|
||||
```
|
||||
|
||||
If `pnpm-lock.yaml` is found, use the following command:
|
||||
|
||||
```bash
|
||||
pnpm i mapbox-gl @turf/turf @types/mapbox-gl
|
||||
```
|
||||
|
||||
The user needs to create a free Mapbox account and create an access token by visiting https://console.mapbox.com/account/access-tokens/.
|
||||
|
||||
The mapbox token needs to be added to the `.env` file:
|
||||
|
||||
```txt title=".env"
|
||||
REMOTION_MAPBOX_TOKEN==pk.your-mapbox-access-token
|
||||
```
|
||||
|
||||
## Adding a map
|
||||
|
||||
Here is a basic example of a map in Remotion.
|
||||
|
||||
```tsx
|
||||
import {useEffect, useMemo, useRef, useState} from 'react';
|
||||
import {AbsoluteFill, useDelayRender, useVideoConfig} from 'remotion';
|
||||
import mapboxgl, {Map} from 'mapbox-gl';
|
||||
|
||||
export const lineCoordinates = [
|
||||
[6.56158447265625, 46.059891147620725],
|
||||
[6.5691375732421875, 46.05679376154153],
|
||||
[6.5842437744140625, 46.05059898938315],
|
||||
[6.594886779785156, 46.04702502069337],
|
||||
[6.601066589355469, 46.0460718554722],
|
||||
[6.6089630126953125, 46.0365370783104],
|
||||
[6.6185760498046875, 46.018420689207964],
|
||||
];
|
||||
|
||||
mapboxgl.accessToken = process.env.REMOTION_MAPBOX_TOKEN as string;
|
||||
|
||||
export const MyComposition = () => {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const {delayRender, continueRender} = useDelayRender();
|
||||
|
||||
const {width, height} = useVideoConfig();
|
||||
const [handle] = useState(() => delayRender('Loading map...'));
|
||||
const [map, setMap] = useState<Map | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const _map = new Map({
|
||||
container: ref.current!,
|
||||
zoom: 11.53,
|
||||
center: [6.5615, 46.0598],
|
||||
pitch: 65,
|
||||
bearing: 0,
|
||||
style: 'mapbox://styles/mapbox/standard',
|
||||
interactive: false,
|
||||
fadeDuration: 0,
|
||||
});
|
||||
|
||||
_map.on('style.load', () => {
|
||||
// Hide all features from the Mapbox Standard style
|
||||
const hideFeatures = [
|
||||
'showRoadsAndTransit',
|
||||
'showRoads',
|
||||
'showTransit',
|
||||
'showPedestrianRoads',
|
||||
'showRoadLabels',
|
||||
'showTransitLabels',
|
||||
'showPlaceLabels',
|
||||
'showPointOfInterestLabels',
|
||||
'showPointsOfInterest',
|
||||
'showAdminBoundaries',
|
||||
'showLandmarkIcons',
|
||||
'showLandmarkIconLabels',
|
||||
'show3dObjects',
|
||||
'show3dBuildings',
|
||||
'show3dTrees',
|
||||
'show3dLandmarks',
|
||||
'show3dFacades',
|
||||
];
|
||||
for (const feature of hideFeatures) {
|
||||
_map.setConfigProperty('basemap', feature, false);
|
||||
}
|
||||
|
||||
_map.setConfigProperty('basemap', 'colorMotorways', 'rgba(0, 0, 0, 0)');
|
||||
_map.setConfigProperty('basemap', 'colorRoads', 'rgba(0, 0, 0, 0)');
|
||||
_map.setConfigProperty('basemap', 'colorTrunks', 'rgba(0, 0, 0, 0)');
|
||||
|
||||
_map.addSource('trace', {
|
||||
type: 'geojson',
|
||||
data: {
|
||||
type: 'Feature',
|
||||
properties: {},
|
||||
geometry: {
|
||||
type: 'LineString',
|
||||
coordinates: lineCoordinates,
|
||||
},
|
||||
},
|
||||
});
|
||||
_map.addLayer({
|
||||
type: 'line',
|
||||
source: 'trace',
|
||||
id: 'line',
|
||||
paint: {
|
||||
'line-color': 'black',
|
||||
'line-width': 5,
|
||||
},
|
||||
layout: {
|
||||
'line-cap': 'round',
|
||||
'line-join': 'round',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
_map.on('load', () => {
|
||||
continueRender(handle);
|
||||
setMap(_map);
|
||||
});
|
||||
}, [handle, lineCoordinates]);
|
||||
|
||||
const style: React.CSSProperties = useMemo(() => ({width, height, position: 'absolute'}), [width, height]);
|
||||
|
||||
return <AbsoluteFill ref={ref} style={style} />;
|
||||
};
|
||||
```
|
||||
|
||||
The following is important in Remotion:
|
||||
|
||||
- Animations must be driven by `useCurrentFrame()` and animations that Mapbox brings itself should be disabled. For example, the `fadeDuration` prop should be set to `0`, `interactive` should be set to `false`, etc.
|
||||
- Loading the map should be delayed using `useDelayRender()` and the map should be set to `null` until it is loaded.
|
||||
- The element containing the ref MUST have an explicit width and height and `position: "absolute"`.
|
||||
- Do not add a `_map.remove();` cleanup function.
|
||||
|
||||
## Drawing lines
|
||||
|
||||
Unless I request it, do not add a glow effect to the lines.
|
||||
Unless I request it, do not add additional points to the lines.
|
||||
|
||||
## Map style
|
||||
|
||||
By default, use the `mapbox://styles/mapbox/standard` style.
|
||||
Hide the labels from the base map style.
|
||||
|
||||
Unless I request otherwise, remove all features from the Mapbox Standard style.
|
||||
|
||||
```tsx
|
||||
// Hide all features from the Mapbox Standard style
|
||||
const hideFeatures = [
|
||||
'showRoadsAndTransit',
|
||||
'showRoads',
|
||||
'showTransit',
|
||||
'showPedestrianRoads',
|
||||
'showRoadLabels',
|
||||
'showTransitLabels',
|
||||
'showPlaceLabels',
|
||||
'showPointOfInterestLabels',
|
||||
'showPointsOfInterest',
|
||||
'showAdminBoundaries',
|
||||
'showLandmarkIcons',
|
||||
'showLandmarkIconLabels',
|
||||
'show3dObjects',
|
||||
'show3dBuildings',
|
||||
'show3dTrees',
|
||||
'show3dLandmarks',
|
||||
'show3dFacades',
|
||||
];
|
||||
for (const feature of hideFeatures) {
|
||||
_map.setConfigProperty('basemap', feature, false);
|
||||
}
|
||||
|
||||
_map.setConfigProperty('basemap', 'colorMotorways', 'transparent');
|
||||
_map.setConfigProperty('basemap', 'colorRoads', 'transparent');
|
||||
_map.setConfigProperty('basemap', 'colorTrunks', 'transparent');
|
||||
```
|
||||
|
||||
## Animating the camera
|
||||
|
||||
You can animate the camera along the line by adding a `useEffect` hook that updates the camera position based on the current frame.
|
||||
|
||||
Unless I ask for it, do not jump between camera angles.
|
||||
|
||||
```tsx
|
||||
import * as turf from '@turf/turf';
|
||||
import {interpolate} from 'remotion';
|
||||
import {Easing} from 'remotion';
|
||||
import {useCurrentFrame, useVideoConfig, useDelayRender} from 'remotion';
|
||||
|
||||
const animationDuration = 20;
|
||||
const cameraAltitude = 4000;
|
||||
```
|
||||
|
||||
```tsx
|
||||
const frame = useCurrentFrame();
|
||||
const {fps} = useVideoConfig();
|
||||
const {delayRender, continueRender} = useDelayRender();
|
||||
|
||||
useEffect(() => {
|
||||
if (!map) {
|
||||
return;
|
||||
}
|
||||
const handle = delayRender('Moving point...');
|
||||
|
||||
const routeDistance = turf.length(turf.lineString(lineCoordinates));
|
||||
|
||||
const progress = interpolate(frame / fps, [0.00001, animationDuration], [0, 1], {
|
||||
easing: Easing.inOut(Easing.sin),
|
||||
extrapolateLeft: 'clamp',
|
||||
extrapolateRight: 'clamp',
|
||||
});
|
||||
|
||||
const camera = map.getFreeCameraOptions();
|
||||
|
||||
const alongRoute = turf.along(turf.lineString(lineCoordinates), routeDistance * progress).geometry.coordinates;
|
||||
|
||||
camera.lookAtPoint({
|
||||
lng: alongRoute[0],
|
||||
lat: alongRoute[1],
|
||||
});
|
||||
|
||||
map.setFreeCameraOptions(camera);
|
||||
map.once('idle', () => continueRender(handle));
|
||||
}, [lineCoordinates, fps, frame, handle, map]);
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
||||
IMPORTANT: Keep the camera by default so north is up.
|
||||
IMPORTANT: For multi-step animations, set all properties at all stages (zoom, position, line progress) to prevent jumps. Override initial values.
|
||||
|
||||
- The progress is clamped to a minimum value to avoid the line being empty, which can lead to turf errors
|
||||
- See [Timing](./timing.md) for more options for timing.
|
||||
- Consider the dimensions of the composition and make the lines thick enough and the label font size large enough to be legible for when the composition is scaled down.
|
||||
|
||||
## Animating lines
|
||||
|
||||
### Straight lines (linear interpolation)
|
||||
|
||||
To animate a line that appears straight on the map, use linear interpolation between coordinates. Do NOT use turf's `lineSliceAlong` or `along` functions, as they use geodesic (great circle) calculations which appear curved on a Mercator projection.
|
||||
|
||||
```tsx
|
||||
const frame = useCurrentFrame();
|
||||
const {durationInFrames} = useVideoConfig();
|
||||
|
||||
useEffect(() => {
|
||||
if (!map) return;
|
||||
|
||||
const animationHandle = delayRender('Animating line...');
|
||||
|
||||
const progress = interpolate(frame, [0, durationInFrames - 1], [0, 1], {
|
||||
extrapolateLeft: 'clamp',
|
||||
extrapolateRight: 'clamp',
|
||||
easing: Easing.inOut(Easing.cubic),
|
||||
});
|
||||
|
||||
// Linear interpolation for a straight line on the map
|
||||
const start = lineCoordinates[0];
|
||||
const end = lineCoordinates[1];
|
||||
const currentLng = start[0] + (end[0] - start[0]) * progress;
|
||||
const currentLat = start[1] + (end[1] - start[1]) * progress;
|
||||
|
||||
const lineData: GeoJSON.Feature<GeoJSON.LineString> = {
|
||||
type: 'Feature',
|
||||
properties: {},
|
||||
geometry: {
|
||||
type: 'LineString',
|
||||
coordinates: [start, [currentLng, currentLat]],
|
||||
},
|
||||
};
|
||||
|
||||
const source = map.getSource('trace') as mapboxgl.GeoJSONSource;
|
||||
if (source) {
|
||||
source.setData(lineData);
|
||||
}
|
||||
|
||||
map.once('idle', () => continueRender(animationHandle));
|
||||
}, [frame, map, durationInFrames]);
|
||||
```
|
||||
|
||||
### Curved lines (geodesic/great circle)
|
||||
|
||||
To animate a line that follows the geodesic (great circle) path between two points, use turf's `lineSliceAlong`. This is useful for showing flight paths or the actual shortest distance on Earth.
|
||||
|
||||
```tsx
|
||||
import * as turf from '@turf/turf';
|
||||
|
||||
const routeLine = turf.lineString(lineCoordinates);
|
||||
const routeDistance = turf.length(routeLine);
|
||||
|
||||
const currentDistance = Math.max(0.001, routeDistance * progress);
|
||||
const slicedLine = turf.lineSliceAlong(routeLine, 0, currentDistance);
|
||||
|
||||
const source = map.getSource('route') as mapboxgl.GeoJSONSource;
|
||||
if (source) {
|
||||
source.setData(slicedLine);
|
||||
}
|
||||
```
|
||||
|
||||
## Markers
|
||||
|
||||
Add labels, and markers where appropriate.
|
||||
|
||||
```tsx
|
||||
_map.addSource('markers', {
|
||||
type: 'geojson',
|
||||
data: {
|
||||
type: 'FeatureCollection',
|
||||
features: [
|
||||
{
|
||||
type: 'Feature',
|
||||
properties: {name: 'Point 1'},
|
||||
geometry: {type: 'Point', coordinates: [-118.2437, 34.0522]},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
_map.addLayer({
|
||||
id: 'city-markers',
|
||||
type: 'circle',
|
||||
source: 'markers',
|
||||
paint: {
|
||||
'circle-radius': 40,
|
||||
'circle-color': '#FF4444',
|
||||
'circle-stroke-width': 4,
|
||||
'circle-stroke-color': '#FFFFFF',
|
||||
},
|
||||
});
|
||||
|
||||
_map.addLayer({
|
||||
id: 'labels',
|
||||
type: 'symbol',
|
||||
source: 'markers',
|
||||
layout: {
|
||||
'text-field': ['get', 'name'],
|
||||
'text-font': ['DIN Pro Bold', 'Arial Unicode MS Bold'],
|
||||
'text-size': 50,
|
||||
'text-offset': [0, 0.5],
|
||||
'text-anchor': 'top',
|
||||
},
|
||||
paint: {
|
||||
'text-color': '#FFFFFF',
|
||||
'text-halo-color': '#000000',
|
||||
'text-halo-width': 2,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Make sure they are big enough. Check the composition dimensions and scale the labels accordingly.
|
||||
For a composition size of 1920x1080, the label font size should be at least 40px.
|
||||
|
||||
IMPORTANT: Keep the `text-offset` small enough so it is close to the marker. Consider the marker circle radius. For a circle radius of 40, this is a good offset:
|
||||
|
||||
```tsx
|
||||
"text-offset": [0, 0.5],
|
||||
```
|
||||
|
||||
## 3D buildings
|
||||
|
||||
To enable 3D buildings, use the following code:
|
||||
|
||||
```tsx
|
||||
_map.setConfigProperty('basemap', 'show3dObjects', true);
|
||||
_map.setConfigProperty('basemap', 'show3dLandmarks', true);
|
||||
_map.setConfigProperty('basemap', 'show3dBuildings', true);
|
||||
```
|
||||
|
||||
## Rendering
|
||||
|
||||
When rendering a map animation, make sure to render with the following flags:
|
||||
|
||||
```
|
||||
npx remotion render --gl=angle --concurrency=1
|
||||
```
|
||||
35
marketing/skills/video/rules/measuring-dom-nodes.md
Normal file
35
marketing/skills/video/rules/measuring-dom-nodes.md
Normal file
@@ -0,0 +1,35 @@
|
||||
---
|
||||
name: measuring-dom-nodes
|
||||
description: Measuring DOM element dimensions in Remotion
|
||||
metadata:
|
||||
tags: measure, layout, dimensions, getBoundingClientRect, scale
|
||||
---
|
||||
|
||||
# Measuring DOM nodes in Remotion
|
||||
|
||||
Remotion applies a `scale()` transform to the video container, which affects values from `getBoundingClientRect()`. Use `useCurrentScale()` to get correct measurements.
|
||||
|
||||
## Measuring element dimensions
|
||||
|
||||
```tsx
|
||||
import { useCurrentScale } from "remotion";
|
||||
import { useRef, useEffect, useState } from "react";
|
||||
|
||||
export const MyComponent = () => {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const scale = useCurrentScale();
|
||||
const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
|
||||
|
||||
useEffect(() => {
|
||||
if (!ref.current) return;
|
||||
const rect = ref.current.getBoundingClientRect();
|
||||
setDimensions({
|
||||
width: rect.width / scale,
|
||||
height: rect.height / scale,
|
||||
});
|
||||
}, [scale]);
|
||||
|
||||
return <div ref={ref}>Content to measure</div>;
|
||||
};
|
||||
```
|
||||
|
||||
143
marketing/skills/video/rules/measuring-text.md
Normal file
143
marketing/skills/video/rules/measuring-text.md
Normal file
@@ -0,0 +1,143 @@
|
||||
---
|
||||
name: measuring-text
|
||||
description: Measuring text dimensions, fitting text to containers, and checking overflow
|
||||
metadata:
|
||||
tags: measure, text, layout, dimensions, fitText, fillTextBox
|
||||
---
|
||||
|
||||
# Measuring text in Remotion
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Install @remotion/layout-utils if it is not already installed:
|
||||
|
||||
```bash
|
||||
npx remotion add @remotion/layout-utils # If project uses npm
|
||||
bunx remotion add @remotion/layout-utils # If project uses bun
|
||||
yarn remotion add @remotion/layout-utils # If project uses yarn
|
||||
pnpm exec remotion add @remotion/layout-utils # If project uses pnpm
|
||||
```
|
||||
|
||||
## Measuring text dimensions
|
||||
|
||||
Use `measureText()` to calculate the width and height of text:
|
||||
|
||||
```tsx
|
||||
import { measureText } from "@remotion/layout-utils";
|
||||
|
||||
const { width, height } = measureText({
|
||||
text: "Hello World",
|
||||
fontFamily: "Arial",
|
||||
fontSize: 32,
|
||||
fontWeight: "bold",
|
||||
});
|
||||
```
|
||||
|
||||
Results are cached - duplicate calls return the cached result.
|
||||
|
||||
## Fitting text to a width
|
||||
|
||||
Use `fitText()` to find the optimal font size for a container:
|
||||
|
||||
```tsx
|
||||
import { fitText } from "@remotion/layout-utils";
|
||||
|
||||
const { fontSize } = fitText({
|
||||
text: "Hello World",
|
||||
withinWidth: 600,
|
||||
fontFamily: "Inter",
|
||||
fontWeight: "bold",
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
fontSize: Math.min(fontSize, 80), // Cap at 80px
|
||||
fontFamily: "Inter",
|
||||
fontWeight: "bold",
|
||||
}}
|
||||
>
|
||||
Hello World
|
||||
</div>
|
||||
);
|
||||
```
|
||||
|
||||
## Checking text overflow
|
||||
|
||||
Use `fillTextBox()` to check if text exceeds a box:
|
||||
|
||||
```tsx
|
||||
import { fillTextBox } from "@remotion/layout-utils";
|
||||
|
||||
const box = fillTextBox({ maxBoxWidth: 400, maxLines: 3 });
|
||||
|
||||
const words = ["Hello", "World", "This", "is", "a", "test"];
|
||||
for (const word of words) {
|
||||
const { exceedsBox } = box.add({
|
||||
text: word + " ",
|
||||
fontFamily: "Arial",
|
||||
fontSize: 24,
|
||||
});
|
||||
if (exceedsBox) {
|
||||
// Text would overflow, handle accordingly
|
||||
break;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Best practices
|
||||
|
||||
**Load fonts first:** Only call measurement functions after fonts are loaded.
|
||||
|
||||
```tsx
|
||||
import { loadFont } from "@remotion/google-fonts/Inter";
|
||||
|
||||
const { fontFamily, waitUntilDone } = loadFont("normal", {
|
||||
weights: ["400"],
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
waitUntilDone().then(() => {
|
||||
// Now safe to measure
|
||||
const { width } = measureText({
|
||||
text: "Hello",
|
||||
fontFamily,
|
||||
fontSize: 32,
|
||||
});
|
||||
})
|
||||
```
|
||||
|
||||
**Use validateFontIsLoaded:** Catch font loading issues early:
|
||||
|
||||
```tsx
|
||||
measureText({
|
||||
text: "Hello",
|
||||
fontFamily: "MyCustomFont",
|
||||
fontSize: 32,
|
||||
validateFontIsLoaded: true, // Throws if font not loaded
|
||||
});
|
||||
```
|
||||
|
||||
**Match font properties:** Use the same properties for measurement and rendering:
|
||||
|
||||
```tsx
|
||||
const fontStyle = {
|
||||
fontFamily: "Inter",
|
||||
fontSize: 32,
|
||||
fontWeight: "bold" as const,
|
||||
letterSpacing: "0.5px",
|
||||
};
|
||||
|
||||
const { width } = measureText({
|
||||
text: "Hello",
|
||||
...fontStyle,
|
||||
});
|
||||
|
||||
return <div style={fontStyle}>Hello</div>;
|
||||
```
|
||||
|
||||
**Avoid padding and border:** Use `outline` instead of `border` to prevent layout differences:
|
||||
|
||||
```tsx
|
||||
<div style={{ outline: "2px solid red" }}>Text</div>
|
||||
```
|
||||
98
marketing/skills/video/rules/parameters.md
Normal file
98
marketing/skills/video/rules/parameters.md
Normal file
@@ -0,0 +1,98 @@
|
||||
---
|
||||
name: parameters
|
||||
description: Make a video parametrizable by adding a Zod schema
|
||||
metadata:
|
||||
tags: parameters, zod, schema
|
||||
---
|
||||
|
||||
To make a video parametrizable, a Zod schema can be added to a composition.
|
||||
|
||||
First, `zod` must be installed - it must be exactly version `3.22.3`.
|
||||
|
||||
Search the project for lockfiles and run the correct command depending on the package manager:
|
||||
|
||||
If `package-lock.json` is found, use the following command:
|
||||
|
||||
```bash
|
||||
npm i zod@3.22.3
|
||||
```
|
||||
|
||||
If `bun.lockb` is found, use the following command:
|
||||
|
||||
```bash
|
||||
bun i zod@3.22.3
|
||||
```
|
||||
|
||||
If `yarn.lock` is found, use the following command:
|
||||
|
||||
```bash
|
||||
yarn add zod@3.22.3
|
||||
```
|
||||
|
||||
If `pnpm-lock.yaml` is found, use the following command:
|
||||
|
||||
```bash
|
||||
pnpm i zod@3.22.3
|
||||
```
|
||||
|
||||
Then, a Zod schema can be defined alongside the component:
|
||||
|
||||
```tsx title="src/MyComposition.tsx"
|
||||
import {z} from 'zod';
|
||||
|
||||
export const MyCompositionSchema = z.object({
|
||||
title: z.string(),
|
||||
});
|
||||
|
||||
const MyComponent: React.FC<z.infer<typeof MyCompositionSchema>> = () => {
|
||||
return (
|
||||
<div>
|
||||
<h1>{props.title}</h1>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
In the root file, the schema can be passed to the composition:
|
||||
|
||||
```tsx title="src/Root.tsx"
|
||||
import {Composition} from 'remotion';
|
||||
import {MycComponent, MyCompositionSchema} from './MyComposition';
|
||||
|
||||
export const RemotionRoot = () => {
|
||||
return <Composition id="MyComposition" component={MyComponent} durationInFrames={100} fps={30} width={1080} height={1080} defaultProps={{title: 'Hello World'}} schema={MyCompositionSchema} />;
|
||||
};
|
||||
```
|
||||
|
||||
Now, the user can edit the parameter visually in the sidebar.
|
||||
|
||||
All schemas that are supported by Zod are supported by Remotion.
|
||||
|
||||
Remotion requires that the top-level type is a z.object(), because the collection of props of a React component is always an object.
|
||||
|
||||
## Color picker
|
||||
|
||||
For adding a color picker, use `zColor()` from `@remotion/zod-types`.
|
||||
|
||||
If it is not installed, use the following command:
|
||||
|
||||
```bash
|
||||
npx remotion add @remotion/zod-types # If project uses npm
|
||||
bunx remotion add @remotion/zod-types # If project uses bun
|
||||
yarn remotion add @remotion/zod-types # If project uses yarn
|
||||
pnpm exec remotion add @remotion/zod-types # If project uses pnpm
|
||||
```
|
||||
|
||||
Then import `zColor` from `@remotion/zod-types`:
|
||||
|
||||
```tsx
|
||||
import {zColor} from '@remotion/zod-types';
|
||||
```
|
||||
|
||||
Then use it in the schema:
|
||||
|
||||
```tsx
|
||||
export const MyCompositionSchema = z.object({
|
||||
color: zColor(),
|
||||
});
|
||||
```
|
||||
118
marketing/skills/video/rules/sequencing.md
Normal file
118
marketing/skills/video/rules/sequencing.md
Normal file
@@ -0,0 +1,118 @@
|
||||
---
|
||||
name: sequencing
|
||||
description: Sequencing patterns for Remotion - delay, trim, limit duration of items
|
||||
metadata:
|
||||
tags: sequence, series, timing, delay, trim
|
||||
---
|
||||
|
||||
Use `<Sequence>` to delay when an element appears in the timeline.
|
||||
|
||||
```tsx
|
||||
import { Sequence } from "remotion";
|
||||
|
||||
const {fps} = useVideoConfig();
|
||||
|
||||
<Sequence from={1 * fps} durationInFrames={2 * fps} premountFor={1 * fps}>
|
||||
<Title />
|
||||
</Sequence>
|
||||
<Sequence from={2 * fps} durationInFrames={2 * fps} premountFor={1 * fps}>
|
||||
<Subtitle />
|
||||
</Sequence>
|
||||
```
|
||||
|
||||
This will by default wrap the component in an absolute fill element.
|
||||
If the items should not be wrapped, use the `layout` prop:
|
||||
|
||||
```tsx
|
||||
<Sequence layout="none">
|
||||
<Title />
|
||||
</Sequence>
|
||||
```
|
||||
|
||||
## Premounting
|
||||
|
||||
This loads the component in the timeline before it is actually played.
|
||||
Always premount any `<Sequence>`!
|
||||
|
||||
```tsx
|
||||
<Sequence premountFor={1 * fps}>
|
||||
<Title />
|
||||
</Sequence>
|
||||
```
|
||||
|
||||
## Series
|
||||
|
||||
Use `<Series>` when elements should play one after another without overlap.
|
||||
|
||||
```tsx
|
||||
import {Series} from 'remotion';
|
||||
|
||||
<Series>
|
||||
<Series.Sequence durationInFrames={45}>
|
||||
<Intro />
|
||||
</Series.Sequence>
|
||||
<Series.Sequence durationInFrames={60}>
|
||||
<MainContent />
|
||||
</Series.Sequence>
|
||||
<Series.Sequence durationInFrames={30}>
|
||||
<Outro />
|
||||
</Series.Sequence>
|
||||
</Series>;
|
||||
```
|
||||
|
||||
Same as with `<Sequence>`, the items will be wrapped in an absolute fill element by default when using `<Series.Sequence>`, unless the `layout` prop is set to `none`.
|
||||
|
||||
### Series with overlaps
|
||||
|
||||
Use negative offset for overlapping sequences:
|
||||
|
||||
```tsx
|
||||
<Series>
|
||||
<Series.Sequence durationInFrames={60}>
|
||||
<SceneA />
|
||||
</Series.Sequence>
|
||||
<Series.Sequence offset={-15} durationInFrames={60}>
|
||||
{/* Starts 15 frames before SceneA ends */}
|
||||
<SceneB />
|
||||
</Series.Sequence>
|
||||
</Series>
|
||||
```
|
||||
|
||||
## Frame References Inside Sequences
|
||||
|
||||
Inside a Sequence, `useCurrentFrame()` returns the local frame (starting from 0):
|
||||
|
||||
```tsx
|
||||
<Sequence from={60} durationInFrames={30}>
|
||||
<MyComponent />
|
||||
{/* Inside MyComponent, useCurrentFrame() returns 0-29, not 60-89 */}
|
||||
</Sequence>
|
||||
```
|
||||
|
||||
## Nested Sequences
|
||||
|
||||
Sequences can be nested for complex timing:
|
||||
|
||||
```tsx
|
||||
<Sequence from={0} durationInFrames={120}>
|
||||
<Background />
|
||||
<Sequence from={15} durationInFrames={90} layout="none">
|
||||
<Title />
|
||||
</Sequence>
|
||||
<Sequence from={45} durationInFrames={60} layout="none">
|
||||
<Subtitle />
|
||||
</Sequence>
|
||||
</Sequence>
|
||||
```
|
||||
|
||||
## 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>
|
||||
```
|
||||
11
marketing/skills/video/rules/tailwind.md
Normal file
11
marketing/skills/video/rules/tailwind.md
Normal file
@@ -0,0 +1,11 @@
|
||||
---
|
||||
name: tailwind
|
||||
description: Using TailwindCSS in Remotion.
|
||||
metadata:
|
||||
---
|
||||
|
||||
You can and should use TailwindCSS in Remotion, if TailwindCSS is installed in the project.
|
||||
|
||||
Don't use `transition-*` or `animate-*` classes - always animate using the `useCurrentFrame()` hook.
|
||||
|
||||
Tailwind must be installed and enabled first in a Remotion project - fetch https://www.remotion.dev/docs/tailwind using WebFetch for instructions.
|
||||
20
marketing/skills/video/rules/text-animations.md
Normal file
20
marketing/skills/video/rules/text-animations.md
Normal file
@@ -0,0 +1,20 @@
|
||||
---
|
||||
name: text-animations
|
||||
description: Typography and text animation patterns for Remotion.
|
||||
metadata:
|
||||
tags: typography, text, typewriter, highlighter ken
|
||||
---
|
||||
|
||||
## Text animations
|
||||
|
||||
Based on `useCurrentFrame()`, reduce the string character by character to create a typewriter effect.
|
||||
|
||||
## Typewriter Effect
|
||||
|
||||
See [Typewriter](assets/text-animations-typewriter.tsx) for an advanced example with a blinking cursor and a pause after the first sentence.
|
||||
|
||||
Always use string slicing for typewriter effects. Never use per-character opacity.
|
||||
|
||||
## Word Highlighting
|
||||
|
||||
See [Word Highlight](assets/text-animations-word-highlight.tsx) for an example for how a word highlight is animated, like with a highlighter pen.
|
||||
179
marketing/skills/video/rules/timing.md
Normal file
179
marketing/skills/video/rules/timing.md
Normal file
@@ -0,0 +1,179 @@
|
||||
---
|
||||
name: timing
|
||||
description: Interpolation curves in Remotion - linear, easing, spring animations
|
||||
metadata:
|
||||
tags: spring, bounce, easing, interpolation
|
||||
---
|
||||
|
||||
A simple linear interpolation is done using the `interpolate` function.
|
||||
|
||||
```ts title="Going from 0 to 1 over 100 frames"
|
||||
import {interpolate} from 'remotion';
|
||||
|
||||
const opacity = interpolate(frame, [0, 100], [0, 1]);
|
||||
```
|
||||
|
||||
By default, the values are not clamped, so the value can go outside the range [0, 1].
|
||||
Here is how they can be clamped:
|
||||
|
||||
```ts title="Going from 0 to 1 over 100 frames with extrapolation"
|
||||
const opacity = interpolate(frame, [0, 100], [0, 1], {
|
||||
extrapolateRight: 'clamp',
|
||||
extrapolateLeft: 'clamp',
|
||||
});
|
||||
```
|
||||
|
||||
## Spring animations
|
||||
|
||||
Spring animations have a more natural motion.
|
||||
They go from 0 to 1 over time.
|
||||
|
||||
```ts title="Spring animation from 0 to 1 over 100 frames"
|
||||
import {spring, useCurrentFrame, useVideoConfig} from 'remotion';
|
||||
|
||||
const frame = useCurrentFrame();
|
||||
const {fps} = useVideoConfig();
|
||||
|
||||
const scale = spring({
|
||||
frame,
|
||||
fps,
|
||||
});
|
||||
```
|
||||
|
||||
### Physical properties
|
||||
|
||||
The default configuration is: `mass: 1, damping: 10, stiffness: 100`.
|
||||
This leads to the animation having a bit of bounce before it settles.
|
||||
|
||||
The config can be overwritten like this:
|
||||
|
||||
```ts
|
||||
const scale = spring({
|
||||
frame,
|
||||
fps,
|
||||
config: {damping: 200},
|
||||
});
|
||||
```
|
||||
|
||||
The recommended configuration for a natural motion without a bounce is: `{ damping: 200 }`.
|
||||
|
||||
Here are some common configurations:
|
||||
|
||||
```tsx
|
||||
const smooth = {damping: 200}; // Smooth, no bounce (subtle reveals)
|
||||
const snappy = {damping: 20, stiffness: 200}; // Snappy, minimal bounce (UI elements)
|
||||
const bouncy = {damping: 8}; // Bouncy entrance (playful animations)
|
||||
const heavy = {damping: 15, stiffness: 80, mass: 2}; // Heavy, slow, small bounce
|
||||
```
|
||||
|
||||
### Delay
|
||||
|
||||
The animation starts immediately by default.
|
||||
Use the `delay` parameter to delay the animation by a number of frames.
|
||||
|
||||
```tsx
|
||||
const entrance = spring({
|
||||
frame: frame - ENTRANCE_DELAY,
|
||||
fps,
|
||||
delay: 20,
|
||||
});
|
||||
```
|
||||
|
||||
### Duration
|
||||
|
||||
A `spring()` has a natural duration based on the physical properties.
|
||||
To stretch the animation to a specific duration, use the `durationInFrames` parameter.
|
||||
|
||||
```tsx
|
||||
const spring = spring({
|
||||
frame,
|
||||
fps,
|
||||
durationInFrames: 40,
|
||||
});
|
||||
```
|
||||
|
||||
### Combining spring() with interpolate()
|
||||
|
||||
Map spring output (0-1) to custom ranges:
|
||||
|
||||
```tsx
|
||||
const springProgress = spring({
|
||||
frame,
|
||||
fps,
|
||||
});
|
||||
|
||||
// Map to rotation
|
||||
const rotation = interpolate(springProgress, [0, 1], [0, 360]);
|
||||
|
||||
<div style={{rotate: rotation + 'deg'}} />;
|
||||
```
|
||||
|
||||
### Adding springs
|
||||
|
||||
Springs return just numbers, so math can be performed:
|
||||
|
||||
```tsx
|
||||
const frame = useCurrentFrame();
|
||||
const {fps, durationInFrames} = useVideoConfig();
|
||||
|
||||
const inAnimation = spring({
|
||||
frame,
|
||||
fps,
|
||||
});
|
||||
const outAnimation = spring({
|
||||
frame,
|
||||
fps,
|
||||
durationInFrames: 1 * fps,
|
||||
delay: durationInFrames - 1 * fps,
|
||||
});
|
||||
|
||||
const scale = inAnimation - outAnimation;
|
||||
```
|
||||
|
||||
## Easing
|
||||
|
||||
Easing can be added to the `interpolate` function:
|
||||
|
||||
```ts
|
||||
import {interpolate, Easing} from 'remotion';
|
||||
|
||||
const value1 = interpolate(frame, [0, 100], [0, 1], {
|
||||
easing: Easing.inOut(Easing.quad),
|
||||
extrapolateLeft: 'clamp',
|
||||
extrapolateRight: 'clamp',
|
||||
});
|
||||
```
|
||||
|
||||
The default easing is `Easing.linear`.
|
||||
There are various other convexities:
|
||||
|
||||
- `Easing.in` for starting slow and accelerating
|
||||
- `Easing.out` for starting fast and slowing down
|
||||
- `Easing.inOut`
|
||||
|
||||
and curves (sorted from most linear to most curved):
|
||||
|
||||
- `Easing.quad`
|
||||
- `Easing.sin`
|
||||
- `Easing.exp`
|
||||
- `Easing.circle`
|
||||
|
||||
Convexities and curves need be combined for an easing function:
|
||||
|
||||
```ts
|
||||
const value1 = interpolate(frame, [0, 100], [0, 1], {
|
||||
easing: Easing.inOut(Easing.quad),
|
||||
extrapolateLeft: 'clamp',
|
||||
extrapolateRight: 'clamp',
|
||||
});
|
||||
```
|
||||
|
||||
Cubic bezier curves are also supported:
|
||||
|
||||
```ts
|
||||
const value1 = interpolate(frame, [0, 100], [0, 1], {
|
||||
easing: Easing.bezier(0.8, 0.22, 0.96, 0.65),
|
||||
extrapolateLeft: 'clamp',
|
||||
extrapolateRight: 'clamp',
|
||||
});
|
||||
```
|
||||
19
marketing/skills/video/rules/transcribe-captions.md
Normal file
19
marketing/skills/video/rules/transcribe-captions.md
Normal file
@@ -0,0 +1,19 @@
|
||||
---
|
||||
name: transcribe-captions
|
||||
description: Transcribing audio to generate captions in Remotion
|
||||
metadata:
|
||||
tags: captions, transcribe, whisper, audio, speech-to-text
|
||||
---
|
||||
|
||||
# Transcribing audio
|
||||
|
||||
Remotion provides several built-in options for transcribing audio to generate captions:
|
||||
|
||||
- `@remotion/install-whisper-cpp` - Transcribe locally on a server using Whisper.cpp. Fast and free, but requires server infrastructure.
|
||||
https://remotion.dev/docs/install-whisper-cpp
|
||||
|
||||
- `@remotion/whisper-web` - Transcribe in the browser using WebAssembly. No server needed and free, but slower due to WASM overhead.
|
||||
https://remotion.dev/docs/whisper-web
|
||||
|
||||
- `@remotion/openai-whisper` - Use OpenAI Whisper API for cloud-based transcription. Fast and no server needed, but requires payment.
|
||||
https://remotion.dev/docs/openai-whisper/openai-whisper-api-to-captions
|
||||
122
marketing/skills/video/rules/transitions.md
Normal file
122
marketing/skills/video/rules/transitions.md
Normal file
@@ -0,0 +1,122 @@
|
||||
---
|
||||
name: transitions
|
||||
description: Fullscreen scene transitions for Remotion.
|
||||
metadata:
|
||||
tags: transitions, fade, slide, wipe, scenes
|
||||
---
|
||||
|
||||
## Fullscreen transitions
|
||||
|
||||
Using `<TransitionSeries>` to animate between multiple scenes or clips.
|
||||
This will absolutely position the children.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
First, the @remotion/transitions package needs to be installed.
|
||||
If it is not, use the following command:
|
||||
|
||||
```bash
|
||||
npx remotion add @remotion/transitions # If project uses npm
|
||||
bunx remotion add @remotion/transitions # If project uses bun
|
||||
yarn remotion add @remotion/transitions # If project uses yarn
|
||||
pnpm exec remotion add @remotion/transitions # If project uses pnpm
|
||||
```
|
||||
|
||||
## Example usage
|
||||
|
||||
```tsx
|
||||
import {TransitionSeries, linearTiming} from '@remotion/transitions';
|
||||
import {fade} from '@remotion/transitions/fade';
|
||||
|
||||
<TransitionSeries>
|
||||
<TransitionSeries.Sequence durationInFrames={60}>
|
||||
<SceneA />
|
||||
</TransitionSeries.Sequence>
|
||||
<TransitionSeries.Transition presentation={fade()} timing={linearTiming({durationInFrames: 15})} />
|
||||
<TransitionSeries.Sequence durationInFrames={60}>
|
||||
<SceneB />
|
||||
</TransitionSeries.Sequence>
|
||||
</TransitionSeries>;
|
||||
```
|
||||
|
||||
## Available Transition Types
|
||||
|
||||
Import transitions from their respective modules:
|
||||
|
||||
```tsx
|
||||
import {fade} from '@remotion/transitions/fade';
|
||||
import {slide} from '@remotion/transitions/slide';
|
||||
import {wipe} from '@remotion/transitions/wipe';
|
||||
import {flip} from '@remotion/transitions/flip';
|
||||
import {clockWipe} from '@remotion/transitions/clock-wipe';
|
||||
```
|
||||
|
||||
## Slide Transition with Direction
|
||||
|
||||
Specify slide direction for enter/exit animations.
|
||||
|
||||
```tsx
|
||||
import {slide} from '@remotion/transitions/slide';
|
||||
|
||||
<TransitionSeries.Transition presentation={slide({direction: 'from-left'})} timing={linearTiming({durationInFrames: 20})} />;
|
||||
```
|
||||
|
||||
Directions: `"from-left"`, `"from-right"`, `"from-top"`, `"from-bottom"`
|
||||
|
||||
## Timing Options
|
||||
|
||||
```tsx
|
||||
import {linearTiming, springTiming} from '@remotion/transitions';
|
||||
|
||||
// Linear timing - constant speed
|
||||
linearTiming({durationInFrames: 20});
|
||||
|
||||
// Spring timing - organic motion
|
||||
springTiming({config: {damping: 200}, durationInFrames: 25});
|
||||
```
|
||||
|
||||
## Duration calculation
|
||||
|
||||
Transitions overlap adjacent scenes, so the total composition length is **shorter** than the sum of all sequence durations.
|
||||
|
||||
For example, with two 60-frame sequences and a 15-frame transition:
|
||||
|
||||
- Without transitions: `60 + 60 = 120` frames
|
||||
- With transition: `60 + 60 - 15 = 105` frames
|
||||
|
||||
The transition duration is subtracted because both scenes play simultaneously during the transition.
|
||||
|
||||
### Getting the duration of a transition
|
||||
|
||||
Use the `getDurationInFrames()` method on the timing object:
|
||||
|
||||
```tsx
|
||||
import {linearTiming, springTiming} from '@remotion/transitions';
|
||||
|
||||
const linearDuration = linearTiming({durationInFrames: 20}).getDurationInFrames({fps: 30});
|
||||
// Returns 20
|
||||
|
||||
const springDuration = springTiming({config: {damping: 200}}).getDurationInFrames({fps: 30});
|
||||
// Returns calculated duration based on spring physics
|
||||
```
|
||||
|
||||
For `springTiming` without an explicit `durationInFrames`, the duration depends on `fps` because it calculates when the spring animation settles.
|
||||
|
||||
### Calculating total composition duration
|
||||
|
||||
```tsx
|
||||
import {linearTiming} from '@remotion/transitions';
|
||||
|
||||
const scene1Duration = 60;
|
||||
const scene2Duration = 60;
|
||||
const scene3Duration = 60;
|
||||
|
||||
const timing1 = linearTiming({durationInFrames: 15});
|
||||
const timing2 = linearTiming({durationInFrames: 20});
|
||||
|
||||
const transition1Duration = timing1.getDurationInFrames({fps: 30});
|
||||
const transition2Duration = timing2.getDurationInFrames({fps: 30});
|
||||
|
||||
const totalDuration = scene1Duration + scene2Duration + scene3Duration - transition1Duration - transition2Duration;
|
||||
// 60 + 60 + 60 - 15 - 20 = 145 frames
|
||||
```
|
||||
53
marketing/skills/video/rules/trimming.md
Normal file
53
marketing/skills/video/rules/trimming.md
Normal file
@@ -0,0 +1,53 @@
|
||||
---
|
||||
name: trimming
|
||||
description: Trimming patterns for Remotion - cut the beginning or end of animations
|
||||
metadata:
|
||||
tags: sequence, trim, clip, cut, offset
|
||||
---
|
||||
|
||||
Use `<Sequence>` with a negative `from` value to trim the start of an animation.
|
||||
|
||||
## Trim the Beginning
|
||||
|
||||
A negative `from` value shifts time backwards, making the animation start partway through:
|
||||
|
||||
```tsx
|
||||
import { Sequence, useVideoConfig } from "remotion";
|
||||
|
||||
const fps = useVideoConfig();
|
||||
|
||||
<Sequence from={-0.5 * fps}>
|
||||
<MyAnimation />
|
||||
</Sequence>
|
||||
```
|
||||
|
||||
The animation appears 15 frames into its progress - the first 15 frames are trimmed off.
|
||||
Inside `<MyAnimation>`, `useCurrentFrame()` starts at 15 instead of 0.
|
||||
|
||||
## Trim the End
|
||||
|
||||
Use `durationInFrames` to unmount content after a specified duration:
|
||||
|
||||
```tsx
|
||||
|
||||
<Sequence durationInFrames={1.5 * fps}>
|
||||
<MyAnimation />
|
||||
</Sequence>
|
||||
```
|
||||
|
||||
The animation plays for 45 frames, then the component unmounts.
|
||||
|
||||
## Trim and Delay
|
||||
|
||||
Nest sequences to both trim the beginning and delay when it appears:
|
||||
|
||||
```tsx
|
||||
<Sequence from={30}>
|
||||
<Sequence from={-15}>
|
||||
<MyAnimation />
|
||||
</Sequence>
|
||||
</Sequence>
|
||||
```
|
||||
|
||||
The inner sequence trims 15 frames from the start, and the outer sequence delays the result by 30 frames.
|
||||
|
||||
171
marketing/skills/video/rules/videos.md
Normal file
171
marketing/skills/video/rules/videos.md
Normal file
@@ -0,0 +1,171 @@
|
||||
---
|
||||
name: videos
|
||||
description: Embedding videos in Remotion - trimming, volume, speed, looping, pitch
|
||||
metadata:
|
||||
tags: video, media, trim, volume, speed, loop, pitch
|
||||
---
|
||||
|
||||
# Using videos in Remotion
|
||||
|
||||
## Prerequisites
|
||||
|
||||
First, the @remotion/media package needs to be installed.
|
||||
If it is not, use the following command:
|
||||
|
||||
```bash
|
||||
npx remotion add @remotion/media # If project uses npm
|
||||
bunx remotion add @remotion/media # If project uses bun
|
||||
yarn remotion add @remotion/media # If project uses yarn
|
||||
pnpm exec remotion add @remotion/media # If project uses pnpm
|
||||
```
|
||||
|
||||
Use `<Video>` from `@remotion/media` to embed videos into your composition.
|
||||
|
||||
```tsx
|
||||
import { Video } from "@remotion/media";
|
||||
import { staticFile } from "remotion";
|
||||
|
||||
export const MyComposition = () => {
|
||||
return <Video src={staticFile("video.mp4")} />;
|
||||
};
|
||||
```
|
||||
|
||||
Remote URLs are also supported:
|
||||
|
||||
```tsx
|
||||
<Video src="https://remotion.media/video.mp4" />
|
||||
```
|
||||
|
||||
## Trimming
|
||||
|
||||
Use `trimBefore` and `trimAfter` to remove portions of the video. Values are in seconds.
|
||||
|
||||
```tsx
|
||||
const { fps } = useVideoConfig();
|
||||
|
||||
return (
|
||||
<Video
|
||||
src={staticFile("video.mp4")}
|
||||
trimBefore={2 * fps} // Skip the first 2 seconds
|
||||
trimAfter={10 * fps} // End at the 10 second mark
|
||||
/>
|
||||
);
|
||||
```
|
||||
|
||||
## Delaying
|
||||
|
||||
Wrap the video in a `<Sequence>` to delay when it appears:
|
||||
|
||||
```tsx
|
||||
import { Sequence, staticFile } from "remotion";
|
||||
import { Video } from "@remotion/media";
|
||||
|
||||
const { fps } = useVideoConfig();
|
||||
|
||||
return (
|
||||
<Sequence from={1 * fps}>
|
||||
<Video src={staticFile("video.mp4")} />
|
||||
</Sequence>
|
||||
);
|
||||
```
|
||||
|
||||
The video will appear after 1 second.
|
||||
|
||||
## Sizing and Position
|
||||
|
||||
Use the `style` prop to control size and position:
|
||||
|
||||
```tsx
|
||||
<Video
|
||||
src={staticFile("video.mp4")}
|
||||
style={{
|
||||
width: 500,
|
||||
height: 300,
|
||||
position: "absolute",
|
||||
top: 100,
|
||||
left: 50,
|
||||
objectFit: "cover",
|
||||
}}
|
||||
/>
|
||||
```
|
||||
|
||||
## Volume
|
||||
|
||||
Set a static volume (0 to 1):
|
||||
|
||||
```tsx
|
||||
<Video src={staticFile("video.mp4")} volume={0.5} />
|
||||
```
|
||||
|
||||
Or use a callback for dynamic volume based on the current frame:
|
||||
|
||||
```tsx
|
||||
import { interpolate } from "remotion";
|
||||
|
||||
const { fps } = useVideoConfig();
|
||||
|
||||
return (
|
||||
<Video
|
||||
src={staticFile("video.mp4")}
|
||||
volume={(f) =>
|
||||
interpolate(f, [0, 1 * fps], [0, 1], { extrapolateRight: "clamp" })
|
||||
}
|
||||
/>
|
||||
);
|
||||
```
|
||||
|
||||
Use `muted` to silence the video entirely:
|
||||
|
||||
```tsx
|
||||
<Video src={staticFile("video.mp4")} muted />
|
||||
```
|
||||
|
||||
## Speed
|
||||
|
||||
Use `playbackRate` to change the playback speed:
|
||||
|
||||
```tsx
|
||||
<Video src={staticFile("video.mp4")} playbackRate={2} /> {/* 2x speed */}
|
||||
<Video src={staticFile("video.mp4")} playbackRate={0.5} /> {/* Half speed */}
|
||||
```
|
||||
|
||||
Reverse playback is not supported.
|
||||
|
||||
## Looping
|
||||
|
||||
Use `loop` to loop the video indefinitely:
|
||||
|
||||
```tsx
|
||||
<Video src={staticFile("video.mp4")} loop />
|
||||
```
|
||||
|
||||
Use `loopVolumeCurveBehavior` to control how the frame count behaves when looping:
|
||||
|
||||
- `"repeat"`: Frame count resets to 0 each loop (for `volume` callback)
|
||||
- `"extend"`: Frame count continues incrementing
|
||||
|
||||
```tsx
|
||||
<Video
|
||||
src={staticFile("video.mp4")}
|
||||
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
|
||||
<Video
|
||||
src={staticFile("video.mp4")}
|
||||
toneFrequency={1.5} // Higher pitch
|
||||
/>
|
||||
<Video
|
||||
src={staticFile("video.mp4")}
|
||||
toneFrequency={0.8} // Lower pitch
|
||||
/>
|
||||
```
|
||||
|
||||
Pitch shifting only works during server-side rendering, not in the Remotion Studio preview or in the `<Player />`.
|
||||
Reference in New Issue
Block a user