motion-animate
Skill4 starsChanged 3 months agoMITView on GitHub
The motion vocabulary that separates premium animation from AI slop — text pop-blur-fade, sequential staggers, always-moving eased cameras, object entrances, and the timing/easing tables behind them. Includes ready code for Remotion (interpolate/spring) and HyperFrames (GSAP). Use when animating designed frames, when motion "feels stiff", "bouncy", "jittery", "too fast", "too uniform", or when writing any timing/easing code for video.
In this repository21 other files it gives its agents.
- AGENTS.mdAGENTS.md
- 1Skill
- 2Skill
- 3Skill
- 4Skill
- 5Skill
- ae-extendscriptSkill
- asapSkill
- backbeforeSkill
- componentsSkill
- githubSkill
- huhSkill
- instantSkill
- motion-assetsSkill
- motion-designSkill
- motion-pipelineSkill
- motion-planSkill
- quick-contextSkill
- shader-uiSkill
- thumbnailSkill
- ui-intergritySkill
The file
Show its rules and text
We split this skill into the rules it gives an agent
15 rules. About 0 are common, 7 are distinctive, and 8 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈7
Distinctive · estimate
This author's own choices. Worth a look.
8
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 14
Nothing is linear: Every property moves on an ease.
- Very specific: line 82
Use transform only — never top/left/width/height.
- Distinctive: line 85
Keep total travel small. Big camera moves amplify every rounding error.
- Distinctive: line 92
Do not crossfade two designed compositions.
- Distinctive: line 98
All elements entering on the same frame.
- Distinctive: line 110
Do consecutive beats feel like a drumbeat?
- Distinctive: line 111
Do edges of a moving layer ever reveal?
- Distinctive: line 217
Put the camera above the ground layer and below any element that must stay pinned to the frame edge.
- Very specific: line 252
Use <Sequence from={...} durationInFrames={...}> per beat, driven directly by the motion-plan table.
- Very specific: line 263
Never Math.random() — if you need scatter, seed it from the element index.
- Very specific: line 264
Never Date.now() or new Date().
- Very specific: line 265
Never fetch inside a component.
- Very specific: line 273
Always pass extrapolateLeft/extrapolateRight: 'clamp' unless you specifically want the value to run past the range.
- Very specific: line 282
Run npx hyperframes docs for the current API.
- Very specific: line 286
Every timed element needs data-start, data-duration, data-track-index and class="clip".
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
motion-animate
Design decides whether a frame is good. Motion decides whether it looks expensive. The same composition animated with linear easing and simultaneous entrances reads as generated; animated with the vocabulary below it reads as authored.
Engine recipes: Appendix A, Appendix B.
The four laws
- Nothing is linear. Every property moves on an ease. Linear motion does not exist in the physical world and the eye knows it instantly.
- Nothing enters at once. Elements arrive in a deliberate order with 60–120ms between them. Simultaneous entrance is the loudest motion tell.
- The camera is never fully still. A slow continuous push or drift under every composition. Static frames read as slides.
- Nothing is metronomic. Vary durations and cut spacing. Uniform 1s beats feel automated no matter how good each beat is.
Timing table
Values at 30fps; scale for 60fps. Times in ms.
| Element | Duration | Stagger | Ease |
| Word in a text line | 400–550 | 60–90 | out-expo / power3.out |
| Text line (as a unit) | 500–650 | 120–180 | out-cubic / power2.out |
| Icon / small UI element | 350–450 | 80–120 | out-back (overshoot ≤1.06) |
| Panel / card | 550–700 | 140–200 | out-quint / power4.out |
| Hero object scale-in | 700–900 | — | out-expo |
| Hero object slide-in | 650–850 | — | out-quint |
| Line / stroke draw | 600–1000 | — | in-out-cubic |
| Camera push (per scene) | full scene length | — | out-sine, or linear-ish if very slow |
| Exit (any element) | 250–350 | 40–60 | in-cubic |
Exits are always faster than entrances, roughly half. A symmetric out-animation feels sluggish.
The core moves
Text — pop-blur-fade, word by word
The signature move. Every property animates together, per word, staggered.
y: +18px → 0
blur: 12px → 0
opacity: 0 → 1
ease: out-expo, duration 480ms, stagger 70msOptionally add scale: 0.96 → 1. Do not add rotation. Do not bounce.
For a long line, stagger by word, not by character — character stagger reads as a typewriter gimmick and takes too long. Reserve character stagger for a single short accent word.
Accent word
Runs the same move but delayed 120–200ms after its neighbours and given one extra property — a color shift, a scale bump to 1.04, or a highlight bar wiping in behind it. It should land last and land differently.
Icons and UI
Pop-fade: scale 0.7 → 1, opacity 0 → 1, out-back with overshoot capped at 1.06. Anything past 1.1 reads as a cheap bounce.
Sequential rule: the anchor element enters first, then its dependents. A card enters, then its icon, then its label. Never the reverse, never together.
Panels / cards / glass
y: +40 → 0, opacity 0 → 1, out-quint, 600ms, staggered 160ms across a stack. Add a light-sweep highlight that runs across the panel 150ms after it lands — the panel arrives, then it catches the light.
Hero object
Two options, pick by archetype:
- Scale-in:
scale 1.25 → 1.00withrotate 3° → 0°,out-expo, 800ms. Feels like it settles into place. - Slide-in: from off-frame with a 2° tilt that resolves to 0°,
out-quint, 750ms. Feels delivered.
The cast shadow and clipped texture must be parented to the hero so they move with it. Un-parented shadows sliding independently is a hard failure.
Stroke / line draw
Animate path length 0 → 100%, in-out-cubic, 600–1000ms. Ease in and out — a draw that starts at full speed looks mechanical. Use to connect a label to a subject, underline an accent word, or trace a chart.
Camera
Under every composition, one of:
- Push in:
scale 1.00 → 1.08over the full beat,out-sine. - Push out:
scale 1.12 → 1.00, faster at the start, settling. - Drift:
xorytranslate of 2–4% over the beat.
Anti-jitter contract:
- Animate a wrapper element, never the element that owns layout or clipping.
- Use
transformonly — nevertop/left/width/height. - Scale up the moving layer ~6% beyond frame so edges never reveal.
- Set
will-change: transformand force GPU compositing. - Keep total travel small. Big camera moves amplify every rounding error.
Transitions between beats
- Hard cut masked by an SFX hit — the default. Crossfades between designed frames muddy both.
- Scale-through — an element scales up to fill frame and the next scene is revealed inside it.
- Wipe by a shape — a circle or panel scales past frame edge, carrying the next scene.
Do not crossfade two designed compositions. Do not use a stock transition preset.
Forbidden
- Linear easing on anything visible.
- Bounce overshoot >1.1 (exception: an intentionally playful SaaS/product style, and then consistently).
- All elements entering on the same frame.
- Uniform beat lengths across an edit.
repeat: -1or any infinite loop — renders are frame-sampled and must be deterministic.Math.random(),Date.now(), or network fetches inside a composition. Same input must produce the same frame every time.- Motion applied to a wrapper the framework already controls (see Appendix B §clip rule).
Self-check
Scrub the render at 0.25× speed. Ask:
- Does anything start and stop abruptly? → missing ease.
- Does everything land together? → missing stagger.
- Does the frame ever sit dead still? → missing camera.
- Do consecutive beats feel like a drumbeat? → durations too uniform.
- Do edges of a moving layer ever reveal? → not over-scaled.
Appendix A — Remotion recipes
Frame-based, deterministic by construction. All timing derives from useCurrentFrame().
Helper: time in ms, not frames
Authoring in frames hardcodes your fps. Convert.
import {useVideoConfig, useCurrentFrame, interpolate, Easing} from 'remotion';
const useMs = () => {
const {fps} = useVideoConfig();
return (ms: number) => (ms / 1000) * fps;
};Easing map
The names in the timing table, in Remotion terms:
Easing.out(Easing.exp) // out-expo
Easing.out(Easing.cubic) // out-cubic
Easing.out(Easing.quad) // out-quad
Easing.out(Easing.poly(4)) // out-quint-ish
Easing.inOut(Easing.cubic) // in-out-cubic
Easing.out(Easing.sin) // out-sine
Easing.in(Easing.cubic) // in-cubic (exits)
Easing.bezier(0.16, 1, 0.3, 1) // the workhorse — expo-ish outFor overshoot, prefer a spring over Easing.back — it is easier to keep subtle.
import {spring} from 'remotion';
const s = spring({frame, fps, config: {damping: 200, stiffness: 120, mass: 0.6}});
// damping 200 = no visible overshoot. Drop to ~14 for a small settle.Text — pop-blur-fade, word by word
const Words: React.FC<{text: string; delay?: number}> = ({text, delay = 0}) => {
const frame = useCurrentFrame();
const ms = useMs();
const STAGGER = ms(70);
const DUR = ms(480);
return (
<span style={{display: 'inline-flex', flexWrap: 'wrap', gap: '0.28em'}}>
{text.split(' ').map((word, i) => {
const local = frame - ms(delay) - i * STAGGER;
const p = interpolate(local, [0, DUR], [0, 1], {
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
easing: Easing.bezier(0.16, 1, 0.3, 1),
});
return (
<span
key={i}
style={{
display: 'inline-block',
opacity: p,
filter: `blur(${(1 - p) * 12}px)`,
transform: `translateY(${(1 - p) * 18}px)`,
willChange: 'transform, filter, opacity',
}}
>
{word}
</span>
);
})}
</span>
);
};The accent word is the same component with delay set 120–200ms later plus its own color and a scale(1.04).
Camera — always-on push
Wrap the whole scene. Over-scale so edges never reveal.
const Camera: React.FC<{children: React.ReactNode; durationInFrames: number}> = ({
children, durationInFrames,
}) => {
const frame = useCurrentFrame();
const scale = interpolate(frame, [0, durationInFrames], [1.0, 1.08], {
extrapolateRight: 'clamp',
easing: Easing.out(Easing.sin),
});
return (
<AbsoluteFill style={{transform: `scale(${scale})`, willChange: 'transform'}}>
{children}
</AbsoluteFill>
);
};Put the camera above the ground layer and below any element that must stay pinned to the frame edge.
Staggered card stack
const cards = ['One', 'Two', 'Three'];
const ms = useMs();
{cards.map((label, i) => {
const local = frame - i * ms(160);
const p = interpolate(local, [0, ms(600)], [0, 1], {
extrapolateLeft: 'clamp', extrapolateRight: 'clamp',
easing: Easing.out(Easing.poly(4)),
});
return (
<div key={i} style={{
opacity: p,
transform: `translateY(${(1 - p) * 40}px)`,
}}>{label}</div>
);
})}Exit — half the entrance
const exitStart = durationInFrames - ms(300);
const out = interpolate(frame, [exitStart, durationInFrames], [1, 0], {
extrapolateLeft: 'clamp', extrapolateRight: 'clamp',
easing: Easing.in(Easing.cubic),
});Structure
Use <Sequence from={...} durationInFrames={...}> per beat, driven directly by the motion-plan table. One Sequence per row. Beat durations come from the plan, not from round numbers.
<Series>
<Series.Sequence durationInFrames={ms(5200)}><BeatTwo /></Series.Sequence>
<Series.Sequence durationInFrames={ms(2100)}><BeatThree /></Series.Sequence>
</Series>Determinism
- Never
Math.random()— if you need scatter, seed it from the element index. - Never
Date.now()ornew Date(). - Never fetch inside a component. Preload with
staticFile()/delayRender(). <Video>/<Audio>must be<OffthreadVideo>where possible for render correctness.
Gotchas
filter: blur()on many simultaneous elements is expensive — cap concurrent blurred nodes, or pre-blur in the asset.- Font loading: use
@remotion/google-fontsordelayRenderaround a localFontFace.load(), or the first frames render in a fallback face. interpolatethrows if the input range is not monotonically increasing — guard computed ranges.- Always pass
extrapolateLeft/extrapolateRight: 'clamp'unless you specifically want the value to run past the range.
Appendix B — HyperFrames / GSAP recipes
HyperFrames composes HTML + GSAP and renders by frame-sampling a headless browser. Determinism is mandatory.
Run npx hyperframes docs for the current API. This file is the motion standard on top of it — see the hyperframes skill for the pipeline (init → preview → lint → render).
The clip rule (breaks renders if violated)
Every timed element needs data-start, data-duration, data-track-index and class="clip". The framework owns visibility and transform on .clip.
Never GSAP the .clip element itself. Animate a child wrapper.
<div class="clip" data-start="4.2" data-duration="5.2" data-track-index="2">
<div class="cam"> <!-- camera moves here -->
<div class="inner"> <!-- element motion here -->
<h1 class="headline">…</h1>
</div>
</div>
</div>Timeline registration
const tl = gsap.timeline({paused: true});
window.__timelines['main'] = tl; // id must match data-composition-idPaused, always. The renderer seeks it.
Easing map
| Table name | GSAP |
| out-expo | expo.out |
| out-cubic | power2.out |
| out-quint | power4.out |
| out-back (capped) | back.out(1.2) |
| in-out-cubic | power2.inOut |
| out-sine | sine.out |
| in-cubic (exits) | power2.in |
Text — pop-blur-fade, word by word
Split into word spans at author time (do not rely on a runtime splitter — it can race the first sampled frame).
<h1 class="headline">
<span class="w">What</span> <span class="w">looks</span>
<span class="w accent">like</span> <span class="w">discipline</span>
</h1>tl.from('.headline .w', {
y: 18,
opacity: 0,
filter: 'blur(12px)',
duration: 0.48,
ease: 'expo.out',
stagger: 0.07,
}, 0);
// accent word lands last and differently
tl.from('.headline .accent', {
scale: 0.96,
color: 'var(--surface)',
duration: 0.5,
ease: 'expo.out',
}, 0.18);Camera
Animate .cam, never .clip.
tl.fromTo('.cam',
{scale: 1.0},
{scale: 1.08, duration: 5.2, ease: 'sine.out', force3D: true},
0);.cam { will-change: transform; transform: translateZ(0); }Over-scale the moving layer ~6% beyond the frame so edges never reveal during a push-out or drift.
Staggered card stack
tl.from('.card', {
y: 40, opacity: 0,
duration: 0.6, ease: 'power4.out', stagger: 0.16,
}, 1.2);
// light sweep lands 150ms after the panel
tl.fromTo('.card .sweep', {xPercent: -140}, {
xPercent: 140, duration: 0.7, ease: 'power2.inOut', stagger: 0.16,
}, 1.35);Stroke draw
tl.fromTo('.connector', {drawSVG: '0%'}, {drawSVG: '100%', duration: 0.8, ease: 'power2.inOut'});
// no DrawSVGPlugin? use stroke-dasharray/offset:
tl.fromTo('.connector', {strokeDashoffset: 1000}, {strokeDashoffset: 0, duration: 0.8, ease: 'power2.inOut'});Hard rules
- Deterministic only — no
Math.random(), noDate.now(), no fetch, norepeat: -1. - 9:16 = 1080×1920. Set
#root { container-type: size; }and size everything incqw/cqh(1cqw = 10.8px), nevervw/vh. - Adjacent same-track clips must not touch. Float math makes
3.2 + 0.35 = 3.5500000000000003, which overlaps a clip starting at3.55and tripsoverlapping_clips_same_track. Leave a gap, or use durations like0.34. - Audio: videos are
muted; use one pre-mixed master<audio>track (VO + SFX mixed with ffmpegadelay+amix) on a high dedicateddata-track-index. Multiple<audio>elements cause echo and desync. - Split long edits into sub-compositions (
data-composition-src="compositions/beat-04.html") — one dense track tripstimeline_track_too_dense. - Fonts must be local
@font-facewoff2. A network font may not be loaded on the first sampled frame.
Verify
Preview and screenshot key beats before rendering. After render, ffprobe for dimensions/duration/codec and spot-check frames against the preview. Motion problems (jitter, edge reveal, missing stagger) are visible in a contact sheet and invisible in a lint pass.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

