Initial release — OpenMontage: the first open-source agentic video production system
11 production pipelines, 47 tools, 124 agent skills. Supports cloud APIs (fal.ai, OpenAI, ElevenLabs, Suno, HeyGen, Runway) and free local providers (diffusers, Piper TTS, WAN 2.1, Hunyuan, CogVideo). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,160 @@
|
||||
---
|
||||
name: remotion
|
||||
description: Toolkit-specific Remotion patterns — custom transitions, shared components, and project conventions. For core Remotion framework knowledge (hooks, animations, rendering, etc.), see the `remotion-official` skill.
|
||||
---
|
||||
|
||||
# Remotion — Toolkit Extensions
|
||||
|
||||
> **Core Remotion knowledge** lives in `.claude/skills/remotion-official/` (synced from the official [remotion-dev/skills](https://github.com/remotion-dev/skills) repo). This file covers **toolkit-specific** patterns only.
|
||||
|
||||
## Shared Components
|
||||
|
||||
Reusable video components in `lib/components/`. Import in templates via:
|
||||
|
||||
```tsx
|
||||
import { AnimatedBackground, SlideTransition, Label } from '../../../../lib/components';
|
||||
```
|
||||
|
||||
| Component | Purpose |
|
||||
|-----------|---------|
|
||||
| `AnimatedBackground` | Floating shapes background (variants: subtle, tech, warm, dark) |
|
||||
| `SlideTransition` | Scene transitions (fade, zoom, slide-up, blur-fade) |
|
||||
| `Label` | Floating label badge with optional JIRA reference |
|
||||
| `Vignette` | Cinematic edge darkening overlay |
|
||||
| `LogoWatermark` | Corner logo branding |
|
||||
| `SplitScreen` | Side-by-side video comparison |
|
||||
| `NarratorPiP` | Picture-in-picture presenter overlay |
|
||||
| `Envelope` | 3D envelope with opening flap animation |
|
||||
| `PointingHand` | Animated hand emoji with slide-in and pulse |
|
||||
| `MazeDecoration` | Animated isometric grid decoration for corners |
|
||||
|
||||
## Custom Transitions
|
||||
|
||||
The toolkit includes a transitions library at `lib/transitions/` for scene-to-scene effects beyond the official `@remotion/transitions` package.
|
||||
|
||||
### Using TransitionSeries
|
||||
|
||||
```tsx
|
||||
import { TransitionSeries, linearTiming } from '@remotion/transitions';
|
||||
// Import custom transitions from lib (adjust path based on your project location)
|
||||
import { glitch, lightLeak, clockWipe, checkerboard } from '../../../../lib/transitions';
|
||||
// Or import from @remotion/transitions for official ones
|
||||
import { slide, fade } from '@remotion/transitions/slide';
|
||||
|
||||
<TransitionSeries>
|
||||
<TransitionSeries.Sequence durationInFrames={90}>
|
||||
<TitleSlide />
|
||||
</TransitionSeries.Sequence>
|
||||
<TransitionSeries.Transition
|
||||
presentation={glitch({ intensity: 0.8 })}
|
||||
timing={linearTiming({ durationInFrames: 30 })}
|
||||
/>
|
||||
<TransitionSeries.Sequence durationInFrames={120}>
|
||||
<ContentSlide />
|
||||
</TransitionSeries.Sequence>
|
||||
</TransitionSeries>
|
||||
```
|
||||
|
||||
### Available Custom Transitions
|
||||
|
||||
| Transition | Options | Best For |
|
||||
|------------|---------|----------|
|
||||
| `glitch()` | `intensity`, `slices`, `rgbShift` | Tech demos, edgy reveals, cyberpunk |
|
||||
| `rgbSplit()` | `direction`, `displacement` | Modern tech, energetic transitions |
|
||||
| `zoomBlur()` | `direction`, `blurAmount` | CTAs, high-energy moments, impact |
|
||||
| `lightLeak()` | `temperature`, `direction` | Celebrations, film aesthetic, warm moments |
|
||||
| `clockWipe()` | `startAngle`, `direction`, `segments` | Time-related content, playful reveals |
|
||||
| `pixelate()` | `maxBlockSize`, `gridSize`, `scanlines`, `glitchArtifacts`, `randomness` | Retro/gaming, digital transformations |
|
||||
| `checkerboard()` | `gridSize`, `pattern`, `stagger`, `squareAnimation` | Playful reveals, structured transitions |
|
||||
|
||||
**Checkerboard patterns:** `sequential`, `random`, `diagonal`, `alternating`, `spiral`, `rows`, `columns`, `center-out`, `corners-in`
|
||||
|
||||
### Transition Examples
|
||||
|
||||
```tsx
|
||||
// Tech/cyberpunk feel
|
||||
glitch({ intensity: 0.8, slices: 8, rgbShift: true })
|
||||
|
||||
// Warm celebration
|
||||
lightLeak({ temperature: 'warm', direction: 'right' })
|
||||
|
||||
// High energy zoom
|
||||
zoomBlur({ direction: 'in', blurAmount: 20 })
|
||||
|
||||
// Chromatic aberration
|
||||
rgbSplit({ direction: 'diagonal', displacement: 30 })
|
||||
|
||||
// Clock sweep reveal
|
||||
clockWipe({ direction: 'clockwise', startAngle: 0 })
|
||||
|
||||
// Retro pixelation
|
||||
pixelate({ maxBlockSize: 50, glitchArtifacts: true })
|
||||
|
||||
// Checkerboard patterns
|
||||
checkerboard({ pattern: 'diagonal', gridSize: 8 })
|
||||
checkerboard({ pattern: 'spiral', gridSize: 10 })
|
||||
checkerboard({ pattern: 'center-out', squareAnimation: 'scale' })
|
||||
```
|
||||
|
||||
### Transition Duration Guidelines
|
||||
|
||||
| Type | Frames | Notes |
|
||||
|------|--------|-------|
|
||||
| Quick cut | 15-20 | Fast, punchy |
|
||||
| Standard | 30-45 | Most common |
|
||||
| Dramatic | 50-60 | Slow reveals |
|
||||
| Glitch effects | 20-30 | Should feel sudden |
|
||||
| Light leak | 45-60 | Needs time to sweep |
|
||||
|
||||
### Preview Transitions
|
||||
|
||||
Run the showcase gallery to see all transitions:
|
||||
|
||||
```bash
|
||||
cd showcase/transitions && npm run studio
|
||||
```
|
||||
|
||||
## Toolkit Best Practices
|
||||
|
||||
1. **Frame-based animations only** — Avoid CSS transitions/animations; they cause flickering during render
|
||||
2. **Use fps from useVideoConfig()** — Make animations frame-rate independent
|
||||
3. **Clamp interpolations** — Use `extrapolateRight: 'clamp'` to prevent runaway values
|
||||
4. **Use OffthreadVideo** — Better performance than `<Video>` for complex compositions
|
||||
5. **delayRender for async** — Always block rendering until data is ready
|
||||
6. **staticFile for assets** — Reference files from `public/` folder correctly
|
||||
7. **All projects use 30fps** — Timing: frames = seconds × 30
|
||||
8. **playbackRate must be constant** — For variable/extreme speeds, pre-process with FFmpeg
|
||||
|
||||
## Project Timing Conventions
|
||||
|
||||
| Scene Type | Duration | Notes |
|
||||
|------------|----------|-------|
|
||||
| Title | 3-5s (90-150f) | Logo + headline |
|
||||
| Overview | 10-20s | 3-5 bullet points |
|
||||
| Demo | 10-30s | Adjust playbackRate to fit |
|
||||
| Stats | 8-12s | 3-4 stat cards |
|
||||
| Credits | 5-10s | Quick fade |
|
||||
|
||||
**Pacing:** ~150 words/minute for voiceover. Voiceover drives timing.
|
||||
|
||||
## Advanced API
|
||||
|
||||
For detailed API documentation on all hooks, components, renderer, Lambda, and Player APIs, see [reference.md](reference.md).
|
||||
|
||||
## License Note
|
||||
|
||||
Remotion has a special license. Companies may need to obtain a license for commercial use. Check https://remotion.dev/license
|
||||
|
||||
---
|
||||
|
||||
## Feedback & Contributions
|
||||
|
||||
If this skill is missing information or could be improved:
|
||||
|
||||
- **Missing a pattern?** Describe what you needed
|
||||
- **Found an error?** Let me know what's wrong
|
||||
- **Want to contribute?** I can help you:
|
||||
1. Update this skill with improvements
|
||||
2. Create a PR to github.com/digitalsamba/claude-code-video-toolkit
|
||||
|
||||
Just say "improve this skill" and I'll guide you through updating `.claude/skills/remotion/SKILL.md`.
|
||||
@@ -0,0 +1,499 @@
|
||||
# Remotion API Reference
|
||||
|
||||
## Core Hooks
|
||||
|
||||
### useCurrentFrame()
|
||||
```tsx
|
||||
const frame = useCurrentFrame();
|
||||
```
|
||||
Returns current frame (0-indexed). Inside `<Sequence>`, returns relative frame.
|
||||
|
||||
### useVideoConfig()
|
||||
```tsx
|
||||
const { width, height, fps, durationInFrames, id, defaultProps } = useVideoConfig();
|
||||
```
|
||||
|
||||
## interpolate()
|
||||
|
||||
```tsx
|
||||
interpolate(
|
||||
input: number,
|
||||
inputRange: number[],
|
||||
outputRange: number[],
|
||||
options?: {
|
||||
extrapolateLeft?: 'extend' | 'clamp' | 'identity' | 'wrap',
|
||||
extrapolateRight?: 'extend' | 'clamp' | 'identity' | 'wrap',
|
||||
easing?: (t: number) => number
|
||||
}
|
||||
): number
|
||||
```
|
||||
|
||||
**Examples:**
|
||||
```tsx
|
||||
// Basic interpolation
|
||||
interpolate(15, [0, 30], [0, 100]); // 50
|
||||
|
||||
// With clamping
|
||||
interpolate(50, [0, 30], [0, 1], { extrapolateRight: 'clamp' }); // 1
|
||||
|
||||
// Multiple keyframes
|
||||
interpolate(frame, [0, 20, 40, 60], [0, 1, 1, 0]);
|
||||
|
||||
// With easing
|
||||
interpolate(frame, [0, 30], [0, 100], { easing: Easing.bezier(0.42, 0, 0.58, 1) });
|
||||
```
|
||||
|
||||
## spring()
|
||||
|
||||
```tsx
|
||||
spring({
|
||||
frame: number,
|
||||
fps: number,
|
||||
config?: {
|
||||
damping?: number, // Default: 10
|
||||
mass?: number, // Default: 1
|
||||
stiffness?: number, // Default: 100
|
||||
overshootClamping?: boolean
|
||||
},
|
||||
from?: number, // Default: 0
|
||||
to?: number, // Default: 1
|
||||
durationInFrames?: number,
|
||||
durationRestThreshold?: number,
|
||||
delay?: number,
|
||||
reverse?: boolean
|
||||
}): number
|
||||
```
|
||||
|
||||
**Config presets:**
|
||||
- High bounce: `{ damping: 5, stiffness: 200 }`
|
||||
- No bounce: `{ damping: 20, stiffness: 100, overshootClamping: true }`
|
||||
- Slow: `{ damping: 20, mass: 2 }`
|
||||
|
||||
## measureSpring()
|
||||
|
||||
Get the duration of a spring animation:
|
||||
```tsx
|
||||
import { measureSpring } from 'remotion';
|
||||
|
||||
const duration = measureSpring({ fps: 30, config: { damping: 10 } });
|
||||
// Returns number of frames until spring settles
|
||||
```
|
||||
|
||||
## interpolateColors()
|
||||
|
||||
```tsx
|
||||
interpolateColors(
|
||||
input: number,
|
||||
inputRange: number[],
|
||||
outputRange: string[], // Hex, rgb(), rgba(), hsl()
|
||||
options?: { extrapolateLeft?, extrapolateRight? }
|
||||
): string
|
||||
```
|
||||
|
||||
## Easing
|
||||
|
||||
```tsx
|
||||
import { Easing } from 'remotion';
|
||||
|
||||
// Basic
|
||||
Easing.linear
|
||||
Easing.ease
|
||||
Easing.quad
|
||||
Easing.cubic
|
||||
|
||||
// In/Out/InOut variants
|
||||
Easing.in(Easing.quad)
|
||||
Easing.out(Easing.cubic)
|
||||
Easing.inOut(Easing.ease)
|
||||
|
||||
// Cubic bezier
|
||||
Easing.bezier(x1, y1, x2, y2)
|
||||
|
||||
// Other
|
||||
Easing.circle
|
||||
Easing.back(s?) // Overshoot
|
||||
Easing.elastic(bounciness?)
|
||||
Easing.bounce
|
||||
Easing.sin
|
||||
Easing.exp
|
||||
Easing.poly(n) // Power of n
|
||||
```
|
||||
|
||||
## Components
|
||||
|
||||
### Composition
|
||||
```tsx
|
||||
<Composition
|
||||
id="MyVideo"
|
||||
component={MyComponent}
|
||||
// OR lazyComponent={() => import('./MyComponent')}
|
||||
durationInFrames={150}
|
||||
fps={30}
|
||||
width={1920}
|
||||
height={1080}
|
||||
defaultProps={{ title: 'Hello' }}
|
||||
calculateMetadata={async ({ props }) => ({
|
||||
durationInFrames: props.items.length * 30,
|
||||
props: { ...props, computed: true }
|
||||
})}
|
||||
/>
|
||||
```
|
||||
|
||||
### Sequence
|
||||
```tsx
|
||||
<Sequence
|
||||
from={30} // Start frame
|
||||
durationInFrames={60} // Optional duration
|
||||
name="Intro" // Label in Studio timeline
|
||||
layout="none" // "none" | "absolute-fill"
|
||||
>
|
||||
<Child />
|
||||
</Sequence>
|
||||
```
|
||||
|
||||
### Series
|
||||
```tsx
|
||||
<Series>
|
||||
<Series.Sequence durationInFrames={30} offset={-5}>
|
||||
<A /> {/* Frames 0-29 */}
|
||||
</Series.Sequence>
|
||||
<Series.Sequence durationInFrames={60}>
|
||||
<B /> {/* Frames 25-84 (offset caused overlap) */}
|
||||
</Series.Sequence>
|
||||
</Series>
|
||||
```
|
||||
|
||||
### Loop
|
||||
```tsx
|
||||
<Loop
|
||||
durationInFrames={30}
|
||||
times={3} // Or Infinity
|
||||
layout="none"
|
||||
>
|
||||
<Animation />
|
||||
</Loop>
|
||||
```
|
||||
|
||||
### AbsoluteFill
|
||||
```tsx
|
||||
<AbsoluteFill style={{ backgroundColor: '#000' }}>
|
||||
{/* Position: absolute, full width/height */}
|
||||
</AbsoluteFill>
|
||||
```
|
||||
|
||||
### Media Components
|
||||
|
||||
**Img** (waits for load):
|
||||
```tsx
|
||||
<Img src={staticFile('photo.jpg')} style={{ width: '100%' }} />
|
||||
```
|
||||
|
||||
**Video/Html5Video:**
|
||||
```tsx
|
||||
<Video
|
||||
src={staticFile('clip.mp4')}
|
||||
volume={0.5} // 0-1, or callback: (f) => f / 100
|
||||
playbackRate={1.5}
|
||||
muted={false}
|
||||
loop={false}
|
||||
startFrom={30} // Skip first 30 frames of source
|
||||
endAt={120} // Stop at frame 120 of source
|
||||
acceptableTimeShiftInSeconds={0.2}
|
||||
/>
|
||||
```
|
||||
|
||||
**OffthreadVideo** (better performance):
|
||||
```tsx
|
||||
<OffthreadVideo
|
||||
src={staticFile('clip.mp4')}
|
||||
volume={0.5}
|
||||
transparent={false} // For videos with alpha
|
||||
toneMapped={true} // HDR tone mapping
|
||||
/>
|
||||
```
|
||||
|
||||
**Audio:**
|
||||
```tsx
|
||||
<Audio
|
||||
src={staticFile('music.mp3')}
|
||||
volume={0.8}
|
||||
startFrom={0}
|
||||
endAt={300}
|
||||
playbackRate={1}
|
||||
/>
|
||||
```
|
||||
|
||||
**AnimatedImage** (GIF/APNG):
|
||||
```tsx
|
||||
<AnimatedImage src={staticFile('animation.gif')} />
|
||||
```
|
||||
|
||||
## Async Handling
|
||||
|
||||
```tsx
|
||||
import { delayRender, continueRender, cancelRender } from 'remotion';
|
||||
|
||||
// Block render
|
||||
const handle = delayRender('Loading data...');
|
||||
|
||||
// Unblock when ready
|
||||
continueRender(handle);
|
||||
|
||||
// Cancel on error
|
||||
cancelRender(new Error('Failed to load'));
|
||||
```
|
||||
|
||||
**With timeout:**
|
||||
```tsx
|
||||
const handle = delayRender('Loading...', { timeoutInMilliseconds: 30000 });
|
||||
```
|
||||
|
||||
## Static Files & Prefetching
|
||||
|
||||
```tsx
|
||||
import { staticFile, prefetch, getStaticFiles } from 'remotion';
|
||||
|
||||
// Reference file in public/
|
||||
const url = staticFile('video.mp4');
|
||||
|
||||
// Prefetch for faster playback
|
||||
const { free, waitUntilDone } = prefetch(url);
|
||||
await waitUntilDone();
|
||||
// Later: free() to release memory
|
||||
|
||||
// List all static files
|
||||
const files = getStaticFiles(); // ['video.mp4', 'image.png', ...]
|
||||
```
|
||||
|
||||
## Input Props
|
||||
|
||||
```tsx
|
||||
import { getInputProps } from 'remotion';
|
||||
|
||||
const props = getInputProps(); // Data passed via --props CLI flag
|
||||
```
|
||||
|
||||
## Environment Detection
|
||||
|
||||
```tsx
|
||||
import { getRemotionEnvironment } from 'remotion';
|
||||
|
||||
const env = getRemotionEnvironment();
|
||||
// { isStudio: boolean, isRendering: boolean, isPlayer: boolean }
|
||||
```
|
||||
|
||||
## random()
|
||||
|
||||
Deterministic random for consistent renders:
|
||||
```tsx
|
||||
import { random } from 'remotion';
|
||||
|
||||
const value = random('my-seed'); // 0-1, same every render
|
||||
const value2 = random('seed', 0, 100); // 0-100
|
||||
const value3 = random(null); // Different each render
|
||||
```
|
||||
|
||||
## @remotion/renderer API
|
||||
|
||||
```tsx
|
||||
import { bundle } from '@remotion/bundler';
|
||||
import {
|
||||
renderMedia,
|
||||
renderStill,
|
||||
selectComposition,
|
||||
getCompositions,
|
||||
renderFrames,
|
||||
stitchFramesToVideo
|
||||
} from '@remotion/renderer';
|
||||
|
||||
// Bundle project
|
||||
const bundleLocation = await bundle({
|
||||
entryPoint: './src/index.ts',
|
||||
webpackOverride: (config) => config,
|
||||
});
|
||||
|
||||
// Get composition
|
||||
const composition = await selectComposition({
|
||||
serveUrl: bundleLocation,
|
||||
id: 'MyComp',
|
||||
inputProps: {},
|
||||
});
|
||||
|
||||
// Render video
|
||||
await renderMedia({
|
||||
composition,
|
||||
serveUrl: bundleLocation,
|
||||
codec: 'h264', // h264, h265, vp8, vp9, gif, prores
|
||||
outputLocation: 'out.mp4',
|
||||
inputProps: {},
|
||||
onProgress: ({ progress }) => console.log(`${progress * 100}%`),
|
||||
imageFormat: 'jpeg', // jpeg or png
|
||||
jpegQuality: 80,
|
||||
scale: 1,
|
||||
frameRange: [0, 59], // Optional: specific frames
|
||||
muted: false,
|
||||
audioBitrate: '128k',
|
||||
videoBitrate: '5M',
|
||||
crf: 18, // Quality (lower = better, bigger)
|
||||
concurrency: 4,
|
||||
});
|
||||
|
||||
// Render still image
|
||||
await renderStill({
|
||||
composition,
|
||||
serveUrl: bundleLocation,
|
||||
output: 'thumbnail.png',
|
||||
frame: 30,
|
||||
imageFormat: 'png',
|
||||
});
|
||||
```
|
||||
|
||||
## @remotion/lambda API
|
||||
|
||||
```tsx
|
||||
import {
|
||||
deployFunction,
|
||||
deploySite,
|
||||
renderMediaOnLambda,
|
||||
renderStillOnLambda,
|
||||
getRenderProgress,
|
||||
downloadMedia,
|
||||
} from '@remotion/lambda';
|
||||
|
||||
// Deploy function
|
||||
const { functionName } = await deployFunction({
|
||||
region: 'us-east-1',
|
||||
timeoutInSeconds: 120,
|
||||
memorySizeInMb: 2048,
|
||||
});
|
||||
|
||||
// Deploy site
|
||||
const { serveUrl } = await deploySite({
|
||||
entryPoint: './src/index.ts',
|
||||
region: 'us-east-1',
|
||||
siteName: 'my-video',
|
||||
});
|
||||
|
||||
// Render
|
||||
const { renderId, bucketName } = await renderMediaOnLambda({
|
||||
region: 'us-east-1',
|
||||
functionName,
|
||||
serveUrl,
|
||||
composition: 'MyComp',
|
||||
codec: 'h264',
|
||||
inputProps: {},
|
||||
framesPerLambda: 20,
|
||||
});
|
||||
|
||||
// Check progress
|
||||
const progress = await getRenderProgress({
|
||||
renderId,
|
||||
bucketName,
|
||||
region: 'us-east-1',
|
||||
functionName,
|
||||
});
|
||||
|
||||
// Download when done
|
||||
if (progress.done) {
|
||||
await downloadMedia({
|
||||
bucketName,
|
||||
renderId,
|
||||
region: 'us-east-1',
|
||||
outPath: 'video.mp4',
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## @remotion/player API
|
||||
|
||||
```tsx
|
||||
import { Player, PlayerRef } from '@remotion/player';
|
||||
|
||||
const playerRef = useRef<PlayerRef>(null);
|
||||
|
||||
<Player
|
||||
ref={playerRef}
|
||||
component={MyComp}
|
||||
// OR lazyComponent={() => import('./MyComp')}
|
||||
durationInFrames={150}
|
||||
fps={30}
|
||||
compositionWidth={1920}
|
||||
compositionHeight={1080}
|
||||
inputProps={{}}
|
||||
style={{ width: '100%' }}
|
||||
controls={true}
|
||||
autoPlay={false}
|
||||
loop={false}
|
||||
showVolumeControls={true}
|
||||
allowFullscreen={true}
|
||||
clickToPlay={true}
|
||||
doubleClickToFullscreen={true}
|
||||
spaceKeyToPlayOrPause={true}
|
||||
playbackRate={1}
|
||||
renderLoading={() => <div>Loading...</div>}
|
||||
errorFallback={({ error }) => <div>Error: {error.message}</div>}
|
||||
numberOfSharedAudioTags={5}
|
||||
initiallyShowControls={3000}
|
||||
renderPlayPauseButton={() => null}
|
||||
moveToBeginningWhenEnded={true}
|
||||
/>
|
||||
|
||||
// Imperative API
|
||||
playerRef.current?.play();
|
||||
playerRef.current?.pause();
|
||||
playerRef.current?.toggle();
|
||||
playerRef.current?.seekTo(30);
|
||||
playerRef.current?.getCurrentFrame();
|
||||
playerRef.current?.isPlaying();
|
||||
playerRef.current?.getVolume();
|
||||
playerRef.current?.setVolume(0.5);
|
||||
playerRef.current?.isMuted();
|
||||
playerRef.current?.mute();
|
||||
playerRef.current?.unmute();
|
||||
playerRef.current?.requestFullscreen();
|
||||
playerRef.current?.exitFullscreen();
|
||||
playerRef.current?.isFullscreen();
|
||||
|
||||
// Events
|
||||
<Player
|
||||
onPlay={() => {}}
|
||||
onPause={() => {}}
|
||||
onEnded={() => {}}
|
||||
onError={(e) => {}}
|
||||
onSeeked={(frame) => {}}
|
||||
onTimeUpdate={({ frame }) => {}}
|
||||
onFullscreenChange={(isFullscreen) => {}}
|
||||
/>
|
||||
```
|
||||
|
||||
## calculateMetadata
|
||||
|
||||
Dynamic composition properties:
|
||||
```tsx
|
||||
export const calculateMetadata: CalculateMetadataFunction<Props> = async ({
|
||||
props,
|
||||
abortSignal,
|
||||
defaultProps
|
||||
}) => {
|
||||
const data = await fetch('/api/data', { signal: abortSignal });
|
||||
|
||||
return {
|
||||
durationInFrames: data.items.length * 30,
|
||||
fps: 60,
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
props: { ...props, items: data.items },
|
||||
};
|
||||
};
|
||||
|
||||
<Composition
|
||||
id="Dynamic"
|
||||
component={MyComp}
|
||||
calculateMetadata={calculateMetadata}
|
||||
// Base values (can be overridden by calculateMetadata)
|
||||
durationInFrames={1}
|
||||
fps={30}
|
||||
width={1920}
|
||||
height={1080}
|
||||
/>
|
||||
```
|
||||
Reference in New Issue
Block a user