mcpField
R
remotion-keyframes

remotion-keyframes

github.com/phamthanhnghiaCreative & media362 installsUpdated 14w ago
Repository

About remotion-keyframes

remotion-keyframes is a skill for creating frame-deterministic animations in Remotion and checking their states in Studio. It emphasizes subject continuity, visible poses, and verification of the final frame.

Key features

  1. 01Derive animation values from the current frame
  2. 02Create keyframes with spring and interpolate
  3. 03Stagger multiple animated items by frame
  4. 04Inspect the full timeline in Studio
  5. 05Avoid nondeterministic animation in render-critical paths

Use cases

  1. 01Create entrances, transitions, and multi-step animations
  2. 02Build word-by-word text or SVG stroke reveals
  3. 03Check the first frame, poses, and final frame

Install

Copy the command and run it in a terminal.

$npx skills add https://github.com/phamthanhnghia/remotion-agent-skills --skill remotion-keyframes

After installing, type/remotion-keyframesin Claude Code to confirm it loaded.

Details

From the project README

Remotion Keyframes

Keyframes are a pose contract: visible states, continuous subject identity, frame-deterministic, verified pixels.

Use remotion-animation for broad scene recipes. Use remotion-cli for full command docs.

Procedure

  1. Identify the animated subject, visible states, final state, and animation primitive (spring or interpolate).
  2. Choose the smallest mechanism that proves the prompt.
  3. Author frame-deterministic keyframes. All values derive from useCurrentFrame().
  4. Verify with npx remotion studio — scrub the full timeline, check first frame, all poses, and final frame.
  5. If proof fails, fix the source keyframes and re-verify before rendering.

Contract

  • Name the moving subject.
  • Name the poses needed to prove the intended motion, including the final state.
  • Keyframe visible channels, not hidden helper state.
  • Preserve object identity when continuity matters.
  • Hold readable or semantic states long enough to see (minimum 10 frames for text, 6 frames for icon).
  • Final frame is part of the animation, not cleanup.
  • Do not reset to rest unless requested.
  • Do not end on black unless requested.

Runtime Rules

interpolate() and spring():

  • all values computed synchronously from frame — no async inside interpolate/spring calls
  • extrapolateLeft: 'clamp' and extrapolateRight: 'clamp' to prevent value leaking outside intended range
  • spring({frame, fps, config}) — frame must be the Remotion useCurrentFrame() value, not a derived Date.now() derivative
  • For entrances, offset spring: spring({frame: frame - delay, fps, config}); use Math.max(0, frame - delay) to avoid negative frames driving spring

Never use for render-critical motion:

  • Date.now() or performance.now()
  • unseeded Math.random() (use Remotion's random(seed) instead)
  • CSS transitions or CSS animations (they don't seek)
  • requestAnimationFrame
  • setTimeout / setInterval in render path
  • useState / useEffect for visual-property values

Spring Config Reference

import {spring, useCurrentFrame, useVideoConfig} from 'remotion';

const frame = useCurrentFrame();
const {fps} = useVideoConfig();

// Snappy entrance (default feel)
const scale = spring({frame, fps, config: {damping: 200}});

// Bouncy entrance
const bounce = spring({frame, fps, config: {damping: 8, stiffness: 300}});

// Very smooth, slow
const smooth = spring({frame, fps, config: {damping: 30, stiffness: 80, mass: 2}});

// Sharp exit (reverse spring — animate from 1 to 0)
const {durationInFrames} = useVideoConfig();
const exitFrame = durationInFrames - frame;
const exitScale = spring({frame: exitFrame, fps, config: {damping: 200}});
const scaleValue = 1 - (1 - exitScale) ; // goes from 1 down to 0

Interpolate Recipes

import {interpolate} from 'remotion';

// Fade in over 20 frames
const opacity = interpolate(frame, [0, 20], [0, 1], {extrapolateRight: 'clamp'});

// Slide in from left
const x = interpolate(frame, [0, 30], [-300, 0], {
  extrapolateLeft: 'clamp',
  extrapolateRight: 'clamp',
});

// Multi-stop color shift
const r = interpolate(frame, [0, 30, 60], [255, 100, 0], {extrapolateRight: 'clamp'});
const g = interpolate(frame, [0, 30, 60], [0, 150, 255], {extrapolateRight: 'clamp'});

// Scale pop
const scale = interpolate(frame, [0, 8, 12, 18], [0, 1.15, 0.95, 1], {
  extrapolateLeft: 'clamp',
  extrapolateRight: 'clamp',
});

Channels

Prefer compositor/visual channels: transform: translateX/Y/Z, transform: scale/rotate/skew, opacity, borderRadius, CSS vars, clipPath, SVG strokeDashoffset.

Avoid layout channels: top/left/right/bottom, width/height, margin/padding — they trigger reflow, are slow, and produce incorrect results when the renderer samples frames in parallel.

Stagger Pattern

const ITEM_COUNT = 5;
const STAGGER_FRAMES = 8; // delay between each item

const items = Array.from({length: ITEM_COUNT}, (_, i) => {
  const delayedFrame = Math.max(0, frame - i * STAGGER_FRAMES);
  const itemSpring = spring({frame: delayedFrame, fps, config: {damping: 200}});
  return {opacity: itemSpring, y: (1 - itemSpring) * 40};
});

Text Animation Pattern

const words = 'Hello Remotion World'.split(' ');

const WordReveal = () => {
  const frame = useCurrentFrame();
  const {fps} = useVideoConfig();

  return (
    <div style={{display: 'flex', gap: 12}}>
      {words.map((word, i) => {
        const delayedFrame = Math.max(0, frame - i * 6);
        const progress = spring({frame: delayedFrame, fps, config: {damping: 200}});
        return (
          <span
            key={i}
            style={{
              opacity: progress,
              transform: `translateY(${(1 - progress) * 20}px)`,
              display: 'inline-block',
            }}
          >
            {word}
          </span>
        );
      })}
    </div>
  );
};

SVG Stroke Draw

// SVG stroke-dasharray draw-on effect
const pathLength = 500; // measure with path.getTotalLength() in development
const drawProgress = interpolate(frame, [0, 60], [0, 1], {extrapolateRight: 'clamp'});

<path
  d="M 0 0 L 300 200"
  stroke="white"
  strokeWidth={4}
  fill="none"
  strokeDasharray={pathLength}
  strokeDashoffset={pathLength * (1 - drawProgress)}
/>

3D Depth (CSS Perspective)

// CSS perspective 3D scene
<div style={{perspective: 800, perspectiveOrigin: '50% 50%'}}>
  <div
    style={{
      transformStyle: 'preserve-3d',
      transform: `rotateY(${interpolate(frame, [0, 60], [45, 0], {extrapolateRight: 'clamp'})}deg)`,
    }}
  >
    {/* 3D content */}
  </div>
</div>

Mechanism Choice

NeedMechanism
Entrance with natural decelerationspring()
Timed value transitioninterpolate()
Multi-stop sequenceinterpolate() with multiple input/output stops
Many items with ordered entrancestagger: indexed frame - i * delay in spring()
Text reveal (word by word)stagger spring per word index
Stroke grows or tracesstrokeDashoffset via interpolate()
Shape reveal boundaryclipPath via interpolate()
3D depth sceneCSS perspective + rotateX/Y/Z via interpolate()

Error Handling

FailureFix
Values jump at certain framesCheck for negative frame passed to spring() — clamp with Math.max(0, frame - delay)
Animation doesn't seek correctlyConfirm no useState/CSS animation driving the value
Black frames at start or endCheck extrapolateLeft: 'clamp' on entrance, extrapolateRight: 'clamp' on exit
Text blinking / inconsistentEnsure random() seed is a stable string, not Math.random()
Spring overshoots off-screenIncrease damping (200 = no overshoot, 8 = lots of bounce)

Done

Verify in Studio: scrub first frame, all visible poses, final frame. Confirm subject-owned motion, no debug overlays, no flash on first frame.

Comments

Markdown supported