Files
AISkills/flat-artist/motion-design-principles/SKILL.md
T
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

168 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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) | 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:
```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`.