Adds motion to the static scenes of a Remotion project so the result reads as one calm system on a muted phone feed. It fixes the failures agents produce by default: springs that bounce text, CSS transitions that flicker in the parallel render, staggers that read as one pop, pans that strobe at 30 fps, text that moves while it should be read, exits that vanish before the cut, and loops with a visible seam.
When to use / when NOT to use
Use when the scenes exist and need animating, when motion feels jerky, mechanical, floaty or flickery, when transitions or a loop seam need fixing, or when someone asks which easing or spring to use in Remotion.
Do NOT use for: beats and timing of the story (animation-storyboard), building or
sizing scenes (vector-scene-layout), encoding and presets (animation-export-presets),
judging a finished render (animation-frame-qa), web UI hover effects, CSS or GSAP
questions, or Remotion API detail (remotion-dev/skills).
Workflow
- Read the contract:
storyboard.jsongives each beat'smotionintent, transition type and overlap;src/timeline.tsgives frames. Readreferences/motion-vocabulary.mdonce: five moves, easing tokens, springs, stagger. - Copy the helpers:
assets/templates/motion.tsintosrc/. Ifsrc/Video.tsxis still the layout template, replace it withassets/templates/Video.tsx(same series plus the loop wrap); if it was customised, patch the wrap in by hand (recipe inreferences/loops-and-transitions.md).assets/templates/scenes/TextCard.tsxshows an animated card. - Animate each scene from the local frame:
const frame = useCurrentFrame(); const { fps } = useVideoConfig();thenpresence()/enter()/exit()for cards,settle()for icons,evolvePathfor draw-on,clamp01()with an easing token for travel. Map each beat'smotionintent to the vocabulary; grouped items useenterGroup()(≤ 4 items, whole cascade inside 0.5 s); keep every text card still for its readable window; passskipExitwhen an overlapping transition does the handoff; animatetransform,opacityand SVG geometry (dash offset, path, fill), never layout properties (font-size,width,left, padding). - Transitions and loops: the storyboard's
transitionper beat drivesTransitionSeries; forloop: truethe template appends the hook copy and shifts the series (recipe inreferences/loops-and-transitions.md). - Lint, then look:
Lint must report 0 errors. Render 10–15 frames around every transition and the loop seam (python3 "${CLAUDE_SKILL_DIR}/scripts/lint_motion.py" src npx remotion render src/index.ts <target> out/frames --sequence --image-format=png --frames=<a>-<b>P-5toP-1, then0to4), tile them (ffmpeg -pattern_type glob -i 'out/frames/*.png' -vf "scale=160:-2,tile=11x1" strip.png) and open the strip. Fix anything that pops, jumps, strobes or moves under text. - Hand off: the list of moves per beat and anything the intent asked for that
was toned down (and why). Next:
animation-export-presets.
Output spec
- Every scene animated from
useCurrentFrame()with the helpers or the easing tokens;lint_motion.py srcexits 0. - Transition strips and, for loops, the wrap strip rendered and reviewed.
- No text moves during its readable window; the CTA beat is still.
Gotchas
- Remotion renders frames in parallel tabs: CSS
transition/animation, timers,Date.now,Math.random, GSAP/Motion do not animate or differ per frame. Userandom(seed); derive everything from the frame. useCurrentFrame()is local inside a<Sequence>; subtracting the beat's start again is the most common timing bug.spring()defaults overshoot; text takesdamping: 200. Always passfps.interpolatewithoutextrapolateRight: "clamp"keeps going after the range.- Enter from
scale(0.92)or a short rise; neverscale(0)or a hard pop. - Stagger 2 frames at 30 fps for grouped items and cap the group at 4 so the whole cascade lands inside 0.5 s; one frame is a pop, five is a sequence of events. A text card never staggers.
- A full-frame pan under ~7 s strobes at 24–30 fps, and an eased pan peaks at about
twice its average speed, so budget half of
maxPanPxPerFramefor eased moves; shorten the travel or use<CameraMotionBlur>on that beat only (render time multiplies bysamples). - Never animate
font-size,width,left: text reflows and line breaks jump. - A loop is a period, not "last frame equals first": the last beat cross-fades into a hook copy and the series is shifted by the wrap; the wrap must be > 0 and ≥ the hook's complete entrance including any stagger (the template throws otherwise), and the wrap is always a fade.
- 60 fps doubles render time and most feeds play 30; keep the storyboard's fps.
Pointers
scripts/lint_motion.py— anti-pattern lint with line numbers;--self-test.assets/templates/motion.ts—EASE,SPRING,enter,enterGroup,exit,presence,rise,scaleIn,settle,clamp01,maxPanPxPerFrame.assets/templates/Video.tsx— TransitionSeries with the loop wrap;scenes/TextCard.tsx— animated card.references/motion-vocabulary.md— moves, tokens, springs, stagger, smoothness rules, determinism, sources.references/loops-and-transitions.md— series arithmetic, morphs, the loop recipe, strobing and motion blur.