--- name: motion-design-principles description: 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`, 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: ```tsx 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 | ```tsx 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: ```tsx 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: ```tsx 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: ```tsx 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 ; } ``` **Secondary motion** — never let a held element go dead. Add a tiny `sin` breathe/shimmer: ```tsx 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): ```tsx 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: (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 `` overlay above ``. 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`.