Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Use when writing Remotion animation code in React: derive every animated value from useCurrentFrame() via interpolate()/spring() with fps from useVideoConfig() — which cheaper models do not do by default.
.claude/skills/remotion-frame-driven-animation/SKILL.md| Model | Eval pass | Runs |
|---|---|---|
| gemini-3.6-flash | 100% | 7 |
| Model | Lift | Δ tokens | Δ turns | Cases | Verified |
|---|---|---|---|---|---|
| gemini-3.6-flashbest | +9% | +97% | 0% | 22 | 54d ago |
| gemini-3.5-flash | pending re-run | — | |||
| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-07 | ✗→✓ | ▲ Improved | — | — |
| case-08 | ✗→✓ | ▲ Improved | — | — |
| case-12 | ✓→✓ | = Same ✓ | — | — |
| case-10 | ✗→✗ | = Same ✗ | — | — |
| case-21 | ✗→✗ | = Same ✗ | — | — |
In Remotion, motion is rendered frame-by-frame on a server, not played in a browser. Every animated value MUST be a pure function of the current frame. Apply this whenever you write a Remotion component that moves, fades, scales, rotates, counts, or otherwise changes over time.
useCurrentFrame() (imported from remotion)and compute every animated style from it. The component must be a pure function of that frame — the same frame always yields the same output.
flicker or drop out when rendered:
transition, CSS animation, @keyframesanimate-* utility classessetTimeout, setInterval, requestAnimationFrame, Date.now(), performance.now()useState + useEffect timers that advance the animationanimate)interpolate. For linear/eased mapping call interpolate with four positionalarguments in this exact order: interpolate(frame, inputRange, outputRange, options?). The input and output ranges are plain arrays. Never use an object-configuration form.
interpolate extrapolates by default ('extend'), so values run past the range.Stop that with the options object keys extrapolateLeft and extrapolateRight set to the string 'clamp'. Use 'clamp' for any value that must not overshoot (opacity, progress 0→1).
spring with a single object argument — spring({ frame, fps })— never positionally. Tune physics through a nested config object using only the keys damping, stiffness, mass, and overshootClamping (defaults 10 / 100 / 1 / false). Stretch a spring to a length with durationInFrames; start it late with delay.
fps from useVideoConfig() and convert any second-based duration toframes by multiplying by fps (e.g. 1.5 * fps). Never hard-code milliseconds or a raw frame count that assumes a fixed fps.
easing key of interpolate's optionsobject using the Easing module (imported from remotion): compose a convexity with a curve, e.g. Easing.inOut(Easing.quad), or Easing.bezier(0.8, 0.22, 0.96, 0.65). Never a CSS timing-function string.
(frame - delayInFrames) or pass delay to spring — do not schedule with a timer or CSS delay.
Fade — CSS transition → frame-driven interpolate
tsx// BEFORE (does not render): <div style={{ opacity: 1, transition: "opacity 2s" }}>Welcome</div> // AFTER import { useCurrentFrame, useVideoConfig, interpolate } from "remotion"; const frame = useCurrentFrame(); const { fps } = useVideoConfig(); const opacity = interpolate(frame, [0, 2 * fps], [0, 1], { extrapolateRight: "clamp" }); return <div style={{ opacity }}>Welcome</div>;
Pop — framer-motion → spring
tsx// BEFORE: <motion.div animate={{ scale: 1 }} transition={{ type: "spring" }} /> // AFTER import { spring, useCurrentFrame, useVideoConfig } from "remotion"; const frame = useCurrentFrame(); const { fps } = useVideoConfig(); const scale = spring({ frame, fps, config: { damping: 12, stiffness: 200 } }); return <div style={{ transform: `scale(${scale})` }} />;
interpolate signature — object form → four positional args
tsx// BEFORE (wrong shape): interpolate({ value: frame, inputRange: [0, 30], outputRange: [0, 100] }) // AFTER const x = interpolate(frame, [0, 30], [0, 100], { extrapolateLeft: "clamp", extrapolateRight: "clamp" });
Delay — setTimeout → frame offset
tsx// BEFORE: setTimeout(() => setShown(true), 500) // AFTER const appear = spring({ frame: frame - 0.5 * fps, fps });
Easing — CSS timing string → Easing module
tsx// BEFORE: style={{ transition: "left 1s cubic-bezier(0.8,0.22,0.96,0.65)" }} // AFTER import { interpolate, Easing } from "remotion"; const left = interpolate(frame, [0, fps], [0, 200], { easing: Easing.inOut(Easing.quad), extrapolateRight: "clamp", });
<Sequence>, useCurrentFrame() is local (starts at 0 when the sequence begins) — build theanimation against that local frame; do not add the sequence's from back in.
interpolate (e.g.rotation interpolate(s, [0, 1], [0, 360])).
useCurrentFrame() — drive itsprogress prop from the frame, never off its own internal clock.
useCurrentFrame(). Don't use CSS transition/@keyframes.interpolate(frame, inRange, outRange, opts) with four positional args. Don't pass anoptions/config object as the only argument.
extrapolateRight: 'clamp'. Don't leave a bounded value on the default 'extend'.spring({ frame, fps }). Don't call spring(frame, fps) positionally.seconds * fps. Don't hard-code milliseconds or assume 30fps.Easing.inOut(Easing.quad). Don't use a CSS cubic-bezier() timing string.animate-* out of React habit — they render blank or flicker.interpolate in an object-config form instead of the four positional arguments.spring positionally, or inventing config keys instead of damping/stiffness/mass.frame / 30, * 1000) instead of seconds * fps from useVideoConfig().setTimeout/requestAnimationFrame/Date.now() to sequence motion.useCurrentFrame().interpolate(frame, inputRange, outputRange, options) — four positional args, arrays for ranges.extrapolateLeft/extrapolateRight: 'clamp'.spring({ frame, fps, config: { damping, stiffness, mass } }) — single object.seconds * fps with fps from useVideoConfig().Easing.* in the easing option; no CSS transitions/timers.| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-10 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-21 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-12 | pass→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-02 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-11 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-20 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-09 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-05 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-01 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-13 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-14 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-15 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-04 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-03 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-06 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-22 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-18 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-17 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-07 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-08 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-16 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-19 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +9 percentage points is the difference between those two pass rates over the 22 comparable cases.
The per-case answers from this run were removed by the retention sweep, so the case table below shows the verdicts without the text either arm produced. The counts above were recorded at the time and are unaffected. Answers are now kept for 180 days.
| Model | Method | Date | Lift |
|---|---|---|---|
| gemini-3.5-flash | verified | 7/9/2026 | +26% |
Other measured skills in the registry, with their headline benchmark lift.