flat-artist is now the single container: all 16 template skills + the R&D references/ moved inside flat-artist/. Cross-references updated — the orchestrator points to bundled `<name>/SKILL.md`, sub-skills point to `../<name>/SKILL.md`, and the R&D report path is relative. README catalog updated. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
11 KiB
name, description
| name | description |
|---|---|
| motion-design-principles | The foundation motion-craft reference for FlatRender Remotion templates — easing curves and when to reach for each, timing & spacing, the 12 animation principles applied to Remotion, anticipation/overshoot/follow-through/settle, staggering & choreography, secondary motion, spring() vs interpolate(), and the blocking→timing→polish workflow. Use whenever animating ANY element in a template, reviewing motion quality, or deciding how something should enter, move, or leave. Read this BEFORE writing animation code. |
Motion design principles (the FlatRender craft floor)
Project: services/remotion/ (Remotion 4 + @remotion/three, R3F v9, gl="angle"). Three aspects (16:9 / 1:1 / 9:16), Persian-first (Vazirmatn, RTL). Helpers: src/lib/anim.ts (hexToRgba, mixHex, rand), src/lib/aspect.ts (useLayout → isWide/isSquare/isTall, vmin, unit, pick), src/lib/branding.ts (colorSchema, BRAND), src/lib/fonts.ts (FONT = Vazirmatn), src/lib/three-kit.tsx (StudioEnv/Lights/Floor/Effects, Confetti3D).
Linear motion is the sound of an amateur. Almost nothing in a FlatRender template should move at a constant rate. This skill is the floor every template stands on.
The one rule everything hangs on
A Remotion frame is pure: frame → pixels, sampled at an arbitrary t (the After Effects mental model — a keyframe graph read at time t). The renderer samples frames out of order and in parallel.
- Derive every value from
useCurrentFrame(). If a value can't be, it doesn't belong in the render. - Never
useFrame(R3F),Math.random(),Date.now(),setState, oruseEffect-driven motion. For "randomness" userand(seed)fromanim.ts. - Never hardcode 30fps.
const { fps } = useVideoConfig(); const sec = (s: number) => Math.round(s * fps);
spring() vs interpolate() — pick deliberately
interpolate() |
spring() |
|
|---|---|---|
| Who authors the curve | you (explicit easing) | physics (mass/damping/stiffness) |
| Reach for it when | a value must hit an exact mark on an exact frame — storyboard reveals, crossfades, value remaps, color/blur sweeps | organic entrances, pops, bounces, anything that should "feel" alive |
| The trap | forgetting extrapolate*: "clamp" → elements drift off-screen / opacity goes negative |
trying to land a value on an exact frame |
Always combine them — spring drives the feel (0→1), interpolate remaps it to real px/units in the layout's own scale:
const L = useLayout();
const p = spring({ frame: frame - start, fps, config: { mass: 0.6, damping: 12, stiffness: 180 } });
const y = interpolate(p, [0, 1], [L.vmin(80), 0]); // remap into layout units
const opacity = interpolate(p, [0, 1], [0, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp" });
Spring config cheat-sheet
Lower damping = more overshoot · higher mass = heavier/slower · higher stiffness = faster snap.
| Feel | mass | damping | stiffness | Use for |
|---|---|---|---|---|
| Snappy, no overshoot | 0.5 | 200 | 200 | Clean UI / logo reveals |
| Natural pop (default) | 0.6 | 12 | 180 | Cards, badges, icons |
| Bouncy / playful | 1 | 8 | 120 | Kids, birthday, mascots |
| Heavy / weighty | 2.5 | 26 | 90 | Big titles, 3D objects landing |
| Loose wobble (follow-through) | 1 | 6 | 80 | Secondary / trailing parts |
Easing cheat-sheet (import { Easing } from "remotion")
| Situation | Curve | Why |
|---|---|---|
| Entrances (default) | Easing.out(Easing.cubic) |
things arrive and decelerate |
| Hero title entrance | Easing.out(Easing.quint) or Easing.bezier(0.16, 1, 0.3, 1) |
dramatic deceleration |
| Exits | Easing.in(Easing.cubic) — always sharper than the entrance |
things leave faster than they arrive |
| A→B on-screen move / camera | Easing.inOut(Easing.cubic) |
smooth both ends |
| "Ta-da" overshoot | Easing.bezier(0.34, 1.56, 0.64, 1) |
snappy pop past target |
| Wind-up / anticipation | Easing.bezier(0.36, 0, 0.66, -0.56) |
dips below before launch |
| Linear ONLY | Easing.linear |
rotation, scroll, conveyor, marquee — mechanical continuous motion |
const t = interpolate(frame, [start, start + 24], [0, 1],
{ extrapolateLeft: "clamp", extrapolateRight: "clamp", easing: Easing.out(Easing.cubic) });
Timing & spacing (30fps baseline — but always derive with sec())
Spacing (the easing) sets feel; timing (frame count) sets weight & mood. Cut frames before you add them — amateurs over-animate.
| Beat | Frames @30fps |
|---|---|
| Micro pop (icon, badge) | 8–14 |
| Standard reveal | 18–28 |
| Hero entrance | 28–40 |
| Scene transition | 12–20 |
| Hold | a comfortable read of the text (size to the longest Persian string) |
Symptoms: robotic = linear spacing · floaty/late = timing too long · jittery = no hold between moves.
The 12 principles → Remotion (the four in bold you reach for every shot)
| Principle | Remotion expression |
|---|---|
| Squash & stretch | scaleX/scaleY inversely around an impact frame, conserve volume (sx = 1/sy) |
| Anticipation | dip the value below its start before the main move |
| Staging | stagger reveals; dim/blur everything but the hero — one idea per beat |
| Straight-ahead vs pose-to-pose | interpolate between keyed frames vs per-frame formula (sim, e.g. Confetti3D) |
| Follow-through & overlapping | same trigger, delayed per child + a looser spring so parts settle later |
| Slow in & slow out | Easing.bezier / spring() — the single biggest quality lever |
| Arcs | drive y with sin/parabola while x moves linearly |
| Secondary action | a small sin bob/shimmer alongside the primary reveal |
| Timing | frame count + spring mass/damping = weight & mood |
| Exaggeration / overshoot | overshoot > 1.0, then settle to 1.0 |
| Solid drawing | StudioLights + reflective material + floor shadows (3D) |
| Appeal | choreography + StudioEffects (bloom/DOF/vignette) + good type |
The four quality multipliers (concrete, reusable)
Anticipation — a small negative dip before launch:
const scale = interpolate(frame, [start, start + 6, start + 30], [0, -0.12, 1],
{ extrapolateRight: "clamp", easing: Easing.bezier(0.36, 0, 0.66, -0.56) });
Overshoot + settle — reach past, then land. Ensure the curve holds the target (clamp) or it micro-drifts forever:
const pop = interpolate(frame, [start, start + 18], [0, 1],
{ extrapolateRight: "clamp", easing: Easing.bezier(0.34, 1.56, 0.64, 1) });
// or: spring with low damping (config { mass: 0.6, damping: 10, stiffness: 170 })
Follow-through — drive children from the same trigger, delay each, looser spring so they settle after the parent. The biggest "feels professional" upgrade for grouped elements:
function Child({ i, start }: { i: number; start: number }) {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const p = spring({ frame: frame - start - i * 4, fps, config: { mass: 1, damping: 6, stiffness: 80 } });
return <g style={{ transform: `translateY(${interpolate(p, [0, 1], [24, 0])}px)`, opacity: p }} />;
}
Secondary motion — never let a held element go dead. Add a tiny sin breathe/shimmer:
const bob = Math.sin(frame / fps * Math.PI) * L.vmin(4); // gentle float during the hold
Staggering & choreography
Default to a cascade, and tune the stagger per aspect — wider frames read faster (tighter stagger), tall frames read slower (looser):
const L = useLayout();
const stagger = L.pick(/*wide*/ 3, /*square*/ 4, /*tall*/ 5); // pick(wide, square, tall)
const start = i * stagger;
Patterns: cascade (lists/features) · center-out (logo/hero rows: delay = Math.abs(i - mid) * stagger) · deterministic random (particles: rand(i) for delay/offset) · beat-synced (snap start to music beat frames — see ../remotion-music-picker/SKILL.md). One thing enters the eye at a time.
pickis the standard per-aspect selector onuseLayout(). If it isn't onLayoutyet, add it inaspect.ts:pick: <T,>(wide: T, square: T, tall: T): T => kind === "wide" ? wide : kind === "tall" ? tall : square,
3D motion (@remotion/three)
Drive every transform off useCurrentFrame() (deterministic under ANGLE) — never useFrame. Rotation/orbit = linear (mechanical); entrances/landings = spring with high mass for weight. Keep crisp Persian text as a 2D <AbsoluteFill> overlay above <ThreeCanvas>. Let StudioEffects (bloom + DOF + vignette) carry the cinematic polish in one component; tune camera.fov/position.z per aspect so the subject fills the frame.
The pro workflow — 5 passes, IN ORDER
Polishing before timing is locked wastes the most time.
- Reference — decide the feel before code; pick style (
../remotion-design-styles/SKILL.md), type (../persian-fonts/SKILL.md), composition (../remotion-template-composition/SKILL.md), per-aspect rules (../remotion-aspect-ratios/SKILL.md). Write the beat list ("logo in → tagline → 3 features cascade → CTA → out"). - Blocking — every element at its final position with crude
interpolatefades, no easing. Fix off-screen/cropping in all three aspects NOW. - Timing — lock frame counts, stagger, beats, holds, transitions. Watch at full speed repeatedly. Mood lives here.
- Polish — swap linear for easing/springs; add anticipation + overshoot/settle, follow-through, secondary motion, arcs, squash/stretch;
StudioEffectsfor 3D; wire SFX (../remotion-sound-effects/SKILL.md) + music sync (../remotion-music-picker/SKILL.md) to the locked frames. - Review — scrub frame-by-frame + full speed against the checklist below.
Top amateur mistakes → fixes (review gate)
- Linear motion → ease/spring · no anticipation/overshoot → dip-then-launch / back bezier
- Everything on one frame → stagger · forgot
clamp→ clamp both ends - Hardcoded 30fps →
useVideoConfig().fps+sec() useFrame/random/Date.now()→useCurrentFrame+rand- Pixel-hardcoded sizes →
vmin/unit+pick/isWide/isSquare/isTall - Over-animating → one idea per beat · no hold → real hold sized to reading
- Exit speed = entrance speed → exits sharper · dead holds →
sinbob/breathe/shimmer - Color hardcoded → read from
colorSchemaprops
Pre-ship motion checklist
- No linear easing anywhere except mechanical continuous motion (rotation/marquee).
- Entrances ease-out; exits ease-in and sharper than entrances.
- Every
interpolatethat could overshoot hasextrapolateLeft/Right: "clamp". - At least one anticipation (dip) and one overshoot-and-settle in the piece.
- Grouped elements stagger; trailing parts follow through (looser spring).
- No dead holds — held heroes have a subtle
sinbreathe/shimmer. - Stagger/scale tuned per aspect via
pick; verified in 16:9 / 1:1 / 9:16. - All timing from
sec()/fps; no hardcoded 30; nouseFrame/random/Date.now. - One clear hero moment with the biggest motion; the eye always knows where to look.
- Re-render twice → pixel-identical (deterministic).
Related: ../remotion-design-styles/SKILL.md, ../remotion-aspect-ratios/SKILL.md, ../remotion-template-composition/SKILL.md, ../remotion-character-design/SKILL.md, ../remotion-sound-effects/SKILL.md, ../remotion-music-picker/SKILL.md, ../persian-fonts/SKILL.md, ../flatrender-template-seo/SKILL.md.