Files
Soroush Asadi 4ffbcac9ee refactor: bundle the whole template suite under flat-artist/ + fix references
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>
2026-06-21 19:31:53 +03:30

11 KiB
Raw Permalink Blame History

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 (useLayoutisWide/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, or useEffect-driven motion. For "randomness" use rand(seed) from anim.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) 814
Standard reveal 1828
Hero entrance 2840
Scene transition 1220
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.

pick is the standard per-aspect selector on useLayout(). If it isn't on Layout yet, add it in aspect.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.

  1. 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").
  2. Blocking — every element at its final position with crude interpolate fades, no easing. Fix off-screen/cropping in all three aspects NOW.
  3. Timing — lock frame counts, stagger, beats, holds, transitions. Watch at full speed repeatedly. Mood lives here.
  4. Polish — swap linear for easing/springs; add anticipation + overshoot/settle, follow-through, secondary motion, arcs, squash/stretch; StudioEffects for 3D; wire SFX (../remotion-sound-effects/SKILL.md) + music sync (../remotion-music-picker/SKILL.md) to the locked frames.
  5. 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 → sin bob/breathe/shimmer
  • Color hardcoded → read from colorSchema props

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 interpolate that could overshoot has extrapolateLeft/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 sin breathe/shimmer.
  • Stagger/scale tuned per aspect via pick; verified in 16:9 / 1:1 / 9:16.
  • All timing from sec()/fps; no hardcoded 30; no useFrame/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.