assets/animated-bar-chart.tsx
import React from 'react';
import { useFrame, useCompositionConfig, interpolate, spring } from '@rendiv/core';
const DATA = [
{ label: 'React', value: 85, color: '#61dafb' },
{ label: 'Vue', value: 62, color: '#42b883' },
{ label: 'Svelte', value: 48, color: '#ff3e00' },
{ label: 'Angular', value: 55, color: '#dd0031' },
{ label: 'Solid', value: 35, color: '#4f88c6' },
];
const BAR_HEIGHT = 48;
const GAP = 16;
const CHART_LEFT = 120;
const CHART_RIGHT = 160;
export const AnimatedBarChart: React.FC = () => {
const frame = useFrame();
const { fps, width, height } = useCompositionConfig();
const maxValue = Math.max(...DATA.map((d) => d.value));
const chartWidth = width - CHART_LEFT - CHART_RIGHT;
const totalHeight = DATA.length * (BAR_HEIGHT + GAP) - GAP;
const topOffset = (height - totalHeight) / 2;
return (
<div
style={{
width,
height,
backgroundColor: '#0f172a',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
fontFamily: 'system-ui, -apple-system, sans-serif',
}}
>
{/* Title */}
<div
style={{
position: 'absolute',
top: topOffset - 80,
left: CHART_LEFT,
color: '#e2e8f0',
fontSize: 36,
fontWeight: 700,
opacity: interpolate(frame, [0, 15], [0, 1], {
extrapolateRight: 'clamp',
}),
}}
>
Framework Popularity
</div>
{/* Bars */}
{DATA.map((item, i) => {
const staggerDelay = i * 4;
const barProgress = spring({
frame: frame - staggerDelay,
fps,
config: { damping: 14, stiffness: 120, mass: 0.8 },
});
const barWidth = (item.value / maxValue) * chartWidth * barProgress;
const labelOpacity = interpolate(frame - staggerDelay, [0, 10], [0, 1], {
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
});
const valueOpacity = interpolate(frame - staggerDelay - 8, [0, 10], [0, 1], {
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
});
return (
<div
key={item.label}
style={{
position: 'absolute',
top: topOffset + i * (BAR_HEIGHT + GAP),
left: 0,
width: '100%',
height: BAR_HEIGHT,
display: 'flex',
alignItems: 'center',
}}
>
{/* Label */}
<div
style={{
width: CHART_LEFT - 16,
textAlign: 'right',
color: '#94a3b8',
fontSize: 20,
fontWeight: 500,
opacity: labelOpacity,
}}
>
{item.label}
</div>
{/* Bar */}
<div
style={{
marginLeft: 16,
width: barWidth,
height: BAR_HEIGHT,
backgroundColor: item.color,
borderRadius: 6,
}}
/>
{/* Value */}
<div
style={{
marginLeft: 12,
color: '#e2e8f0',
fontSize: 22,
fontWeight: 600,
opacity: valueOpacity,
}}
>
{Math.round(item.value * barProgress)}
</div>
</div>
);
})}
</div>
);
};
assets/text-reveal.tsx
import React from 'react';
import { useFrame, useCompositionConfig, interpolate, Easing } from '@rendiv/core';
const TEXT = 'Build videos with code.';
const CHARS_PER_SECOND = 18;
export const TextReveal: React.FC = () => {
const frame = useFrame();
const { fps, width, height } = useCompositionConfig();
const framesPerChar = fps / CHARS_PER_SECOND;
const totalChars = TEXT.length;
return (
<div
style={{
width,
height,
backgroundColor: '#0c0c1d',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontFamily: 'system-ui, -apple-system, sans-serif',
}}
>
<div style={{ display: 'flex', flexWrap: 'wrap', justifyContent: 'center' }}>
{TEXT.split('').map((char, i) => {
const charStartFrame = i * framesPerChar;
const opacity = interpolate(
frame,
[charStartFrame, charStartFrame + 6],
[0, 1],
{ extrapolateLeft: 'clamp', extrapolateRight: 'clamp' },
);
const translateY = interpolate(
frame,
[charStartFrame, charStartFrame + 8],
[20, 0],
{
easing: Easing.out(Easing.ease),
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
},
);
// Cursor blink after all characters are revealed
const allRevealed = frame > (totalChars - 1) * framesPerChar + 10;
const showCursor =
i === totalChars - 1 && allRevealed
? Math.floor(frame / (fps / 2)) % 2 === 0
: false;
return (
<span
key={i}
style={{
display: 'inline-block',
fontSize: 72,
fontWeight: 700,
color: '#f1f5f9',
opacity,
transform: `translateY(${translateY}px)`,
whiteSpace: 'pre',
borderRight: showCursor ? '3px solid #6bd4ff' : 'none',
paddingRight: showCursor ? 4 : 0,
}}
>
{char}
</span>
);
})}
</div>
</div>
);
};
rules/animation.md
---
name: animation
description: >
Frame-driven animation in rendiv using interpolate, spring, easing curves,
color blending, and spring duration measurement.
---
# Animation
All animation in rendiv is derived from the current frame number. There are no
imperative keyframes or timeline state machines.
## `interpolate`
Maps a numeric input through a piecewise-linear (or eased) function.
```ts
import { interpolate } from '@rendiv/core';
interpolate(
input: number,
inputRange: readonly number[], // e.g. [0, 30]
outputRange: readonly number[], // e.g. [0, 1]
options?: {
easing?: (t: number) => number;
extrapolateLeft?: 'extend' | 'clamp' | 'identity'; // default: 'extend'
extrapolateRight?: 'extend' | 'clamp' | 'identity'; // default: 'extend'
}
): number
```
### Constraints
- `inputRange` and `outputRange` MUST have equal length, with at least 2 elements.
- `inputRange` MUST be monotonically non-decreasing.
- Multi-segment interpolation is supported (3+ points).
### Extrapolation modes
| Mode | Behavior when input is outside range |
|---|---|
| `'extend'` | Continues the slope of the nearest segment (default) |
| `'clamp'` | Clamps to the nearest output boundary |
| `'identity'` | Returns the raw input value |
### Examples
```tsx
const frame = useFrame();
// Fade in over 30 frames, stay at full opacity after
const opacity = interpolate(frame, [0, 30], [0, 1], { extrapolateRight: 'clamp' });
// Slide from offscreen left to center, then offscreen right
const translateX = interpolate(frame, [0, 30, 60], [-100, 0, 100]);
// Scale down then up (multi-segment)
const scale = interpolate(frame, [0, 15, 30], [1, 0.8, 1], {
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
});
```
## `spring`
Physics-based spring animation using a damped harmonic oscillator.
```ts
import { spring } from '@rendiv/core';
spring({
frame: number, // current frame (required)
fps: number, // frames per second (required)
config?: {
damping?: number, // default: 10
mass?: number, // default: 1
stiffness?: number, // default: 100
clampOvershoot?: boolean, // default: false
},
from?: number, // start value (default: 0)
to?: number, // target value (default: 1)
durationInFrames?: number,
durationRestThreshold?: number, // default: 0.005
}): number
```
### Usage
```tsx
const frame = useFrame();
const { fps } = useCompositionConfig();
// Basic entrance spring (0 → 1)
const scale = spring({ frame, fps });
// Custom physics
const bounce = spring({
frame,
fps,
config: { damping: 8, stiffness: 200, mass: 0.6 },
});
// Clamp overshoot for smooth scaling
const size = spring({
frame,
fps,
from: 0,
to: 100,
config: { damping: 12, stiffness: 150, clampOvershoot: true },
});
```
### Stagger pattern
Delay springs by subtracting from the frame to create staggered entrances:
```tsx
{items.map((item, i) => {
const delay = i * 3;
const s = spring({
frame: frame - delay,
fps,
config: { damping: 10, stiffness: 150, mass: 0.8 },
});
return <div key={i} style={{ opacity: s, transform: `scale(${s})` }}>{item}</div>;
})}
```
If `frame < 0` (before the delay), `spring` returns `from`.
## `Easing`
Easing functions for use with `interpolate`:
```ts
import { Easing } from '@rendiv/core';
```
| Function | Description |
|---|---|
| `Easing.linear` | Identity `t => t` |
| `Easing.ease` | CSS `ease` curve |
| `Easing.easeIn` | Accelerate from zero velocity |
| `Easing.easeOut` | Decelerate to zero velocity |
| `Easing.easeInOut` | Accelerate then decelerate |
| `Easing.bezier(x1, y1, x2, y2)` | Custom cubic bezier |
| `Easing.bounce` | Bounce effect |
| `Easing.elastic(bounciness?)` | Elastic overshoot (default bounciness: 1) |
| `Easing.in(fn)` | Apply easing as-is |
| `Easing.out(fn)` | Reverse an easing curve |
| `Easing.inOut(fn)` | Mirror an easing for in-out |
```tsx
const opacity = interpolate(frame, [0, 30], [0, 1], {
easing: Easing.easeInOut,
extrapolateRight: 'clamp',
});
const bounce = interpolate(frame, [0, 45], [0, 1], {
easing: Easing.out(Easing.bounce),
extrapolateRight: 'clamp',
});
```
## `getSpringDuration`
Calculates how many frames a spring takes to settle.
```ts
import { getSpringDuration } from '@rendiv/core';
const duration = getSpringDuration({
fps: 30,
config: { damping: 10, stiffness: 100 },
threshold: 0.005, // default
});
// Returns a frame number (e.g., 47)
```
Useful for sizing `<Sequence>` or `<Series.Sequence>` durations to match spring animations.
## `blendColors`
Interpolates between CSS colors in RGBA space.
```ts
import { blendColors } from '@rendiv/core';
blendColors(
value: number,
inputRange: readonly number[],
outputRange: readonly string[], // CSS color strings
options?: InterpolateOptions
): string // returns 'rgb(r, g, b)' or 'rgba(r, g, b, a)'
```
### Supported color formats
- Hex: `#fff`, `#ffffff`, `#ffffffaa`
- Functions: `rgb(r, g, b)`, `rgba(r, g, b, a)`
- Named: `black`, `white`, `red`, `green`, `blue`, `yellow`, `cyan`, `magenta`,
`orange`, `purple`, `pink`, `gray`, `grey`, `transparent`
### Example
```tsx
const frame = useFrame();
const bgColor = blendColors(frame, [0, 60, 120], ['#1a1a2e', '#16213e', '#0f3460']);
return <div style={{ backgroundColor: bgColor, width: '100%', height: '100%' }} />;
```
Same constraints as `interpolate` for input/output ranges.
rules/captions.md
---
name: captions
description: >
Adding subtitles and captions to rendiv compositions — parsing SRT files,
Whisper transcripts, word-by-word highlighting, and rendering overlays.
---
# Captions — @rendiv/captions
Parse subtitle files, create highlighted word-by-word captions (TikTok/Reels
style), and render caption overlays in rendiv compositions.
## Installation
```bash
pnpm add @rendiv/captions
```
Peer dependencies: `react`, `@rendiv/core`.
## Basic Usage
### SRT Subtitles
```tsx
import { Fill } from '@rendiv/core';
import { parseSrt, CaptionRenderer } from '@rendiv/captions';
const srt = `1
00:00:00,500 --> 00:00:02,000
Hello world
2
00:00:02,500 --> 00:00:04,000
Welcome to rendiv`;
const captions = parseSrt(srt);
export function SubtitledVideo(): React.ReactElement {
return (
<Fill>
{/* Your video content */}
<CaptionRenderer
captions={captions}
align="bottom"
padding={40}
activeStyle={{
fontSize: 32,
color: '#fff',
fontWeight: 600,
textShadow: '0 2px 8px rgba(0,0,0,0.8)',
}}
/>
</Fill>
);
}
```
### Word-by-Word Highlighting
```tsx
import { parseSrt, createHighlightedCaptions, CaptionRenderer } from '@rendiv/captions';
import type { Caption } from '@rendiv/captions';
// Captions with word-level timing
const captions: Caption[] = [
{
text: 'Build stunning videos',
startMs: 500,
endMs: 2000,
words: [
{ text: 'Build', startMs: 500, endMs: 900 },
{ text: 'stunning', startMs: 900, endMs: 1400 },
{ text: 'videos', startMs: 1400, endMs: 2000 },
],
},
];
const highlighted = createHighlightedCaptions(captions, { maxWordsPerChunk: 3 });
export function HighlightedSubs(): React.ReactElement {
return (
<Fill>
<CaptionRenderer
captions={highlighted}
align="bottom"
activeStyle={{ fontSize: 36, color: 'rgba(255,255,255,0.5)' }}
highlightedWordStyle={{ color: '#ff0', fontWeight: 800 }}
/>
</Fill>
);
}
```
## Parsing Functions
### `parseSrt(srt: string): Caption[]`
Parses SRT subtitle format:
```
1
00:00:00,500 --> 00:00:02,000
Caption text here
```
Supports both `,` and `.` as millisecond separators.
### `serializeSrt(captions: Caption[]): string`
Converts `Caption[]` back to SRT format string.
### `parseWhisperTranscript(json: WhisperVerboseJson): Caption[]`
Parses OpenAI Whisper verbose JSON output. Timestamps in seconds are converted
to milliseconds. Word-level timing is preserved when present.
```tsx
import { parseWhisperTranscript } from '@rendiv/captions';
const whisperOutput = {
segments: [
{ text: 'Hello world', start: 0.5, end: 2.0, words: [
{ word: 'Hello', start: 0.5, end: 1.0 },
{ word: 'world', start: 1.0, end: 2.0 },
]},
],
};
const captions = parseWhisperTranscript(whisperOutput);
```
## `createHighlightedCaptions(captions, options?)`
Splits word-level captions into chunks for TikTok/Reels-style highlighting.
| Option | Type | Default | Description |
|---|---|---|---|
| `maxWordsPerChunk` | `number` | `3` | Max words visible at once |
Each word in a chunk gets its own `HighlightedCaption` entry with
`highlightedWordIndex` pointing to the active word during that word's time span.
## `<CaptionRenderer>` Props
| Prop | Type | Default | Description |
|---|---|---|---|
| `captions` | `Caption[] \| HighlightedCaption[]` | (required) | Captions to display |
| `style` | `CSSProperties` | — | Container style |
| `activeStyle` | `CSSProperties` | — | Style for the active caption text |
| `highlightedWordStyle` | `CSSProperties` | — | Style for the highlighted word |
| `align` | `'top' \| 'center' \| 'bottom'` | `'bottom'` | Vertical alignment |
| `padding` | `number` | `40` | Padding from edge in pixels |
### How It Works
1. Reads the current frame via `useFrame()` and fps via `useCompositionConfig()`.
2. Converts frame to milliseconds: `currentMs = (frame / fps) * 1000`.
3. Finds the active caption where `startMs <= currentMs < endMs`.
4. Returns `null` if no caption is active at the current time.
5. If the active caption is a `HighlightedCaption` with words, renders each word
as a `<span>` with the `highlightedWordStyle` on the active word.
## Utility Functions
### `msToFrame(ms, fps): number`
Convert milliseconds to a frame number.
### `frameToMs(frame, fps): number`
Convert a frame number to milliseconds.
## Types
```ts
interface CaptionWord {
text: string;
startMs: number;
endMs: number;
}
interface Caption {
text: string;
startMs: number;
endMs: number;
words?: CaptionWord[];
}
interface HighlightedCaption extends Caption {
highlightedWordIndex: number;
}
interface WhisperVerboseJson {
segments: WhisperSegment[];
}
```
## Important Notes
- **Pure CSS/DOM rendering** — captions are rendered as styled divs/spans, not canvas.
This means they're resolution-independent and work with any font.
- **Absolute positioning** — `<CaptionRenderer>` positions itself absolutely within
its parent. Make sure the parent has `position: relative` or is a `<Fill>`.
- **No word timing required** — `parseSrt()` returns plain `Caption[]` without
word-level timing. Word-by-word highlighting requires `words` with individual
`startMs`/`endMs` on each word.
rules/cli-and-studio.md
---
name: cli-and-studio
description: >
Using the rendiv CLI for rendering, the Studio dev server for previewing and
debugging, and the Player component for browser embedding.
---
# CLI, Studio, and Player
## CLI (`@rendiv/cli`)
The `rendiv` command-line tool handles rendering, still image capture, composition
listing, and launching Studio.
### Commands
#### `rendiv render`
Renders a composition to a video file.
```bash
rendiv render <entry> <compositionId> <output>
```
```bash
# Render to MP4
rendiv render src/index.tsx MyScene out/video.mp4
# Render to WebM
rendiv render src/index.tsx MyScene out/video.webm
# With options
rendiv render src/index.tsx MyScene out/video.mp4 \
--concurrency 4 \
--image-format jpeg \
--preset fast \
--crf 18 \
--profiling
```
The output format is determined by the file extension (`.mp4` or `.webm`).
##### Render options
| Flag | Default | Description |
|---|---|---|
| `--props <json>` | `'{}'` | Input props as JSON |
| `--codec <codec>` | `mp4` | Output codec (`mp4`, `webm`) |
| `--concurrency <n>` | `1` | Parallel browser tabs for frame capture |
| `--frames <range>` | all | Frame range (e.g. `0-59`) |
| `--image-format <fmt>` | `png` | Intermediate frame format (`png`, `jpeg`) |
| `--preset <preset>` | — | FFmpeg encoding preset (`ultrafast`, `fast`, `medium`, `slow`, `veryslow`) |
| `--crf <number>` | `18` | Quality factor 0-51, lower is better |
| `--video-encoder <enc>` | — | Video encoder (`libx264`, `h264_videotoolbox`, `h264_nvenc`) |
| `--gl <renderer>` | `swiftshader` | GL renderer (`swiftshader`, `egl`, `angle`) |
| `--profiling` | off | Enable per-frame profiling with phase breakdown |
#### `rendiv still`
Captures a single frame as an image.
```bash
rendiv still <entry> <compositionId> <output> [--frame <n>]
```
```bash
# Capture frame 0 (default)
rendiv still src/index.tsx Thumbnail out/thumb.png
# Capture a specific frame
rendiv still src/index.tsx MyScene out/frame90.png --frame 90
```
#### `rendiv compositions`
Lists all registered compositions in a project.
```bash
rendiv compositions <entry>
```
```bash
rendiv compositions src/index.tsx
# Outputs: id, dimensions, fps, duration for each composition
```
#### `rendiv upgrade`
Updates all `@rendiv/*` dependencies to the latest version.
```bash
# Check for updates without installing
rendiv upgrade --check
# Apply updates
rendiv upgrade
```
Auto-detects your package manager (pnpm, yarn, bun, npm), preserves version
prefix style (`^`, `~`), and runs install automatically.
#### `rendiv studio`
Launches the Studio dev server for interactive preview and rendering.
```bash
rendiv studio <entry>
```
```bash
rendiv studio src/index.tsx
```
## Studio
Studio is a Vite-powered dev server with a full preview UI.
### Features
- **Composition navigator**: Browse compositions organized by folder
- **Live preview**: Player-based preview with play/pause controls
- **Timeline scrubber**: Drag to seek through frames
- **Render modal**: Configure and queue render jobs with full settings
- Video or still image output (PNG/JPEG)
- Image format, encoding preset, CRF, video encoder, GL renderer, concurrency
- Still rendering captures a single frame (defaults to current playhead position)
- **Render queue**: Render jobs run server-side
- Jobs persist across page refreshes (stored in server memory)
- Jobs continue even if the browser tab is closed
- Download button for completed renders
- REST API at `/__rendiv_api__/render/queue`
### Architecture
Studio writes temp files (`entry.tsx`, `studio.html`, `favicon.svg`) to `.studio/`
in the project root. These are needed for Vite module resolution. The directory is
cleaned up when the server closes.
Studio UI components ship as uncompiled `.tsx` source — Vite processes them at
dev time.
### Package.json script pattern
```json
{
"scripts": {
"studio": "rendiv studio src/index.tsx",
"render": "rendiv render src/index.tsx MyScene out/video.mp4",
"still": "rendiv still src/index.tsx MyScene out/still.png"
}
}
```
## Player (`@rendiv/player`)
Embeds a composition in any React application for browser playback.
```tsx
import { Player } from '@rendiv/player';
import { Root } from './Root';
<Player
compositionId="MyScene"
component={Root}
durationInFrames={150}
fps={30}
compositionWidth={1920}
compositionHeight={1080}
style={{ width: 800 }}
controls
autoPlay
loop
/>
```
### Key Props
| Prop | Type | Description |
|---|---|---|
| `compositionId` | `string` | ID of the composition to render |
| `component` | `ComponentType` | Root component that defines compositions |
| `durationInFrames` | `number` | Total frames |
| `fps` | `number` | Frames per second |
| `compositionWidth` | `number` | Native video width |
| `compositionHeight` | `number` | Native video height |
| `controls` | `boolean` | Show playback controls |
| `autoPlay` | `boolean` | Start playing immediately |
| `loop` | `boolean` | Loop playback |
| `style` | `CSSProperties` | Container styles |
| `inputProps` | `object` | Props to pass to the composition |
### Aspect ratio
The Player maintains the composition's aspect ratio. Set a `width` or `height` on
the `style` prop and the Player scales proportionally.
### Difference from Studio
- **Player**: Lightweight, embeddable in any React app. No server required.
- **Studio**: Full dev environment with timeline, render queue, composition browser.
Requires `rendiv studio` to run.
rules/composition-setup.md
---
name: composition-setup
description: >
Setting up a rendiv project entry point, registering compositions and stills,
organizing with folders, and configuring default props.
---
# Composition Setup
## Entry Point
Every rendiv project has a single entry file that calls `setRootComponent` with a
React component that declares all compositions:
```tsx
import { setRootComponent, Composition, Folder } from '@rendiv/core';
import { MyScene } from './MyScene';
import { Thumbnail } from './Thumbnail';
const Root: React.FC = () => (
<>
<Folder name="Scenes">
<Composition
id="MyScene"
component={MyScene}
durationInFrames={150}
fps={30}
width={1920}
height={1080}
/>
</Folder>
<Folder name="Thumbnails">
<Still
id="Thumbnail"
component={Thumbnail}
width={1280}
height={720}
/>
</Folder>
</>
);
setRootComponent(Root);
```
### Rules
- `setRootComponent` MUST be called exactly once. A second call throws an error.
- The root component renders `<Composition>`, `<Still>`, and `<Folder>` elements.
These are metadata-only — they render `null` and register into the composition manager.
## `<CanvasElement>`
**IMPORTANT: Always wrap your composition's content with `<CanvasElement id="...">`.** This
makes the composition self-contained — its timeline overrides (position, scale, timing
edits from Studio) work correctly whether the composition is rendered standalone or nested
inside another "master" composition.
```tsx
import { CanvasElement, Series, useFrame } from '@rendiv/core';
export function MyScene(): React.ReactElement {
return (
<CanvasElement id="MyScene">
<Series>
<Series.Sequence durationInFrames={60}>
<IntroScene />
</Series.Sequence>
<Series.Sequence durationInFrames={90}>
<MainScene />
</Series.Sequence>
</Series>
</CanvasElement>
);
}
```
Without `<CanvasElement>`, overrides saved under `MyScene/...` keys will not apply when
the component is used inside a different composition. With it, inner Sequences always
build namePaths starting with the given `id`, regardless of nesting context.
### Props
| Prop | Type | Required | Description |
|---|---|---|---|
| `id` | `string` | Yes | The composition ID — must match the `<Composition>` id |
| `children` | `ReactNode` | Yes | The composition content |
### How it works
`<CanvasElement>` provides a `CanvasElementContext` that `<Sequence>` reads when building
its namePath. It also resets the parent `SequenceContext.namePath` to `''` so inner
Sequences start fresh. All timing fields (accumulatedOffset, playbackRate, etc.) pass
through unchanged — `useFrame()` and frame arithmetic are unaffected.
## `<Composition>`
Registers a video composition with the framework.
```tsx
<Composition
id="MyScene" // unique identifier (required)
component={MyScene} // React component or React.lazy() (required)
durationInFrames={150} // total frame count (required)
fps={30} // frames per second (required)
width={1920} // pixel width (required)
height={1080} // pixel height (required)
defaultProps={{ title: 'Hi' }} // optional default props
resolveConfig={async (params) => ({ ... })} // optional dynamic config
/>
```
### Props
| Prop | Type | Required | Description |
|---|---|---|---|
| `id` | `string` | Yes | Unique composition identifier |
| `component` | `ComponentType` or `LazyExoticComponent` | Yes | The scene component |
| `durationInFrames` | `number` | Yes | Total frames |
| `fps` | `number` | Yes | Frames per second |
| `width` | `number` | Yes | Video width in pixels |
| `height` | `number` | Yes | Video height in pixels |
| `defaultProps` | `Props` | No | Default props passed to the component |
| `resolveConfig` | `ResolveConfigFunction` | No | Async function to resolve config dynamically |
The `component` receives `defaultProps` (merged with any input props) as its React props.
## `<Still>`
Registers a single-frame image composition. Equivalent to a `<Composition>` with
`durationInFrames={1}` and `fps={30}`.
```tsx
import { Still } from '@rendiv/core';
<Still
id="Poster"
component={PosterDesign}
width={1080}
height={1080}
defaultProps={{ text: 'Coming Soon' }}
/>
```
Render a still: `rendiv still src/index.tsx Poster out/poster.png`
## `<Folder>`
Groups compositions into folders for organization in Studio and CLI output:
```tsx
import { Folder } from '@rendiv/core';
<Folder name="Social">
<Composition id="InstagramReel" ... />
<Composition id="TikTokClip" ... />
</Folder>
```
Folders can be nested:
```tsx
<Folder name="Marketing">
<Folder name="Social">
<Composition id="Tweet" ... />
</Folder>
</Folder>
```
The composition's folder path is built from the nesting: `Marketing/Social`.
## Common Resolutions
| Format | Width | Height | Aspect |
|---|---|---|---|
| 1080p landscape | 1920 | 1080 | 16:9 |
| 4K landscape | 3840 | 2160 | 16:9 |
| 1080p portrait | 1080 | 1920 | 9:16 |
| Instagram square | 1080 | 1080 | 1:1 |
| YouTube Shorts | 1080 | 1920 | 9:16 |
## Common Frame Rates
| FPS | Use case |
|---|---|
| 24 | Cinematic |
| 25 | PAL broadcast |
| 30 | Standard web video |
| 60 | Smooth motion / gaming |
rules/gif.md
---
name: gif
description: >
Rendering animated GIFs with frame-accurate playback in rendiv compositions
using the @rendiv/gif package.
---
# Animated GIFs — @rendiv/gif
Render animated GIFs as frame-accurate video components. Decodes GIF files using
`gifuct-js`, draws frames to a `<canvas>`, and integrates with rendiv's render
lifecycle via `holdRender`.
## Installation
```bash
pnpm add @rendiv/gif
```
Peer dependencies: `react`, `@rendiv/core`.
## Basic Usage
```tsx
import { Fill } from '@rendiv/core';
import { Gif } from '@rendiv/gif';
export function MyScene(): React.ReactElement {
return (
<Fill style={{ backgroundColor: '#000' }}>
<Gif
src="https://example.com/animation.gif"
width={400}
height={300}
fit="cover"
/>
</Fill>
);
}
```
## `<Gif>` Props
| Prop | Type | Default | Description |
|---|---|---|---|
| `src` | `string` | (required) | URL or path to the GIF file |
| `width` | `number` | GIF native | Display width in pixels |
| `height` | `number` | GIF native | Display height in pixels |
| `fit` | `'fill' \| 'contain' \| 'cover'` | `'fill'` | How the GIF fits within dimensions |
| `playbackRate` | `number` | `1` | Speed multiplier (0.5 = half speed, 2 = double) |
| `loop` | `boolean` | `true` | Whether the animation loops |
| `style` | `CSSProperties` | — | CSS styles on the canvas element |
| `className` | `string` | — | CSS class name |
| `holdRenderTimeout` | `number` | `30000` | Timeout in ms for holdRender |
## How It Works
1. On mount, calls `holdRender()` to block frame capture.
2. Fetches and decodes the GIF via `gifuct-js` (`parseGIF` + `decompressFrames`).
3. Calls `releaseRender()` once decoded — the renderer can now capture.
4. Each rendiv frame: calculates playback position in ms from `localFrame / fps`,
walks cumulative frame delays to find the correct GIF frame index.
5. Renders to a `<canvas>` using an offscreen compositing canvas for proper
GIF disposal handling (methods 0–3).
6. Optimizes forward playback (renders only new frames) and handles backward
seeks by re-compositing from frame 0.
## Preloading
Use `preloadGif()` to start decoding before the component mounts:
```tsx
import { preloadGif } from '@rendiv/gif';
// Call early — e.g., at module scope or in a parent effect
preloadGif('https://example.com/animation.gif');
```
GIFs are cached by URL in a module-level Map, so the same GIF is never decoded twice.
## Getting Duration
```tsx
import { getGifDurationInSeconds } from '@rendiv/gif';
const duration = await getGifDurationInSeconds('https://example.com/animation.gif');
// Use to calculate durationInFrames: Math.ceil(duration * fps)
```
## Playback Rate
```tsx
// Slow motion
<Gif src={url} playbackRate={0.5} />
// Double speed
<Gif src={url} playbackRate={2} />
// Reversed time mapping is not supported — use negative spring values
// in a parent to offset the frame if needed
```
## Important Notes
- **Use `<Gif>` instead of `<AnimatedImage>`** when you need playback rate control,
fit modes, or cross-browser GIF decoding (no `ImageDecoder` dependency).
- **`<AnimatedImage>` from `@rendiv/core`** uses the browser's `ImageDecoder` API
(Chromium-only). `<Gif>` uses `gifuct-js` which works everywhere.
- **Canvas-based rendering** — the GIF is drawn to a `<canvas>`, which works
correctly with Playwright screenshot capture during rendering.
- **GIF disposal types** are handled: no disposal (0/1), restore to background (2),
and restore to previous (3, treated as keep).
rules/lottie.md
# Lottie Animations — @rendiv/lottie
Embed frame-accurate Lottie animations inside rendiv compositions using
[lottie-web](https://github.com/airbnb/lottie-web) under the hood.
## Installation
```bash
pnpm add @rendiv/lottie lottie-web
```
Peer dependencies: `react`, `react-dom`, `@rendiv/core`.
## Basic Usage
```tsx
import { useFrame, useCompositionConfig, Fill, interpolate } from '@rendiv/core';
import { Lottie } from '@rendiv/lottie';
import animData from './my-animation.json';
export function MyScene(): React.ReactElement {
return (
<Fill style={{ backgroundColor: '#000' }}>
<Lottie
animationData={animData}
loop
style={{ width: 400, height: 400 }}
/>
</Fill>
);
}
```
## Props
| Prop | Type | Default | Description |
|---|---|---|---|
| `animationData` | `object` | (required) | Parsed Lottie JSON. **Memoize** with `useMemo` to avoid re-initialization on re-renders. |
| `renderer` | `'svg' \| 'canvas' \| 'html'` | `'svg'` | lottie-web renderer engine. SVG has the best feature coverage. |
| `loop` | `boolean` | `false` | Wrap the animation when rendiv frames exceed the Lottie duration. |
| `direction` | `'forward' \| 'backward'` | `'forward'` | Playback direction. |
| `playbackRate` | `number` | `1` | Speed multiplier mapping rendiv frames to Lottie frames. |
| `style` | `CSSProperties` | — | Container style. |
| `className` | `string` | — | Container class name. |
## How It Works
1. On mount, `<Lottie>` loads the animation with `lottie.loadAnimation({ autoplay: false })`.
2. It calls `holdRender()` until the `DOMLoaded` event fires — the renderer waits before capturing the frame.
3. On every rendiv frame change, it calculates the target lottie frame:
- `targetFrame = localFrame * playbackRate`
- If `loop`: wraps with modulo
- If `direction === 'backward'`: reverses
4. Calls `anim.goToAndStop(targetFrame, true)` for frame-accurate seeking.
5. On unmount, destroys the animation and releases any pending holds.
## Important Notes
- **Memoize `animationData`**: If you construct the data inline, wrap it in `useMemo` to prevent re-initialization on every render.
- **Frame mapping**: A Lottie file at 30 fps inside a rendiv composition at 30 fps maps 1:1. If the fps differ, adjust `playbackRate` accordingly (e.g., `playbackRate={lottie_fps / composition_fps}`).
- **No CSS animations**: The Lottie component is fully frame-driven. Do not rely on lottie-web's built-in playback — it uses wall-clock time.
## Combining With Sequences
```tsx
import { Sequence, Composition } from '@rendiv/core';
import { Lottie } from '@rendiv/lottie';
import introAnim from './intro.json';
import outroAnim from './outro.json';
export function AnimatedIntro(): React.ReactElement {
return (
<>
<Sequence from={0} durationInFrames={60}>
<Lottie animationData={introAnim} style={{ width: '100%', height: '100%' }} />
</Sequence>
<Sequence from={60} durationInFrames={60}>
<Lottie animationData={outroAnim} style={{ width: '100%', height: '100%' }} />
</Sequence>
</>
);
}
```
The `<Lottie>` component reads `SequenceContext` internally, so it automatically
adjusts to the local frame offset of its parent `<Sequence>`.
rules/media-components.md
---
name: media-components
description: >
Embedding images, video, audio, animated images (GIF/APNG/WebP), and iframes
in rendiv compositions using the built-in media components.
---
# Media Components
Rendiv provides drop-in replacements for native HTML media elements. These
components integrate with the render lifecycle via `holdRender` — they block
frame capture until the media is loaded, ensuring no blank frames in output.
**You MUST use these components instead of native HTML elements** (`<img>`,
`<video>`, `<audio>`, `<iframe>`).
## `<Img>`
Renders an image with automatic render-hold until loaded.
```tsx
import { Img } from '@rendiv/core';
<Img src={staticFile('photo.jpg')} style={{ width: '100%' }} />
```
### Props
Accepts all standard `<img>` HTML attributes, plus:
| Prop | Type | Default | Description |
|---|---|---|---|
| `holdRenderTimeout` | `number` | `30000` | Timeout in ms before throwing |
### Behavior
- Calls `holdRender()` on mount
- Calls `releaseRender()` on `load`, `error`, or unmount
- Forwards your `onLoad` and `onError` handlers (they run alongside the internal ones)
## `<Video>`
Embeds a video that syncs with rendiv's frame-based timeline.
```tsx
import { Video } from '@rendiv/core';
<Video
src={staticFile('clip.mp4')}
startFrom={30}
endAt={120}
volume={0.8}
playbackRate={1}
style={{ width: '100%' }}
/>
```
### Props
| Prop | Type | Default | Description |
|---|---|---|---|
| `src` | `string` | — | Video URL (required) |
| `startFrom` | `number` | `0` | Frame offset into the video |
| `endAt` | `number` | — | Frame at which to stop (relative to video) |
| `volume` | `number` | `1` | Volume (0 to 1) |
| `playbackRate` | `number` | `1` | Playback speed multiplier |
| `muted` | `boolean` | `false` | Mute audio |
| `holdRenderTimeout` | `number` | `30000` | Timeout in ms |
| `style` | `CSSProperties` | — | CSS styles |
| `className` | `string` | — | CSS class name |
Also accepts all standard `<video>` HTML attributes (except `autoPlay`).
### Environment-aware behavior
- **Rendering mode**: Pauses the video and seeks precisely to `(localFrame + startFrom) / fps`
for each frame. Uses `holdRender` to wait for seek completion.
- **Player/Studio mode**: Plays naturally with drift correction (re-syncs if > 0.1s off).
Auto-plays/pauses with the timeline.
## `<OffthreadVideo>` (Recommended)
**Best practice: Use `<OffthreadVideo>` instead of `<Video>` for all video embeds.**
It provides better rendering performance by extracting frames via FFmpeg rather than
relying on browser seeking, and handles audio extraction automatically.
```tsx
import { OffthreadVideo } from '@rendiv/core';
<OffthreadVideo src={staticFile('clip.mp4')} startFrom={0} style={{ width: '100%' }} />
```
### Props
Same as `<Video>`: `src`, `startFrom`, `endAt`, `volume`, `playbackRate`, `muted`,
`style`, `className`, `holdRenderTimeout`.
### Behavior
- **Player/Studio mode**: Delegates entirely to `<Video>` (full playback with sync).
- **Rendering mode**: Fetches each frame as an image from an HTTP endpoint
(`/__offthread_video__?src=...&time=...`) and displays it as an `<img>`.
Audio track metadata is registered separately so FFmpeg can mux it into the output.
Uses `holdRender` for each frame fetch and image load.
### Preloading with `premountFor`
Combine `<OffthreadVideo>` with `premountFor` on the parent Sequence to eliminate
buffering when a video scene appears:
```tsx
<Series>
<Series.Sequence durationInFrames={60}>
<TitleCard />
</Series.Sequence>
{/* Start loading the video 60 frames before it appears */}
<Series.Sequence durationInFrames={90} premountFor={60}>
<OffthreadVideo src={staticFile('intro.mp4')} style={{ width: '100%' }} />
</Series.Sequence>
</Series>
```
During premount the video component mounts invisibly (opacity 0) and starts
buffering, so when the sequence becomes visible playback begins immediately.
## `<Audio>`
Adds audio to a composition.
```tsx
import { Audio } from '@rendiv/core';
<Audio src={staticFile('music.mp3')} volume={0.5} startFrom={0} />
```
### Props
| Prop | Type | Default | Description |
|---|---|---|---|
| `src` | `string` | — | Audio URL (required) |
| `startFrom` | `number` | `0` | Frame offset into the audio |
| `endAt` | `number` | — | Frame at which to stop |
| `volume` | `number` | `1` | Volume (0 to 1) |
| `playbackRate` | `number` | `1` | Playback speed |
| `muted` | `boolean` | `false` | Mute |
### Behavior
- **Rendering mode**: Returns `null` (not visible in screenshots). Audio metadata is
registered to a global Map, which the renderer collects after frame capture. FFmpeg
then trims, delays, adjusts tempo, and mixes all audio tracks into the final output.
- **Player/Studio mode**: Plays and syncs with drift correction, auto-play/pause.
## `<AnimatedImage>`
Renders animated images (GIF, APNG, WebP) with frame-accurate playback on a canvas.
```tsx
import { AnimatedImage } from '@rendiv/core';
<AnimatedImage
src={staticFile('animation.gif')}
width={400}
height={300}
/>
```
### Props
| Prop | Type | Default | Description |
|---|---|---|---|
| `src` | `string` | — | URL of animated image (required) |
| `width` | `number` | — | Canvas width |
| `height` | `number` | — | Canvas height |
| `style` | `CSSProperties` | — | CSS styles |
| `className` | `string` | — | CSS class |
| `holdRenderTimeout` | `number` | `30000` | Timeout in ms |
### How it works
Uses the `ImageDecoder` API (Chromium) to extract individual frames with their
durations. Maps the rendiv frame to the correct animation frame, accounting for
per-frame timing and looping. Falls back to a static image in non-Chromium browsers.
## `<IFrame>`
Embeds an iframe with render-hold until loaded.
```tsx
import { IFrame } from '@rendiv/core';
<IFrame src="https://example.com" style={{ width: '100%', height: '100%' }} />
```
### Props
Accepts all standard `<iframe>` HTML attributes, plus:
| Prop | Type | Default | Description |
|---|---|---|---|
| `holdRenderTimeout` | `number` | `30000` | Timeout in ms |
Uses the same `holdRender`/`releaseRender` pattern as `<Img>`.
rules/procedural-effects.md
---
name: procedural-effects
description: >
Procedural animation with simplex noise (2D/3D/4D) and cinematic motion blur
using MotionTrail and ShutterBlur components.
---
# Procedural Effects
## @rendiv/noise
Simplex noise for organic, non-repetitive motion.
```ts
import { seed, noise2D, noise3D, noise4D } from '@rendiv/noise';
```
### API
| Function | Returns | Description |
|---|---|---|
| `seed(value)` | `void` | Seeds the permutation table (call before noise functions for reproducible results) |
| `noise2D(x, y)` | `number` in [-1, 1] | 2D simplex noise |
| `noise3D(x, y, z)` | `number` in [-1, 1] | 3D simplex noise |
| `noise4D(x, y, z, w)` | `number` in [-1, 1] | 4D simplex noise |
Default seed (0) is applied at import time.
### Common patterns
#### Organic drift
```tsx
import { noise2D, seed } from '@rendiv/noise';
seed(42);
const frame = useFrame();
const driftX = noise2D(0, frame * 0.02) * 60; // ±60px horizontal drift
const driftY = noise2D(100, frame * 0.02) * 40; // ±40px vertical drift
<div style={{ transform: `translate(${driftX}px, ${driftY}px)` }}>
<Logo />
</div>
```
Use different first arguments (0, 100) to get independent noise channels.
#### Noise grid
```tsx
import { noise3D, seed } from '@rendiv/noise';
seed(7);
{grid.map((_, i) => {
const col = i % cols;
const row = Math.floor(i / cols);
const n = noise3D(col * 0.15, row * 0.15, frame * 0.025);
const brightness = interpolate(n, [-1, 1], [0.2, 1]);
return (
<div
key={i}
style={{
opacity: brightness,
width: cellSize,
height: cellSize,
backgroundColor: 'white',
}}
/>
);
})}
```
#### Frequency and amplitude
- **Frequency** = how fast the noise changes. Multiply the input: `frame * 0.01` (slow)
vs `frame * 0.1` (fast).
- **Amplitude** = how far the output moves. Multiply the result: `noise2D(...) * 100`.
## @rendiv/motion-blur
Simulates motion blur by compositing multiple copies of children at slightly
different frames.
### `<MotionTrail>`
Renders layered copies at progressively earlier frames.
```tsx
import { MotionTrail } from '@rendiv/motion-blur';
<MotionTrail layers={8} offset={1} fadeRate={0.55}>
<MovingObject />
</MotionTrail>
```
#### Props
| Prop | Type | Default | Description |
|---|---|---|---|
| `layers` | `number` | `5` | Number of trail copies |
| `offset` | `number` | `1` | Frame offset between each layer |
| `fadeRate` | `number` | `0.6` | Opacity multiplier per layer (layer `i` has opacity `fadeRate^i`) |
#### How it works
- Renders `layers` copies of children
- Each layer overrides `TimelineContext.frame` to `currentFrame - (layerIndex * offset)`
- Oldest copy (most faded) is at the bottom, newest (full opacity) on top
- All layers are absolutely positioned within a relative container
### `<ShutterBlur>`
Simulates cinematic motion blur by averaging sub-frame samples.
```tsx
import { ShutterBlur } from '@rendiv/motion-blur';
<ShutterBlur angle={180} layers={10}>
<SpinningWheel />
</ShutterBlur>
```
#### Props
| Prop | Type | Default | Description |
|---|---|---|---|
| `angle` | `number` | `180` | Shutter angle in degrees (0–360). 180 = half-frame exposure |
| `layers` | `number` | `10` | Number of sub-frame samples |
#### How it works
- Each layer has opacity `1 / layers`
- Samples are distributed evenly from `frame - shutterFraction` to `frame`
where `shutterFraction = angle / 360`
- A 180° shutter angle (cinema standard) samples the previous half-frame
- Higher `layers` = smoother blur but more rendering work
### When to use which
| Component | Best for |
|---|---|
| `MotionTrail` | Stylistic afterimage trails, speed lines, ghosting effects |
| `ShutterBlur` | Realistic cinematic motion blur on fast-moving objects |
rules/render-lifecycle.md
---
name: render-lifecycle
description: >
The holdRender/releaseRender pattern for async resource loading, environment
awareness, static file serving, input props, and the rendering pipeline.
---
# Render Lifecycle
## `holdRender` / `releaseRender`
The renderer captures each frame as a screenshot. If a component needs to load
async resources (fonts, images, data), it must signal the renderer to wait.
```ts
import { holdRender, releaseRender } from '@rendiv/core';
const handle = holdRender('Loading my data', { timeoutInMilliseconds: 15000 });
// ... async work ...
releaseRender(handle);
```
### API
```ts
holdRender(label?: string, options?: { timeoutInMilliseconds?: number }): number
releaseRender(handle: number): void
abortRender(message: string): void
getPendingHoldCount(): number
getPendingHoldLabels(): string[]
```
### Rules
- `holdRender` returns a numeric handle. The renderer waits until
`getPendingHoldCount() === 0` before capturing each frame.
- `releaseRender(handle)` removes the hold. Throws if the handle does not exist.
- If `timeoutInMilliseconds` is set and the hold is not released in time, a
descriptive error is thrown (includes the label).
- `abortRender(message)` throws immediately and aborts the render.
- Built-in media components (`<Img>`, `<Video>`, `<AnimatedImage>`, `<IFrame>`)
already use this pattern internally — you only need it for custom async loading.
### Custom async loading pattern
```tsx
import { useEffect, useState } from 'react';
import { holdRender, releaseRender } from '@rendiv/core';
export const DataDriven: React.FC<{ apiUrl: string }> = ({ apiUrl }) => {
const [data, setData] = useState<Data | null>(null);
useEffect(() => {
const handle = holdRender('Fetching API data', { timeoutInMilliseconds: 10000 });
fetch(apiUrl)
.then((r) => r.json())
.then(setData)
.finally(() => releaseRender(handle));
}, [apiUrl]);
if (!data) return null;
return <Chart data={data} />;
};
```
## Environment Awareness
Components can detect the current environment to adapt behavior:
```ts
import { getRendivEnvironment, useRendivEnvironment } from '@rendiv/core';
const env = getRendivEnvironment();
// or inside a component:
const env = useRendivEnvironment();
// env is 'rendering' | 'player' | 'studio'
```
### Environment differences
| Behavior | Rendering | Player | Studio |
|---|---|---|---|
| `<Video>` | Seeks per frame | Plays naturally | Plays naturally |
| `<Audio>` | Returns `null` | Plays with sync | Plays with sync |
| Frame advance | Controlled by renderer | Real-time | Scrubber or play |
## `staticFile`
Returns a URL path for files in the project's `public/` directory:
```ts
import { staticFile } from '@rendiv/core';
const videoUrl = staticFile('background.mp4'); // returns '/background.mp4'
const imageUrl = staticFile('images/hero.png'); // returns '/images/hero.png'
```
Place media files in `public/` at the project root. `staticFile` prepends `/` and
strips any leading slash from the input.
## `getInputProps`
Receives data passed from the CLI or renderer to a composition at render time:
```ts
import { getInputProps } from '@rendiv/core';
interface MyProps {
title: string;
color: string;
}
const props = getInputProps<MyProps>();
// In the browser: reads window.__RENDIV_INPUT_PROPS__
// Returns {} if not set
```
Useful for parameterized renders where data is injected externally.
## Rendering Pipeline
The end-to-end rendering process:
1. **Bundle**: Vite builds the entry file into a static bundle.
Temp files (`__rendiv_entry__.jsx` + `__rendiv_entry__.html`) are written to
the project root (not `/tmp/` — Vite needs them in the project directory for
module resolution). Cleaned up in a `finally` block.
2. **Serve**: The bundled output is served as static files via a local HTTP server.
3. **Capture**: Playwright launches headless Chromium, navigates to the page, and
calls `__RENDIV_SET_FRAME__(n)` for each frame, then takes a PNG screenshot.
4. **Stitch**: FFmpeg combines all PNG frames into an MP4 or WebM video file.
If `<Audio>`, `<Video>`, or `<OffthreadVideo>` components registered audio
metadata, FFmpeg builds a filter graph to trim, delay, adjust tempo, and
mix all audio tracks into the output.
The renderer waits for `getPendingHoldCount() === 0` before capturing each frame,
which is why the `holdRender` pattern is critical for async resources.
## Render Profiling
Enable profiling to get per-frame timing breakdowns:
```bash
rendiv render src/index.tsx MyScene out/video.mp4 --profiling
```
The profiling summary shows:
- **Total frames** and overall render time (fps)
- **Phase breakdown** (average ms per frame):
- `React render` — `setFrame` + React reconciliation
- `Wait for holds` — waiting for `holdRender` releases (media loading)
- `Screenshot` — Playwright page screenshot
- **Bottleneck indicator** — highlights the slowest phase
### Performance tips
- Use `--image-format jpeg` for faster screenshots (vs PNG)
- Increase `--concurrency` to render frames in parallel browser tabs
- Use `--video-encoder h264_videotoolbox` (macOS) or `h264_nvenc` (NVIDIA) for
GPU-accelerated encoding
- Use `--preset ultrafast` for quick previews, `--preset slow` for final output
## Still Rendering
Capture a single frame as an image:
```bash
rendiv still src/index.tsx MyScene out/thumb.png --frame 45
```
Also available in Studio via the render modal — toggle to "Still" mode and
select the frame number (defaults to current playhead position).
rules/sequencing-and-timing.md
---
name: sequencing-and-timing
description: >
Time-shifting with Sequence, back-to-back playback with Series, looping with
Loop, and freezing frames with Freeze.
---
# Sequencing and Timing
## `useFrame()`
Returns the current frame number relative to the nearest enclosing `<Sequence>`.
At the top level (no Sequence), it returns the absolute composition frame.
```tsx
import { useFrame } from '@rendiv/core';
export const Counter: React.FC = () => {
const frame = useFrame();
return <div>{frame}</div>;
};
```
## `useCompositionConfig()`
Returns the composition's configuration:
```tsx
import { useCompositionConfig } from '@rendiv/core';
const { id, width, height, fps, durationInFrames } = useCompositionConfig();
```
## `<Sequence>`
Time-shifts its children to start at a specific frame. Children see frame 0 when
the parent's frame reaches `from`.
```tsx
import { Sequence } from '@rendiv/core';
<Sequence from={30} durationInFrames={60}>
<FadeIn /> {/* useFrame() returns 0 when parent is at frame 30 */}
</Sequence>
```
### Props
| Prop | Type | Default | Description |
|---|---|---|---|
| `from` | `number` | `0` | Parent frame at which this sequence begins |
| `durationInFrames` | `number` | `Infinity` | How many frames this sequence is visible |
| `name` | `string` | — | Display name in Studio timeline |
| `layout` | `'absolute-fill' \| 'none'` | `'absolute-fill'` | Wrapper layout |
| `style` | `CSSProperties` | — | Additional styles |
| `trackIndex` | `number` | `0` | Track for z-ordering. Lower = in front. See [timeline-overrides](timeline-overrides.md) |
| `playbackRate` | `number` | `1` | Playback speed multiplier (2 = double speed, 0.5 = half speed) |
| `premountFor` | `number` | `0` | Mount children N frames early so media can preload in the background |
### Layout modes
- `'absolute-fill'` (default): Wraps children in a `<Fill>` component (absolute positioning, fills parent).
- `'none'`: Renders children without any wrapper element.
### Visibility
A Sequence renders `null` (hides its children) when the current frame is before
`from` or after `from + durationInFrames`.
### Premounting
When `premountFor` is set, the Sequence mounts its children N frames before the
sequence becomes visible. During premount, children render invisibly (opacity 0,
pointer-events none) with a frozen timeline at the sequence start frame. This
allows media elements like `<Video>` and `<OffthreadVideo>` to preload in the
background, eliminating buffering when the sequence becomes visible.
```tsx
<Series>
<Series.Sequence durationInFrames={60}>
<IntroScene />
</Series.Sequence>
{/* Video starts loading 60 frames before it appears */}
<Series.Sequence durationInFrames={90} premountFor={60}>
<VideoScene />
</Series.Sequence>
</Series>
```
## `<Series>`
Plays `<Series.Sequence>` children back-to-back with automatically calculated start times.
```tsx
import { Series } from '@rendiv/core';
<Series>
<Series.Sequence durationInFrames={60}>
<TitleCard />
</Series.Sequence>
<Series.Sequence durationInFrames={90}>
<MainContent />
</Series.Sequence>
<Series.Sequence durationInFrames={45}>
<Outro />
</Series.Sequence>
</Series>
```
### `<Series.Sequence>` props
| Prop | Type | Default | Description |
|---|---|---|---|
| `durationInFrames` | `number` | — | Duration of this segment (required) |
| `offset` | `number` | `0` | Shift start time: positive = gap, negative = overlap |
| `name` | `string` | — | Display name |
| `layout` | `'absolute-fill' \| 'none'` | `'absolute-fill'` | Wrapper layout |
| `style` | `CSSProperties` | — | Additional styles |
| `trackIndex` | `number` | `0` | Track for z-ordering. Lower = in front. See [timeline-overrides](timeline-overrides.md) |
| `premountFor` | `number` | `0` | Mount children N frames early for media preloading |
### Constraints
- Only `<Series.Sequence>` elements are allowed as direct children of `<Series>`.
Any other element will throw an error.
- `<Series.Sequence>` MUST NOT be rendered outside a `<Series>` — it throws.
### Offset example
```tsx
<Series>
<Series.Sequence durationInFrames={60}>
<SceneA />
</Series.Sequence>
{/* 10-frame gap before SceneB */}
<Series.Sequence durationInFrames={60} offset={10}>
<SceneB />
</Series.Sequence>
{/* SceneC overlaps with SceneB's last 5 frames */}
<Series.Sequence durationInFrames={60} offset={-5}>
<SceneC />
</Series.Sequence>
</Series>
```
## `<Loop>`
Repeats children on a cycle using modulo arithmetic.
```tsx
import { Loop } from '@rendiv/core';
<Loop durationInFrames={30} times={4}>
<PulsingDot /> {/* Sees frames 0-29, repeated 4 times */}
</Loop>
```
### Props
| Prop | Type | Default | Description |
|---|---|---|---|
| `durationInFrames` | `number` | — | Duration of one iteration (required) |
| `times` | `number` | `Infinity` | Number of iterations |
| `layout` | `'absolute-fill' \| 'none'` | `'absolute-fill'` | Wrapper layout |
| `style` | `CSSProperties` | — | Additional styles |
Children always see a frame in `[0, durationInFrames)` via modulo. After all iterations
complete (when `times` is finite), the Loop renders `null`.
## `<Freeze>`
Locks children to a specific frame number, regardless of the actual playback position.
```tsx
import { Freeze } from '@rendiv/core';
<Freeze frame={20}>
<AnimatedScene /> {/* Always sees frame 20 */}
</Freeze>
```
### How it works
`<Freeze>` overrides `TimelineContext.frame` for all children. `useFrame()` inside
the frozen subtree always returns the frozen frame (adjusted for Sequence offset).
### Common patterns
```tsx
{/* Freeze at the end of an animation */}
<Sequence from={0} durationInFrames={30}>
<AnimatedTitle />
</Sequence>
<Sequence from={30} durationInFrames={60}>
<Freeze frame={29}>
<AnimatedTitle /> {/* Frozen at last frame of animation */}
</Freeze>
</Sequence>
```
## Context Override Architecture
`<Sequence>`, `<Loop>`, and `<Freeze>` all work by providing new `TimelineContext`
and/or `SequenceContext` values to their children. This is why `useFrame()` returns
a local frame — it subtracts the accumulated sequence offset from the timeline frame.
Nesting is composable: a `<Loop>` inside a `<Sequence>` inside another `<Sequence>`
applies all offsets correctly.
rules/shapes-and-paths.md
---
name: shapes-and-paths
description: >
Generating SVG shapes and manipulating SVG paths — parsing, measuring, morphing,
stroke reveal animations, and geometric transforms.
---
# Shapes and Paths
## @rendiv/shapes
Generates SVG path data for common shapes. Every function returns a `ShapeResult`:
```ts
interface ShapeResult {
d: string; // SVG path d attribute
width: number; // Natural width
height: number; // Natural height
viewBox: string; // "0 0 width height"
}
```
### Shape Functions
```ts
import {
shapeCircle,
shapeEllipse,
shapeRect,
shapeTriangle,
shapePolygon,
shapeStar,
shapePie,
} from '@rendiv/shapes';
```
| Function | Parameters | Notes |
|---|---|---|
| `shapeCircle({ radius })` | `radius > 0` | Two semicircular arcs |
| `shapeEllipse({ rx, ry })` | `rx > 0, ry > 0` | Two semicircular arcs |
| `shapeRect({ width, height, roundness? })` | `width > 0, height > 0`, `roundness` default 0 | Roundness clamped to `min(roundness, width/2, height/2)` |
| `shapeTriangle({ length, direction? })` | `length > 0`, direction: `'up'`\|`'down'`\|`'left'`\|`'right'` (default `'up'`) | Equilateral |
| `shapePolygon({ radius, sides })` | `radius > 0, sides >= 3` | Regular, first vertex at 12 o'clock |
| `shapeStar({ innerRadius, outerRadius, points })` | Both radii > 0, `points >= 3` | Alternating vertices |
| `shapePie({ radius, startAngle, endAngle, closePath? })` | `radius > 0`, angles in degrees (0 = 12 o'clock, clockwise), `closePath` default `true` | Arc segment |
### Example: Animated shape
```tsx
import { shapeCircle } from '@rendiv/shapes';
const circle = shapeCircle({ radius: 50 });
<svg viewBox={circle.viewBox} width={circle.width} height={circle.height}>
<path d={circle.d} fill="cyan" />
</svg>
```
## @rendiv/paths
Parses, measures, and transforms SVG path strings.
### Parsing
```ts
import { readPath, writePath } from '@rendiv/paths';
const segments = readPath('M 10 10 L 90 90 Z');
const d = writePath(segments);
```
`readPath` converts all commands to absolute coordinates. `writePath` serializes
back with values rounded to 3 decimal places.
### Measurement
```ts
import { pathLength, pointOnPath, tangentOnPath, slicePath } from '@rendiv/paths';
```
| Function | Returns | Description |
|---|---|---|
| `pathLength(d)` | `number` | Total path length |
| `pointOnPath(d, length)` | `{ x, y, angle }` | Point and tangent angle at a given length |
| `tangentOnPath(d, length)` | `{ x, y }` | Normalized tangent vector |
| `slicePath(d, start, end)` | `string` | Sub-path between two lengths |
### Animation
#### `strokeReveal(progress, d)`
Animates a "draw-on" line effect.
```ts
import { strokeReveal } from '@rendiv/paths';
const progress = interpolate(frame, [0, 60], [0, 1], { extrapolateRight: 'clamp' });
const reveal = strokeReveal(progress, pathD);
<path
d={pathD}
stroke="white"
strokeWidth={3}
fill="none"
style={{
strokeDasharray: reveal.strokeDasharray,
strokeDashoffset: reveal.strokeDashoffset,
}}
/>
```
- `progress = 0`: invisible
- `progress = 1`: fully drawn
- Clamped to [0, 1]
#### `morphPath(progress, from, to)`
Interpolates between two SVG paths.
```ts
import { morphPath } from '@rendiv/paths';
const morphed = morphPath(progress, circleD, starD);
<path d={morphed} fill="white" />
```
**Constraint**: Both paths MUST have the same number of segments with matching
command types. Throws otherwise.
### Transforms
```ts
import { resizePath, movePath, flipPath, pathBounds } from '@rendiv/paths';
```
| Function | Description |
|---|---|
| `resizePath(d, scaleX, scaleY?)` | Scale path. Uniform if `scaleY` omitted. |
| `movePath(d, dx, dy)` | Translate all points |
| `flipPath(d)` | Reverse path direction |
| `pathBounds(d)` | Bounding box: `{ x, y, width, height }` |
### Combined example: Morphing shapes
```tsx
import { shapeCircle, shapeStar } from '@rendiv/shapes';
import { morphPath } from '@rendiv/paths';
const circle = shapeCircle({ radius: 60 });
const star = shapeStar({ innerRadius: 30, outerRadius: 60, points: 5 });
const frame = useFrame();
const progress = interpolate(frame, [0, 45], [0, 1], {
easing: Easing.easeInOut,
extrapolateRight: 'clamp',
});
const d = morphPath(progress, circle.d, star.d);
<svg viewBox={circle.viewBox} width={200} height={200}>
<path d={d} fill="gold" />
</svg>
```
Note: For `morphPath` to work, both shapes must produce compatible path segments.
Shapes from `@rendiv/shapes` with the same structural complexity (e.g., two
arc-based shapes) work well together.
rules/text-animation.md
---
name: text-animation
description: >
Animated text with per-character, per-word, or per-line splitting, staggered
entrances, and built-in animation presets using @rendiv/text.
---
# Text Animation
## @rendiv/text
Split text into individual units (characters, words, or lines) and animate each
with staggered timing. Built on `useFrame()` and `interpolate()` from `@rendiv/core`.
```tsx
import { AnimatedText, slideUp } from '@rendiv/text';
<AnimatedText
text="Hello World"
splitBy="word"
animation={slideUp({ distance: 30, durationInFrames: 20 })}
stagger={5}
style={{ fontSize: 48, color: '#58a6ff', fontFamily: 'system-ui' }}
/>
```
## `<AnimatedText>` Component
### Props
| Prop | Type | Default | Description |
|---|---|---|---|
| `text` | `string` | required | The text to animate |
| `splitBy` | `'character' \| 'word' \| 'line'` | `'character'` | How to split the text into animation units |
| `animation` | `TextAnimationConfig` | required | Animation config (use a preset or custom) |
| `stagger` | `number` | `3` | Delay in frames between each unit starting |
| `style` | `CSSProperties` | — | Styles applied to the outer wrapper |
| `className` | `string` | — | CSS class for the outer wrapper |
### How it works
1. Text is split into units via `splitText(text, splitBy)`
2. Each unit renders as `<span style="display: inline-block">` (whitespace units stay inline)
3. Per-unit progress is computed: `interpolate(frame - i * stagger, [0, durationInFrames], [0, 1], { clamp })`
4. The animation's `style(progress, index, total)` function returns CSS for each unit
## Animation Presets
All presets are factory functions returning `TextAnimationConfig`.
| Preset | Effect | Key options |
|---|---|---|
| `fadeIn()` | Opacity 0→1 | `durationInFrames` |
| `slideUp()` | Translate up + fade | `distance`, `durationInFrames` |
| `slideDown()` | Translate down + fade | `distance`, `durationInFrames` |
| `slideLeft()` | Translate left + fade | `distance`, `durationInFrames` |
| `slideRight()` | Translate right + fade | `distance`, `durationInFrames` |
| `scaleIn()` | Scale up + fade | `from` (start scale), `durationInFrames` |
| `bounce()` | Spring-based bounce in | `fps`, `durationInFrames` |
| `typewriter()` | Discrete character reveal | `durationInFrames` |
| `scramble()` | Random chars resolve to final text | `characters`, `durationInFrames` |
| `blurIn()` | Deblur + fade | `from` (start blur px), `durationInFrames` |
| `rotateIn()` | Rotate + fade | `degrees`, `durationInFrames` |
### Preset examples
```tsx
import { AnimatedText, bounce, typewriter, scramble, blurIn } from '@rendiv/text';
// Bouncy characters
<AnimatedText
text="Bouncy!"
animation={bounce({ fps: 30 })}
stagger={2}
style={{ fontSize: 48, color: '#f78166' }}
/>
// Typewriter
<AnimatedText
text="Typing..."
animation={typewriter()}
stagger={3}
style={{ fontSize: 36, fontFamily: 'monospace' }}
/>
// Scramble decode
<AnimatedText
text="DECODED"
animation={scramble({ durationInFrames: 20 })}
stagger={2}
style={{ fontSize: 44, letterSpacing: 4 }}
/>
// Blur reveal by word
<AnimatedText
text="Blur Reveal"
splitBy="word"
animation={blurIn({ from: 12, durationInFrames: 25 })}
stagger={8}
style={{ fontSize: 48 }}
/>
```
## Custom Animations
Create a custom `TextAnimationConfig` for full control:
```tsx
import { AnimatedText } from '@rendiv/text';
import type { TextAnimationConfig } from '@rendiv/text';
const customWave: TextAnimationConfig = {
durationInFrames: 20,
style: (progress, index, total) => ({
opacity: progress,
transform: `translateY(${Math.sin(progress * Math.PI * 2) * -15}px)`,
}),
};
<AnimatedText text="Wave Motion" animation={customWave} stagger={2} />
```
### Text content override
For effects that change the displayed text (like `scramble`), use `renderText`:
```tsx
const reveal: TextAnimationConfig = {
durationInFrames: 10,
style: (progress) => ({ opacity: 1 }),
renderText: (original, progress) => progress >= 1 ? original : '_',
};
```
## Utilities
### `splitText(text, mode)`
Splits text into `SplitUnit[]` objects with `{ text, index, isWhitespace }`.
```ts
import { splitText } from '@rendiv/text';
splitText('Hello World', 'character'); // 11 units (including space)
splitText('Hello World', 'word'); // 3 units: "Hello", " ", "World"
splitText('Line 1\nLine 2', 'line'); // 2 units
```
### `stagger(count, delayFrames)`
Calculates total extra frames needed for staggered animation. Useful for sizing
a `<Sequence>` to fit the full animation:
```ts
import { stagger, splitText } from '@rendiv/text';
const units = splitText('Hello', 'character');
const totalExtraFrames = stagger(units.length, 3); // (5 - 1) * 3 = 12
// Total animation duration = preset durationInFrames + totalExtraFrames
```
## Tips
- Use `splitBy="word"` with higher `stagger` (5-10) for clean title reveals
- Use `splitBy="character"` with low `stagger` (2-3) for kinetic text effects
- Combine with `<Sequence>` to time text animations within a composition
- The `bounce` preset uses `spring()` from `@rendiv/core` — pass `fps` matching
your composition's fps for accurate physics
rules/three.md
# 3D Scenes — @rendiv/three
Embed frame-accurate 3D scenes in rendiv compositions using
[React Three Fiber](https://docs.pmnd.rs/react-three-fiber) (R3F) and
[Three.js](https://threejs.org/).
## Installation
```bash
pnpm add @rendiv/three three @react-three/fiber
```
Peer dependencies: `react`, `react-dom`, `@rendiv/core`, `three`, `@react-three/fiber`.
## Basic Usage
```tsx
import React, { useRef } from 'react';
import { useFrame, interpolate, Fill } from '@rendiv/core';
import { ThreeCanvas } from '@rendiv/three';
import * as THREE from 'three';
function SpinningCube(): React.ReactElement {
const meshRef = useRef<THREE.Mesh>(null);
const frame = useFrame(); // rendiv's useFrame — returns the frame number
if (meshRef.current) {
meshRef.current.rotation.y = interpolate(frame, [0, 90], [0, Math.PI * 2]);
}
return (
<mesh ref={meshRef}>
<boxGeometry args={[2, 2, 2]} />
<meshStandardMaterial color="#6bd4ff" />
</mesh>
);
}
export function My3DScene(): React.ReactElement {
return (
<Fill style={{ backgroundColor: '#0a0a1a' }}>
<ThreeCanvas camera={{ position: [0, 2, 5], fov: 50 }} style={{ width: 1920, height: 1080 }}>
<ambientLight intensity={0.5} />
<directionalLight position={[5, 5, 5]} />
<SpinningCube />
</ThreeCanvas>
</Fill>
);
}
```
## `<ThreeCanvas>` Props
| Prop | Type | Default | Description |
|---|---|---|---|
| `children` | `ReactNode` | (required) | R3F scene graph (meshes, lights, helpers, etc.) |
| `style` | `CSSProperties` | — | Container style. Set `width`/`height` to match composition dimensions. |
| `className` | `string` | — | Container class name. |
| `camera` | R3F camera config | — | Pass-through to R3F `<Canvas camera={...}>`. |
| `gl` | R3F gl config | — | Pass-through to R3F `<Canvas gl={...}>`. |
## Critical: `useFrame` Naming Conflict
Both rendiv and R3F export a hook called `useFrame`, but they do completely
different things:
| Hook | Source | Returns | Use in rendiv? |
|---|---|---|---|
| `useFrame()` | `@rendiv/core` | Current frame number (`number`) | **YES** — use this one |
| `useFrame()` | `@react-three/fiber` | Callback-based animation loop | **NO** — runs on wall-clock time, will desync during rendering |
**Always import `useFrame` from `@rendiv/core`** inside rendiv compositions.
Never use R3F's `useFrame` — it is time-based and does not respect rendiv's
frame-by-frame rendering pipeline.
```tsx
// CORRECT
import { useFrame } from '@rendiv/core';
// WRONG — will desync during rendering
import { useFrame } from '@react-three/fiber';
```
## How It Works
### Context bridging
R3F's `<Canvas>` creates a **separate React reconciler** (its own React tree).
This means rendiv's contexts (`TimelineContext`, `SequenceContext`, etc.) are not
automatically available inside the Canvas.
`<ThreeCanvas>` solves this by:
1. Reading all rendiv contexts **outside** the Canvas.
2. Re-providing them **inside** the Canvas via `<RendivContextBridge>`.
This means `useFrame()`, `useCompositionConfig()`, and all other rendiv hooks
work normally inside `<ThreeCanvas>` children.
### Render mode
During rendering (`rendiv render`):
- `frameloop` is set to `"never"` — R3F does not run its own animation loop.
- An internal `<FrameAdvancer>` component calls `advance()` whenever the rendiv
frame changes, ensuring the 3D scene updates exactly once per captured frame.
- `holdRender()` is called on mount and released once the Canvas is created.
During preview/studio:
- `frameloop` is set to `"always"` for smooth interactive playback.
## Animating 3D Objects
Drive all animations from `useFrame()` (rendiv) + `interpolate()` / `spring()`:
```tsx
import { useRef } from 'react';
import { useFrame, useCompositionConfig, interpolate, spring } from '@rendiv/core';
import * as THREE from 'three';
function AnimatedSphere(): React.ReactElement {
const meshRef = useRef<THREE.Mesh>(null);
const frame = useFrame();
const { fps } = useCompositionConfig();
const y = interpolate(frame, [0, 60], [-2, 2]);
const scale = spring({ frame, fps, config: { damping: 10, stiffness: 80 } });
if (meshRef.current) {
meshRef.current.position.y = y;
meshRef.current.scale.setScalar(scale);
}
return (
<mesh ref={meshRef}>
<sphereGeometry args={[1, 32, 32]} />
<meshStandardMaterial color="#ff6b6b" />
</mesh>
);
}
```
## Using With Sequences
```tsx
import { Sequence } from '@rendiv/core';
import { ThreeCanvas } from '@rendiv/three';
export function Staged3D(): React.ReactElement {
return (
<ThreeCanvas camera={{ position: [0, 0, 10] }} style={{ width: 1920, height: 1080 }}>
<ambientLight />
<Sequence from={0} durationInFrames={60}>
<SpinningCube />
</Sequence>
<Sequence from={60} durationInFrames={60}>
<AnimatedSphere />
</Sequence>
</ThreeCanvas>
);
}
```
Sequences work inside `<ThreeCanvas>` because the context bridge ensures
`SequenceContext` is available to children.
## Tips
- **Set explicit dimensions** on `<ThreeCanvas>` via `style` to match your composition's `width`/`height`.
- **Mutate refs directly** instead of using React state for per-frame updates — this avoids re-renders for every frame.
- **Use `@react-three/drei`** for additional helpers (OrbitControls, Text3D, etc.) — install as a separate dependency.
- **Avoid R3F's time-based hooks** (`useFrame` from fiber, `Clock`). Derive all motion from rendiv's frame number.
rules/timeline-overrides.md
---
name: timeline-overrides
description: >
Control z-ordering with trackIndex, position and scale sequences with
x/y/scaleX/scaleY overrides, persist timeline edits in
timeline-overrides.json, and use overrides in headless rendering.
---
# Timeline Overrides and Track Z-Ordering
## `trackIndex` prop
Every `<Sequence>` has a `trackIndex` prop that controls stacking order when
sequences overlap. Lower values render in front.
```tsx
import { Sequence } from '@rendiv/core';
{/* trackIndex 0 = frontmost (default) */}
<Sequence from={0} durationInFrames={90} trackIndex={0} name="Foreground">
<ForegroundScene />
</Sequence>
{/* trackIndex 1 = behind track 0 */}
<Sequence from={30} durationInFrames={90} trackIndex={1} name="Background">
<BackgroundScene />
</Sequence>
```
### How z-index is computed
`zIndex = 10000 - trackIndex`. Track 0 gets `zIndex: 10000`, track 1 gets
`zIndex: 9999`, and so on. The z-index is applied to the `<Fill>` wrapper
when `layout` is `'absolute-fill'` (the default).
### Default behavior
`trackIndex` defaults to `0`. All sequences render at `zIndex: 10000` unless
explicitly assigned to different tracks.
### Works in `<Series.Sequence>` too
```tsx
import { Series } from '@rendiv/core';
<Series>
<Series.Sequence durationInFrames={60} offset={-15} trackIndex={0}>
<SceneA /> {/* Overlaps with SceneB, renders in front */}
</Series.Sequence>
<Series.Sequence durationInFrames={60} trackIndex={1}>
<SceneB /> {/* Renders behind SceneA during overlap */}
</Series.Sequence>
</Series>
```
## `timeline-overrides.json`
Timeline overrides persist modifications to sequence timing, track assignment,
position, and scale. The file lives at the **project root**
(`timeline-overrides.json`) and survives Studio server restarts.
### File format
```json
{
"CompositionId/SequenceName[from]": {
"from": 10,
"durationInFrames": 60,
"trackIndex": 1,
"playbackRate": 2,
"x": 100,
"y": 50,
"scaleX": 0.5,
"scaleY": 0.5
}
}
```
All fields are optional. Only include the fields you want to override.
Each key is a **namePath** — a hierarchical identifier built from the composition
ID (or `<CanvasElement>` scope), sequence names, and their `from` values. For
nested sequences the path segments are joined with `/`:
```
CompositionId/OuterSequence[0]/InnerSequence[30]
```
### `<CanvasElement>` and override scoping
The namePath prefix comes from `<CanvasElement id="...">` when present, otherwise
from the rendering `<Composition>` id. **Always wrap your composition content with
`<CanvasElement>`** so that overrides are self-contained and work correctly when the
composition is nested inside another "master" composition.
Without `<CanvasElement>`, nesting a child composition inside a master changes the
namePath prefix from the child's ID to the master's ID, causing all overrides to
silently miss.
```tsx
// Self-contained — overrides work when nested
export function MyScene() {
return (
<CanvasElement id="MyScene">
<Series>
<Series.Sequence durationInFrames={60} name="Intro">
<IntroScene />
</Series.Sequence>
</Series>
</CanvasElement>
);
}
// Can be used standalone or nested — overrides always apply
function MasterComp() {
return (
<CanvasElement id="MasterComp">
<Series>
<Series.Sequence durationInFrames={300}>
<MyScene /> {/* MyScene's overrides still use "MyScene/..." prefix */}
</Series.Sequence>
</Series>
</CanvasElement>
);
}
```
### Override precedence
When both a prop and an override exist, the **override wins**:
1. `trackIndex` prop on `<Sequence>` provides the base value (default `0`)
2. If `timeline-overrides.json` has an entry for this sequence's namePath with a
`trackIndex` field, it replaces the prop value
3. The final `trackIndex` is converted to `zIndex = 10000 - trackIndex`
The same precedence applies to `from`, `durationInFrames`, `playbackRate`, `x`,
`y`, `scaleX`, and `scaleY`.
### How overrides are created
- **Studio timeline**: Drag sequences in the timeline editor to change their
start frame, duration, or track. Changes are saved to `timeline-overrides.json`
automatically.
- **Studio position mode**: Toggle Position Mode (press `P` or click the
"Position" button) in the preview panel. Drag a sequence body to reposition it,
or drag corner handles to scale. Shift-drag for proportional scaling. Dragging
past the anchor flips the content (negative scale). Click "Reset" on a
sequence to clear its position and scale overrides.
- **Manual**: Edit `timeline-overrides.json` directly. Use the namePath format
shown above as keys.
### Headless rendering
The bundler reads `timeline-overrides.json` at build time and embeds the data
into the render bundle. Overrides apply automatically during `rendiv render` —
no extra flags needed.
```bash
# Overrides from timeline-overrides.json are included in the render
rendiv render src/index.tsx MyComposition out/video.mp4
```
### Clearing overrides
- **Studio timeline**: Click "Reset All" in the timeline toolbar, or right-click
a block and choose "Reset Position".
- **Studio position mode**: Hover a sequence and click "Reset" to clear its
position and scale while keeping timing overrides.
- **Manual**: Delete `timeline-overrides.json` or remove specific entries.
## Playback rate overrides
The `playbackRate` field controls how fast a sequence's children advance through
frames. A rate of `2` means children see frames at double speed; `0.5` means
half speed. Nested playback rates compound — a 2x sequence inside a 1.5x parent
runs at 3x effective rate.
```json
{
"MyComp/VideoScene[60]": {
"from": 60,
"durationInFrames": 90,
"playbackRate": 1.5
}
}
```
Playback rate affects both visual rendering and audio. During rendering, audio
tracks from `<Video>`, `<OffthreadVideo>`, and `<Audio>` components are
automatically tempo-adjusted by FFmpeg to match the effective playback rate.
You can also set playback rate in code via the `playbackRate` prop on
`<Sequence>`:
```tsx
<Sequence from={60} durationInFrames={90} playbackRate={1.5}>
<VideoScene />
</Sequence>
```
## Position and scale overrides
The `x`, `y`, `scaleX`, and `scaleY` fields offset and resize a sequence
relative to the composition. They are applied as a CSS
`transform: translate(x, y) scale(sX, sY)` with `transform-origin: 0 0`
on the sequence's wrapper element.
### Override fields
| Field | Type | Default | Description |
|---|---|---|---|
| `x` | number | `0` | Horizontal offset in composition pixels |
| `y` | number | `0` | Vertical offset in composition pixels |
| `scaleX` | number | `1` | Horizontal scale factor (1 = 100%) |
| `scaleY` | number | `1` | Vertical scale factor (1 = 100%) |
### Negative scale (flipping)
Negative values flip the content. `scaleX: -1` mirrors horizontally,
`scaleY: -1` mirrors vertically. In Studio position mode, drag a corner
handle past the opposite anchor to flip.
### Manual override example
```json
{
"MyComp/Webcam[0]": {
"from": 0,
"durationInFrames": 150,
"trackIndex": 0,
"x": 1400,
"y": 750,
"scaleX": 0.3,
"scaleY": 0.3
},
"MyComp/Background[0]": {
"from": 0,
"durationInFrames": 150,
"trackIndex": 1,
"scaleX": -1
}
}
```
The first entry places a webcam overlay at the bottom-right corner scaled to
30%. The second flips the background horizontally.
## Overlap patterns
### Crossfade with z-ordering
```tsx
<Sequence from={0} durationInFrames={60} trackIndex={1} name="SceneA">
<SceneA />
</Sequence>
<Sequence from={45} durationInFrames={60} trackIndex={0} name="SceneB">
<SceneB /> {/* Renders in front during the 15-frame overlap */}
</Sequence>
```
### Picture-in-picture (via overrides)
Define both sequences at full size in code, then use timeline overrides to
position and scale the PiP layer:
```tsx
<Sequence from={0} durationInFrames={150} trackIndex={1} name="Main">
<MainVideo />
</Sequence>
<Sequence from={30} durationInFrames={90} trackIndex={0} name="PiP">
<PipVideo />
</Sequence>
```
In `timeline-overrides.json`:
```json
{
"MyComp/PiP[30]": {
"x": 1400,
"y": 750,
"scaleX": 0.25,
"scaleY": 0.25
}
}
```
Or use Studio position mode to drag and resize the PiP layer visually.
rules/transitions.md
---
name: transitions
description: >
Scene transitions using TransitionSeries with timing functions (linear, spring)
and visual presentations (fade, slide, wipe, flip, clockWipe).
---
# Transitions
The `@rendiv/transitions` package provides overlapping scene transitions via
`TransitionSeries` — a variant of `<Series>` that supports transition elements
between sequences.
```
npm install @rendiv/transitions
```
## `TransitionSeries`
A compound component with three parts:
```tsx
import {
TransitionSeries,
linearTiming,
fade,
} from '@rendiv/transitions';
<TransitionSeries>
<TransitionSeries.Sequence durationInFrames={60}>
<SceneA />
</TransitionSeries.Sequence>
<TransitionSeries.Transition
timing={linearTiming({ durationInFrames: 15 })}
presentation={fade()}
/>
<TransitionSeries.Sequence durationInFrames={60}>
<SceneB />
</TransitionSeries.Sequence>
</TransitionSeries>
```
### How it works
A `<TransitionSeries.Transition>` causes the previous and next sequences to
overlap by `timing.durationInFrames`. During the overlap:
- The exiting sequence receives CSS from `presentation.style(progress).exiting`
- The entering sequence receives CSS from `presentation.style(progress).entering`
- `progress` goes from 0 (transition start) to 1 (transition end)
## Timing Functions
### `linearTiming`
Linear progression from 0 to 1.
```ts
import { linearTiming } from '@rendiv/transitions';
linearTiming({ durationInFrames: 20 })
```
### `springTiming`
Physics-based spring progression.
```ts
import { springTiming } from '@rendiv/transitions';
springTiming({
fps: 30, // required
config: { damping: 12, stiffness: 100 }, // optional SpringConfig
durationInFrames: 25, // optional, auto-calculated if omitted
})
```
If `durationInFrames` is omitted, it is calculated via `getSpringDuration()`.
## Presentations
### `fade()`
Cross-fade between scenes.
```ts
import { fade } from '@rendiv/transitions';
// entering: { opacity: progress }
// exiting: { opacity: 1 - progress }
```
### `slide({ direction? })`
Slide the entering scene in from an edge.
```ts
import { slide } from '@rendiv/transitions';
slide({ direction: 'from-left' })
// Directions: 'from-left' | 'from-right' | 'from-top' | 'from-bottom'
// Default: 'from-right'
```
Uses `translateX` / `translateY` percentage transforms.
### `wipe({ direction? })`
Wipe-reveal the entering scene using CSS `clip-path: inset()`.
```ts
import { wipe } from '@rendiv/transitions';
wipe({ direction: 'from-left' })
// Directions: 'from-left' | 'from-right' | 'from-top' | 'from-bottom'
// Default: 'from-left'
```
### `flip({ direction?, perspective? })`
3D flip transition.
```ts
import { flip } from '@rendiv/transitions';
flip({ direction: 'horizontal', perspective: 1000 })
// Directions: 'horizontal' | 'vertical'. Default: 'horizontal'
// perspective default: 1000
```
First half: exiting scene rotates 0° → 90°. Second half: entering scene rotates
90° → 0°.
### `clockWipe({ segments? })`
Clockwise radial wipe starting from 12 o'clock.
```ts
import { clockWipe } from '@rendiv/transitions';
clockWipe({ segments: 64 })
// segments default: 64 (polygon resolution)
```
Uses `clip-path: polygon()` to sweep a wedge clockwise.
## Full Example
```tsx
import {
TransitionSeries,
springTiming,
slide,
fade,
wipe,
} from '@rendiv/transitions';
const { fps } = useCompositionConfig();
<TransitionSeries>
<TransitionSeries.Sequence durationInFrames={90}>
<IntroScene />
</TransitionSeries.Sequence>
<TransitionSeries.Transition
timing={springTiming({ fps, config: { damping: 15 } })}
presentation={slide({ direction: 'from-right' })}
/>
<TransitionSeries.Sequence durationInFrames={120}>
<MainScene />
</TransitionSeries.Sequence>
<TransitionSeries.Transition
timing={linearTiming({ durationInFrames: 20 })}
presentation={fade()}
/>
<TransitionSeries.Sequence durationInFrames={60}>
<OutroScene />
</TransitionSeries.Sequence>
</TransitionSeries>
```
rules/typography.md
---
name: typography
description: >
Loading and using Google Fonts and local font files in rendiv compositions,
with automatic render-hold to prevent FOUT during rendering.
---
# Typography
Both font packages use `holdRender` internally — the renderer waits for fonts
to load before capturing frames, preventing blank or fallback-font frames.
## @rendiv/google-fonts
Load any Google Font by name.
```tsx
import { useFont } from '@rendiv/google-fonts';
export const Title: React.FC = () => {
const fontFamily = useFont({ family: 'Space Grotesk', weight: '700' });
return (
<h1 style={{ fontFamily, fontSize: 80 }}>
Hello Rendiv
</h1>
);
};
```
### `useFont` (hook)
```ts
useFont(options: {
family: string; // Google Font name (required)
weight?: string | number; // default: '400'
style?: 'normal' | 'italic'; // default: 'normal'
display?: FontDisplay; // default: 'block'
subsets?: string[]; // default: ['latin']
text?: string; // only load glyphs for this text
}): string // returns CSS fontFamily value, e.g. '"Space Grotesk", sans-serif'
```
### `fetchFont` (imperative)
```ts
import { fetchFont } from '@rendiv/google-fonts';
const { fontFamily, cleanup } = await fetchFont({
family: 'Roboto',
weight: '400',
});
// Use fontFamily in styles
// Call cleanup() when done
```
### `buildGoogleFontsUrl`
```ts
import { buildGoogleFontsUrl } from '@rendiv/google-fonts';
const url = buildGoogleFontsUrl({ family: 'Inter', weight: '500' });
// Returns a Google Fonts CSS v2 URL
```
### Behavior
- Injects a `<link>` stylesheet for the font
- Waits for the font to load via `document.fonts.load()`
- Uses `holdRender` with a 30-second timeout
- Removes the `<link>` on unmount (hook) or when `cleanup()` is called (imperative)
## @rendiv/fonts
Load local font files (WOFF2, WOFF, TTF, OTF).
```tsx
import { useLocalFont } from '@rendiv/fonts';
import { staticFile } from '@rendiv/core';
export const CustomTitle: React.FC = () => {
const fontFamily = useLocalFont({
family: 'MyCustomFont',
src: staticFile('fonts/my-font.woff2'),
});
return <h1 style={{ fontFamily }}>Custom Typography</h1>;
};
```
### `useLocalFont` (hook)
```ts
useLocalFont(options: {
family: string; // Font family name to register (required)
src: string; // URL or path to font file (required)
format?: 'woff2' | 'woff' | 'truetype' | 'opentype'; // auto-detected from extension
weight?: string | number; // default: '400'
style?: 'normal' | 'italic'; // default: 'normal'
display?: FontDisplay; // default: 'block'
unicodeRange?: string; // optional Unicode range
}): string // returns CSS fontFamily value
```
### `fetchLocalFont` (imperative)
```ts
import { fetchLocalFont } from '@rendiv/fonts';
const { fontFamily, cleanup } = await fetchLocalFont({
family: 'BrandFont',
src: '/fonts/brand.woff2',
});
```
### Behavior
- Creates a `FontFace` object and adds it to `document.fonts`
- Waits for the font to load
- Uses `holdRender` with a 30-second timeout
- Calls `cleanup()` / unmount removes the font from `document.fonts`
## Font Display Values
| Value | Behavior |
|---|---|
| `'block'` | Short block period, infinite swap (default, best for rendering) |
| `'swap'` | Minimal block, infinite swap |
| `'fallback'` | Short block, short swap |
| `'optional'` | Minimal block, no swap |
| `'auto'` | Browser default |
For video rendering, `'block'` is recommended — the `holdRender` mechanism ensures
the font loads before any frame is captured anyway.
## Multiple weights / styles
Load multiple variants by calling the hook multiple times:
```tsx
const regular = useFont({ family: 'Inter', weight: '400' });
const bold = useFont({ family: 'Inter', weight: '700' });
const italic = useFont({ family: 'Inter', weight: '400', style: 'italic' });
```
Each call loads its variant independently with its own `holdRender`.
rules/visual-effects.md
---
name: visual-effects
description: >
Composable CSS filter effects and visual presets using @rendiv/effects —
blur, glow, glitch, vignette, chromatic aberration, and more.
---
# Visual Effects
## @rendiv/effects
Apply animated CSS filters to any content. Filters are composable and
frame-driven via `useFrame()`.
```tsx
import { Effect, blur, brightness } from '@rendiv/effects';
import { interpolate, useFrame } from '@rendiv/core';
<Effect filters={[
blur((frame) => interpolate(frame, [0, 30], [10, 0], { extrapolateRight: 'clamp' })),
brightness(1.2),
]}>
<MyContent />
</Effect>
```
## `<Effect>` Component
Wraps children in a `<div>` with a composed CSS `filter` string.
### Props
| Prop | Type | Description |
|---|---|---|
| `filters` | `FilterConfig[]` | Array of filter configs to compose |
| `style` | `CSSProperties` | Additional styles on the wrapper div |
| `className` | `string` | CSS class for the wrapper div |
| `children` | `ReactNode` | Content to apply filters to |
## Filter Factories
Each factory returns a `FilterConfig`. All accept either a static value or
a function `(frame: number) => value` for animation.
| Factory | CSS function | Value type |
|---|---|---|
| `blur(px)` | `blur(Npx)` | `number` — pixels |
| `brightness(n)` | `brightness(N)` | `number` — 1 = normal |
| `contrast(n)` | `contrast(N)` | `number` — 1 = normal |
| `saturate(n)` | `saturate(N)` | `number` — 1 = normal |
| `hueRotate(deg)` | `hue-rotate(Ndeg)` | `number` — degrees |
| `grayscale(n)` | `grayscale(N)` | `number` — 0-1 |
| `sepia(n)` | `sepia(N)` | `number` — 0-1 |
| `invert(n)` | `invert(N)` | `number` — 0-1 |
| `opacity(n)` | `opacity(N)` | `number` — 0-1 |
| `dropShadow(config)` | `drop-shadow(...)` | `{ x, y, blur, color }` — all animatable |
### Static vs animated values
```tsx
// Static: constant filter
blur(5)
// Animated: filter changes per frame
blur((frame) => interpolate(frame, [0, 60], [10, 0], { extrapolateRight: 'clamp' }))
// Animated drop shadow
dropShadow({
x: 0,
y: (frame) => interpolate(frame, [0, 30], [0, 10]),
blur: 8,
color: '#000000',
})
```
## Filter-Only Presets
These return `FilterConfig[]` and are used directly with `<Effect>`.
### `glowEffect(options?)`
Brightness boost + layered drop shadows for a glow look.
```tsx
import { Effect, glowEffect } from '@rendiv/effects';
<Effect filters={glowEffect({ color: '#f78166', intensity: 1.3, blur: 12 })}>
<Text />
</Effect>
```
| Option | Type | Default | Description |
|---|---|---|---|
| `color` | `string` | `'#ffffff'` | Glow color |
| `intensity` | `number` | `1.2` | Brightness multiplier |
| `blur` | `number` | `10` | Glow radius in pixels |
### `vintageEffect(options?)`
Sepia + desaturation + slight blur for a vintage film look.
```tsx
<Effect filters={vintageEffect({ intensity: 0.8 })}>
<Scene />
</Effect>
```
| Option | Type | Default | Description |
|---|---|---|---|
| `intensity` | `number` | `1` | Effect strength (0-1) |
### `nightVisionEffect(options?)`
Green tint + boosted brightness and contrast.
```tsx
<Effect filters={nightVisionEffect({ intensity: 1 })}>
<Scene />
</Effect>
```
| Option | Type | Default | Description |
|---|---|---|---|
| `intensity` | `number` | `1` | Effect strength |
## Component Presets
These need extra DOM elements (overlays, multiple layers) and export as
standalone components.
### `<VignetteEffect>`
Radial gradient overlay that darkens edges.
```tsx
import { VignetteEffect } from '@rendiv/effects';
<VignetteEffect intensity={0.7}>
<Scene />
</VignetteEffect>
```
| Prop | Type | Default | Description |
|---|---|---|---|
| `intensity` | `number` | `0.6` | Darkness at edges (0-1) |
| `style` | `CSSProperties` | — | Wrapper styles |
### `<GlitchEffect>`
RGB channel splitting with clip-path slicing. Deterministic per frame (safe
for rendering).
```tsx
import { GlitchEffect } from '@rendiv/effects';
<GlitchEffect intensity={0.6} seed={42}>
<Title />
</GlitchEffect>
```
| Prop | Type | Default | Description |
|---|---|---|---|
| `intensity` | `number` | `1` | Glitch strength |
| `seed` | `number` | `42` | Random seed for reproducible glitch pattern |
| `style` | `CSSProperties` | — | Wrapper styles |
**Note:** Renders children 3 times (base + 2 offset layers). Avoid using with
heavy or stateful children.
### `<ChromaEffect>`
Chromatic aberration — RGB channel separation with slight position offsets.
```tsx
import { ChromaEffect } from '@rendiv/effects';
<ChromaEffect shift={4}>
<Logo />
</ChromaEffect>
```
| Prop | Type | Default | Description |
|---|---|---|---|
| `shift` | `number` | `3` | Pixel offset between channels |
| `style` | `CSSProperties` | — | Wrapper styles |
**Note:** Renders children 3 times (R, G, B layers).
## Composing Effects
Combine multiple approaches:
```tsx
import { Effect, blur, brightness, glowEffect, VignetteEffect } from '@rendiv/effects';
// Layer filter-only + component presets
<VignetteEffect intensity={0.5}>
<Effect filters={[...glowEffect({ color: '#58a6ff' }), blur(0.5)]}>
<MyScene />
</Effect>
</VignetteEffect>
```
## Tips
- Filter-only presets compose with spread: `[...glowEffect(), blur(2)]`
- Animated values use `(frame) =>` callbacks — combine with `interpolate` or `spring`
- `<GlitchEffect>` and `<ChromaEffect>` render children multiple times (same pattern
as `<MotionTrail>` from `@rendiv/motion-blur`)
- All effects are frame-driven and deterministic — safe for headless rendering
SKILL.md
---
name: rendiv-video
description: >
Guidance for building programmatic videos with rendiv — a React/TypeScript
framework for composing video scenes, animating with springs and interpolation,
and rendering to MP4/WebM. Use when writing or modifying rendiv compositions,
working with rendiv animation APIs, or setting up a rendiv project.
license: Apache-2.0
compatibility: Requires Node.js 18+, pnpm, and a React 19 project
metadata:
author: rendiv
version: "1.0"
---
# Rendiv Video Skills
Use these skills whenever you are working with rendiv code — writing compositions,
animating elements, embedding media, or rendering output.
## Core Mental Model
Rendiv treats video as a **pure function of a frame number**. Every visual property
(position, opacity, color, scale) is derived from the current frame via `useFrame()`.
There is no timeline state machine, no imperative keyframe API. You write a React
component that accepts a frame and returns JSX — rendiv handles the rest.
```tsx
import { useFrame, interpolate } from '@rendiv/core';
export const FadeIn: React.FC = () => {
const frame = useFrame();
const opacity = interpolate(frame, [0, 30], [0, 1], { extrapolateRight: 'clamp' });
return <div style={{ opacity }}>Hello rendiv</div>;
};
```
### Key principles
- Every animation MUST be driven by `useFrame()`. CSS animations and transitions are
forbidden — they run on wall-clock time and will desync during frame-by-frame rendering.
- Use `interpolate()` for linear mappings and `spring()` for physics-based motion.
- Use `<Img>`, `<Video>`, `<Audio>`, and `<AnimatedImage>` from `@rendiv/core` instead
of native HTML elements — they integrate with the render lifecycle via `holdRender`.
- Compositions are registered declaratively via `<Composition>` and `<Still>` — they
render `null` and only provide metadata to the framework.
## Quick Start
A minimal rendiv project entry point:
```tsx
// FadeIn.tsx — composition component
import { useFrame, interpolate, CanvasElement } from '@rendiv/core';
export const FadeIn: React.FC = () => {
const frame = useFrame();
const opacity = interpolate(frame, [0, 30], [0, 1], { extrapolateRight: 'clamp' });
return (
<CanvasElement id="FadeIn">
<div style={{ opacity }}>Hello rendiv</div>
</CanvasElement>
);
};
```
```tsx
// index.tsx — entry point
import { setRootComponent, Composition } from '@rendiv/core';
import { FadeIn } from './FadeIn';
const Root: React.FC = () => (
<>
<Composition
id="FadeIn"
component={FadeIn}
durationInFrames={90}
fps={30}
width={1920}
height={1080}
/>
</>
);
setRootComponent(Root);
```
Render to MP4: `rendiv render src/index.tsx FadeIn out/fade-in.mp4`
## Topic Guide
Load the relevant rule file based on the task at hand:
| Task | Rule file |
|---|---|
| Animate with `interpolate`, `spring`, `Easing`, `blendColors` | [animation.md](rules/animation.md) |
| Set up compositions, stills, folders, entry point | [composition-setup.md](rules/composition-setup.md) |
| Time-shift with `Sequence`, `Series`, `Loop`, `Freeze` | [sequencing-and-timing.md](rules/sequencing-and-timing.md) |
| Control z-ordering and timeline overrides | [timeline-overrides.md](rules/timeline-overrides.md) |
| Embed images, video, audio, GIFs, iframes | [media-components.md](rules/media-components.md) |
| Render animated GIFs with playback control | [gif.md](rules/gif.md) |
| Add subtitles, SRT parsing, word highlighting | [captions.md](rules/captions.md) |
| Understand `holdRender`, environment modes, rendering pipeline | [render-lifecycle.md](rules/render-lifecycle.md) |
| Animate between scenes with `TransitionSeries` | [transitions.md](rules/transitions.md) |
| Generate SVG shapes or manipulate paths | [shapes-and-paths.md](rules/shapes-and-paths.md) |
| Add noise-driven motion or motion blur | [procedural-effects.md](rules/procedural-effects.md) |
| Animate text per character, word, or line | [text-animation.md](rules/text-animation.md) |
| Apply visual effects and CSS filters | [visual-effects.md](rules/visual-effects.md) |
| Load Google Fonts or local font files | [typography.md](rules/typography.md) |
| Embed Lottie animations | [lottie.md](rules/lottie.md) |
| Add 3D scenes with Three.js / R3F | [three.md](rules/three.md) |
| Use the CLI, Studio, or Player | [cli-and-studio.md](rules/cli-and-studio.md) |
## Critical Constraints
1. **No CSS animations or transitions.** Everything MUST be frame-driven via `useFrame()`.
2. **Use rendiv media components** (`<Img>`, `<Video>`, `<Audio>`, `<AnimatedImage>`)
instead of native HTML elements. They manage `holdRender` automatically.
3. **Always wrap composition content with `<CanvasElement id="...">`.** This makes
the composition self-contained so its timeline overrides work correctly when nested
inside other compositions. The `id` must match the `<Composition>` id.
4. **`<Composition>` renders null.** It only registers metadata. The actual component
is rendered by the Player, Studio, or Renderer — not by `<Composition>` itself.
5. **`setRootComponent` can only be called once.** It registers the root that defines
all compositions.
6. **`inputRange` must be monotonically non-decreasing** in `interpolate()` and
`blendColors()`. Both ranges must have equal length with at least 2 elements.
7. **`<Series.Sequence>` must be a direct child of `<Series>`.** It throws if rendered
outside a `<Series>` parent.
8. **`morphPath` requires matching segments.** Both paths must have the same number of
segments with matching command types.
## Packages
| Package | Purpose |
|---|---|
| `@rendiv/core` | Hooks, components, animation, contexts |
| `@rendiv/player` | Browser `<Player>` component |
| `@rendiv/renderer` | Playwright + FFmpeg rendering |
| `@rendiv/bundler` | Vite-based project bundler |
| `@rendiv/cli` | CLI: render, still, compositions, studio |
| `@rendiv/studio` | Studio dev server with render queue |
| `@rendiv/transitions` | TransitionSeries with fade, slide, wipe, flip, clockWipe |
| `@rendiv/shapes` | SVG shape generators (circle, rect, star, polygon, etc.) |
| `@rendiv/paths` | SVG path parsing, measurement, morphing, stroke reveal |
| `@rendiv/noise` | Simplex noise (2D, 3D, 4D) |
| `@rendiv/fonts` | Local font loading with holdRender |
| `@rendiv/google-fonts` | Google Fonts loading with holdRender |
| `@rendiv/motion-blur` | MotionTrail and ShutterBlur components |
| `@rendiv/gif` | Animated GIF playback with speed control and fit modes |
| `@rendiv/captions` | SRT/Whisper parsing, word-by-word highlighting, caption overlay |
| `@rendiv/text` | Animated text: per-character/word/line split, stagger, presets |
| `@rendiv/effects` | Visual effects: composable CSS filters, glow, glitch, vignette |
| `@rendiv/lottie` | Frame-accurate Lottie animations via lottie-web |
| `@rendiv/three` | 3D scenes via React Three Fiber with context bridging |
## Example Assets
- [Animated bar chart](assets/animated-bar-chart.tsx) — Spring-animated bars with staggered entrances
- [Text reveal](assets/text-reveal.tsx) — Character-by-character text animation