Remotion renders frames out of order, in parallel tabs. Anything that depends on wall-clock time, render order or hidden state makes frames disagree, flicker or differ between preview and render. Every animation must produce the same image whenever the same frame is rendered. The official skill in studio/.claude/skills/ is the API reference; these are the rules this studio enforces.
Use
useCurrentFrame()(local to the enclosing<Sequence>),useVideoConfig()forfps,width,height,durationInFrames.interpolate(frame, [a, b], [from, to], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing }). Always clamp unless you want extrapolation.spring({ frame, fps, config, delay, durationInFrames })for entrances; use the presets intokens.ts. Damping high (e.g. 200) for no overshoot; lower damping for a small, intentional overshoot.Easing.bezier(...)/Easing.out(Easing.cubic)for eased tweens; linear only when mechanical on purpose (progress bars, tickers).<Sequence from durationInFrames premountFor>and<Series>;@remotion/transitions<TransitionSeries>for scene transitions. Remember: transitions overlap scenes, so total duration = sum of sequences minus sum of transition lengths. Compute it, don't guess.random('seed-string')for any variation; same seed, same value.staticFile()for files instudio/public/;<Img>, and the video/audio components named in the official skill (this project ships@remotion/media), so frames wait for assets.- Fonts through
@remotion/google-fontsloadFont()or local fonts as the official fonts guide says, so text never renders in a fallback face on early frames. delayRender/continueRenderonly for genuinely async data, always with a timeout and error path.
Never
- CSS
transition,animation,@keyframes;requestAnimationFrame,setTimeout,setInterval;Date.now(),performance.now();Math.random(). - Animation state in
useState/useEffect/ refs that accumulates across frames. <img>,<video>,<audio>HTML tags for media in the composition.will-changeon text that is scaled (blurry text).- Hard-coded colours, font sizes, durations or easings in scenes: take them from
tokens.ts/timing.ts.
Timing as data
timing.tsper video: scene starts and lengths in frames, derived once from the storyboard's seconds. Scenes read their offsets from it; nothing else hard-codes a frame number that another file depends on.hits.ts:[{ frame, label, sfx, volume }], a one-to-one copy of the storyboard's hits table. The composition renders each SFX in a<Sequence from={frame - peakOffset}>so the sound's peak (not its start) lands on the hit. Export the same list as JSON (seconds) forinspect.mjs sync. No SFX anywhere else.- Voice-over driven pieces: measure word times (Whisper) and store them as data; place changes ~0.1 s before the word. If every cut must land on spoken words with live HTML review, use the
motion-studioHTML engine instead.
Check before rendering
npm run lintpasses (eslint with the Remotion config catches several of the above).- Render the same frame twice (two stills runs) when something looks unstable: identical images, or there is hidden state.
- Search the video folder for banned APIs:
Math.random,Date.now,setTimeout,requestAnimationFrame,transition:,@keyframes.
Frame arithmetic traps
Failure-derived from a 44-shot explainer (evidence: real rework). Full list with reasons: ~/.claude/skills/motion-studio/references/gotchas.md. The ones that bite Remotion:
interpolateorspringreturning 0 at the shot's first frame leaves it empty: start soft (25 to 57 percent) or start on a settled frame.- An exit that ends at 40 to 57 percent opacity pops at a hard cut: finish the fade (
1 - (n/N)^1.5) or end on a cut frame. A fade under 0.3 s reads as a pop. - First elements start on the shot's first frame, not when the caption starts. Do not wrap an already ramping component in a second fade.
- Keyframe tables start with the off-screen value at shot start. Slide-ins from below over about 120 px cross the caption band.
TransitionSeriesoverlaps change durations: recompute the sum before hard-coding any frame after a transition.- Do not change words after frame numbers are hard-coded; derive from
timing.ts. - Fast moves need 8 motion-blur subframes (render the frame at sub-offsets and blend); 4 ghosts on floods, wipes and drags.
- Helpers: see
studio/src/lib/motion.ts(buoyant(), and exit and entry ramps) andstudio/src/lib/transitions.tsx(velocity-matched cut); check any move withinspect.mjs plotand glides or cuts withinspect.mjs flow.