remotion-patterns
ProAnimate's custom Remotion-compatible engine API, layer component patterns, and composition architecture. Auto-applied when creating or modifying Remotion layers or the video export pipeline.
SKILL.mdAutoAnimation/.claude/skills/remotion-patterns/SKILL.md
GitHubProAnimate Engine — Remotion-Compatible API
ProAnimate reimplements Remotion's core API in a custom engine. The actual remotion npm package is NOT installed. All imports come from @/engine/ not remotion.
API Mapping
| Remotion API | ProAnimate Equivalent | Import From |
|---|---|---|
useFrame() |
useFrame() |
@/engine/CompositionContext |
useVideoConfig() |
useComposition() |
@/engine/CompositionContext |
AbsoluteFill |
Fill |
@/engine/primitives |
Sequence |
Clip |
@/engine/primitives |
Audio |
AudioTrack |
@/engine/primitives |
OffthreadVideo |
VideoTrack |
@/engine/primitives |
Player |
CompositionPlayer |
@/engine/CompositionPlayer |
interpolate() |
interpolate() |
@/engine/interpolate |
spring() |
spring() |
@/engine/spring |
measureSpring() |
measureSpring() |
@/engine/spring |
NEVER import from remotion. Always import from @/engine/.
Primitives
Fill — Full-viewport cover (replaces AbsoluteFill)
<Fill style={{ background: '#000' }}>
{children}
</Fill>
Renders <div> with position: absolute; inset: 0.
Clip — Frame-range visibility (replaces Sequence)
<Clip from={30} durationInFrames={60}>
{/* Only visible from frame 30 to 89 */}
</Clip>
Returns null if current frame is outside [from, from + durationInFrames).
AudioTrack — Synced audio playback
<AudioTrack src={audioUrl} volume={0.8} />
Frame-syncs via HTMLAudioElement.currentTime. Drift threshold: 0.5 frames.
VideoTrack — Synced video playback
<VideoTrack src={videoUrl} style={{ width: '100%' }} muted={false} />
Animation Functions
interpolate(value, inputRange, outputRange, options?)
Multi-stop linear interpolation with optional easing.
const opacity = interpolate(frame, [0, 30], [0, 1])
const x = interpolate(frame, [0, 30, 60], [0, 100, 0], { easing: easeInOut })
Options: easing, extrapolateLeft: 'clamp' | 'extend', extrapolateRight: 'clamp' | 'extend'.
spring({ frame, fps, config?, from?, to?, durationInFrames? })
Damped spring physics via RK4 integration. Results are cached per config.
const scale = spring({ frame, fps: 30, config: { stiffness: 100, damping: 10 }, from: 0, to: 1 })
Config defaults: damping: 10, mass: 1, stiffness: 100, overshootClamping: false.
measureSpring(config?, options?)
Returns frame count until spring settles (within threshold for 3 consecutive frames).
const dur = measureSpring({ stiffness: 100 }, { fps: 30, threshold: 0.001 })
Layer Component Pattern
All layer components in src/remotion/ follow this structure:
import { useFrame } from '@/engine/CompositionContext'
import { interpolate } from '@/engine/interpolate'
interface RemotionXXXLayerProps {
items: XXXItem[]
canvasWidth: number
canvasHeight: number
keyframeData?: KeyframeData
}
export function RemotionXXXLayer({ items, canvasWidth, canvasHeight, keyframeData }: RemotionXXXLayerProps) {
const frame = useFrame()
if (items.length === 0) return null
return (
<>
{items.map(item => {
// Frame-based transforms using interpolate/spring
const opacity = interpolate(frame, [item.startFrame, item.startFrame + 10], [0, 1])
return (
<div key={item.id} style={{
position: 'absolute',
left: `${(item.x / canvasWidth) * 100}%`,
top: `${(item.y / canvasHeight) * 100}%`,
opacity,
}}>
{/* Render item content */}
</div>
)
})}
</>
)
}
Key rules:
- Always call
useFrame()for frame-indexed rendering - Always check
items.length === 0and returnnullearly - Position using percentage-based layout relative to
canvasWidth/canvasHeight - Use
interpolate()orspring()for all animations — never CSS transitions - Accept
keyframeDataprop for shared transform synchronization
Composition Architecture
VideoComposition.tsx is the master renderer. Layer stacking order (bottom to top):
Fill (background)
└─ CameraTransformWrapper
├─ StyleEffectFilters (SVG defs: woodcut, cel-shade, neon, glitch, VHS)
├─ StyleFilter (CSS global filters)
├─ Background Lottie (zIndex < 0)
├─ RemotionVideoLayer
├─ RemotionMediaLayer (images)
├─ RemotionHTMLTemplateLayer
├─ RemotionShapeLayer
├─ RemotionArtCurveLayer
├─ RemotionCrowdLayer
├─ RemotionSVGObjectLayer
├─ Characters (Dialogue or Single mode)
│ ├─ CharacterStyleEffectWrapper
│ │ └─ RemotionCharacter (4-layer sprite composite)
│ └─ AudioTrack via Clip (per dialogue line)
├─ RemotionRiggedCharacter
├─ RemotionPixelArtCharacter
├─ RemotionAvatarCharacter
├─ Remotion3DLayer (Three.js scene)
├─ RemotionTextOverlay
├─ RemotionAnnotationLayer
├─ RemotionParticleLayer
├─ RemotionAudioReactiveLayer
├─ Overlay Lottie (zIndex >= 0)
└─ Background Audio (Clip + AudioTrack)
├─ RemotionCaptions (outside camera, fixed position)
├─ RemotionRetentionHookLayer
└─ RemotionTransitionLayer
Adding a New Layer
- Create
src/remotion/RemotionXXXLayer.tsxfollowing the pattern above - Add types to
src/types/xxx.ts - Add store at
src/stores/useXXXStore.ts(Zustand + Immer) - Import and render in
VideoComposition.tsxat the correct z-order position - Add prop to
VideoCompositionPropsinterface - Pass data from store in the parent that mounts
CompositionPlayer
Dialogue Mode
Two rendering modes for characters:
Sprite mode: Direct RemotionCharacter — 4-layer composite (body, head, viseme mouth, hair)
Rigged mode: RemotionRiggedCharacter (Canvas2D mesh body) + RemotionCharacter overlay (sprite head/viseme/hair) with CSS scale from boundsWidth / rigExportData.imageWidth
Audio tracks per dialogue line:
<Clip from={line.startFrame} durationInFrames={line.endFrame - line.startFrame}>
<AudioTrack src={line.audioUrl} volume={1} />
</Clip>
Viseme/emotion events are offset: event.startFrame + line.startFrame
Video Export Pipeline
Export uses WebCodecs + mp4-muxer/webm-muxer directly (NOT Remotion's rendering). See src/services/videoExport.ts. The composition is rendered frame-by-frame to an offscreen canvas.