4ffbcac9ee
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>
168 lines
11 KiB
Markdown
168 lines
11 KiB
Markdown
---
|
||
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 <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:
|
||
```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: <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`.
|