feat: design+motion R&D report and 6 professional craft skills
R&D brief (references/design-motion-rnd.md): 2024-2026 design/motion trends, animating-anything craft, Iran-aware asset pipeline, masterpiece + platform playbook. New craft skills: motion-design-principles, scene-transitions, kinetic-typography, video-hooks, particles-and-effects, asset-sourcing — grounded in the Remotion stack. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
---
|
||||
name: asset-sourcing
|
||||
description: How to source, license, AI-generate, prepare, and organize royalty-free assets (footage, images, textures, HDRIs, GLTF/GLB, icons, illustrations) for FlatRender Remotion templates — Iran-aware (geo-blocks), vendored-only, license-firewalled. Use when a template needs real media, when downloading/committing assets into public/, when grading/masking/looping footage or compositing it via Video/OffthreadVideo/Img/staticFile + Ken-Burns, or when generating bespoke assets with local AI models.
|
||||
---
|
||||
|
||||
# Asset sourcing for templates
|
||||
|
||||
Project: `services/remotion/`. Helpers: `src/lib/anim.ts` (`hexToRgba`, `mixHex`, `rand`), `src/lib/aspect.ts` (`useLayout` → `isWide/isSquare/isTall`, `vmin`, `unit`), `src/lib/branding.ts` (`colorSchema`, `BRAND`), `src/lib/fonts.ts` (`FONT` = Vazirmatn, RTL), `src/lib/three-kit.tsx` (`StudioEnv/Lights/Floor/Effects`, `Confetti3D`). Render is **headless Chrome in Docker** — every value derives from `useCurrentFrame()` (never `Math.random`/`Date.now`/`useFrame`; use `rand(i)`).
|
||||
|
||||
## The Iron Rule — vendor everything
|
||||
The Iran environment punishes runtime dependencies. **Download once (VPN if needed), commit into `public/`, reference with `staticFile()`.** Never put `https://…` in a shipped template — a geo-block or flaky tunnel kills the render mid-frame. Mirror npm/NuGet/Docker via Nexus (`mirror.soroushasadi.com`); asset *binaries* are sourced by hand. **Record the license at acquisition time, not later.** `public/` today holds only `fonts/` — you build the rest.
|
||||
|
||||
## License taxonomy (know cold — this is the firewall)
|
||||
| Class | Examples | Ship? |
|
||||
|---|---|---|
|
||||
| CC0 / Public Domain / Pixabay / Pexels / Unsplash | Poly Haven, ambientCG, Kenney, Mixkit | ✅ free, no credit — **default target** |
|
||||
| CC-BY | many Sketchfab, Bensound | ⚠️ ship only with a tracked on-screen/end-card credit |
|
||||
| CC-BY-SA | some Wikimedia | ❌ share-alike can infect our proprietary template |
|
||||
| CC-BY-NC | "free for personal" tiers | ❌ we are a **paid** product = commercial |
|
||||
| Editorial / rights-managed | news/celebrity stock | ❌ |
|
||||
| Paid stock | Envato, Adobe, Shutterstock | ✅ per license — **keep the receipt/PDF** |
|
||||
|
||||
No license row = unknown license = **do not ship**.
|
||||
|
||||
## Sourcing map (CC0 / no-attribution first) + Iran access
|
||||
| Type | Best CC0 sources | Commit to | Iran access |
|
||||
|---|---|---|---|
|
||||
| Footage (H.264 MP4, right-sized) | Pexels Video, Pixabay Video, Mixkit, Coverr, Videvo (filter CC0) | `public/footage/{nature,business,abstract}/` | Pixabay/Mixkit/Coverr OK; Pexels VPN-ish |
|
||||
| Images | Pexels, Pixabay, Unsplash, StockSnap, Burst | `public/images/` | Pixabay OK; Pexels/Unsplash VPN-ish |
|
||||
| Textures / overlays | Poly Haven, ambientCG; grain/light-leak/dust CC0 clips | `public/textures/`, `public/overlays/` | OK |
|
||||
| HDRIs (1k–2k for render speed) | Poly Haven, ambientCG | `public/hdri/` | OK |
|
||||
| 3D (**prefer GLB** over glTF+textures) | Poly Haven Models, Kenney, Khronos glTF samples, Sketchfab (check each) | `public/models/` | Poly Haven OK; Sketchfab VPN-ish |
|
||||
| Icons (bundle via Nexus npm, **never CDN**) | Lucide, Tabler, Heroicons, Phosphor | npm dep | npm via Nexus OK |
|
||||
| Illustrations (recolorable **SVG**) | unDraw, Open Peeps, Humaaans | `public/illustrations/` | OK |
|
||||
|
||||
For Persian/Iran imagery search English terms ("Tehran", "Iranian food") + self-shot/local stock. **Sanction-blocked at account/payment: Adobe Stock, Envato** — use a foreign account/partner or skip. **Mitigation: do one batched "asset run" over a stable tunnel, commit binaries, render never touches the open internet again.** Draco-compress GLBs (`gltf-pipeline -i in.glb -o out.glb -d`), keep low-poly for headless render speed.
|
||||
|
||||
## AI-generated assets — when it's right
|
||||
- **Use when:** the asset doesn't exist as stock (specific Persian cultural scene, branded mascot), you need consistency across a template set (reference-image control), or it beats a 5-site license hunt.
|
||||
- **Don't when:** clean CC0 already exists, you need photographic authenticity, or a free tier's **commercial license is unclear** (watermarks / non-commercial = legal landmine for a paid product).
|
||||
- **Iran-pragmatic:** self-host open models — **HunyuanVideo 1.5** (~RTX 4090, no geo-block/payment/watermark) for video; **FLUX/SDXL** locally for image/texture/illustration. Hosted SaaS (Runway, Kling) only when local quality falls short and a VPN+foreign-account path exists. **Always record prompt + tool + plan-tier + date** in the asset's `.license.txt` sidecar.
|
||||
|
||||
## Preparing footage in Remotion (composite, grade, mask, loop)
|
||||
**Primitives:** `<OffthreadVideo>` = default for **all** video in a render (FFmpeg extraction, deterministic, no seek drift). `<Video>` = preview only. `<Img>` over raw `<img>` (waits for load → no half-loaded frames). `staticFile()` for every vendored asset.
|
||||
|
||||
```tsx
|
||||
import { OffthreadVideo, Img, staticFile, useCurrentFrame, interpolate, Easing } from "remotion";
|
||||
import { useLayout } from "./lib/aspect";
|
||||
|
||||
const frame = useCurrentFrame();
|
||||
const L = useLayout();
|
||||
|
||||
// Ken-Burns: overscan ≥1 so no edges reveal; cover + center crops cleanly in all 3 aspects.
|
||||
const scale = interpolate(frame, [0, 150], [1.08, 1.2], { extrapolateRight: "clamp" });
|
||||
const ty = interpolate(frame, [0, 150], [0, L.vmin(-30)], { easing: Easing.out(Easing.cubic), extrapolateRight: "clamp" });
|
||||
|
||||
<OffthreadVideo
|
||||
src={staticFile("footage/nature/forest-loop.mp4")}
|
||||
style={{ width: "100%", height: "100%", objectFit: "cover",
|
||||
transform: `scale(${scale}) translateY(${ty}px)`,
|
||||
filter: "contrast(1.08) saturate(1.15) brightness(0.96)" }} // grade
|
||||
/>
|
||||
```
|
||||
|
||||
| Job | Pattern |
|
||||
|---|---|
|
||||
| **Color grade** | per-layer CSS `filter` (`contrast/saturate/brightness/hue-rotate`); build a shared `lib/grades.ts` (`warm`, `teal-orange`, `mono`, `filmic`) so palette can drive `hue-rotate`/`saturate`. Heavy grade → pre-grade in DaVinci Resolve (free), then commit. |
|
||||
| **Masking / keying** | no native keyer — pre-key in Resolve/AE, export **alpha** (ProRes 4444 or WebM/VP9 alpha), then `<OffthreadVideo>`. Shape masks via CSS `maskImage`/`clipPath` + `hexToRgba` gradients, or SVG `<mask>`. |
|
||||
| **Seamless loop** | source loop-designed clips (Coverr/Mixkit) or mirror-pingpong; `<OffthreadVideo loop>` once first/last frames match; crossfade-to-self with overlapping `<Sequence>` for imperfect footage. |
|
||||
| **Overlays (cheap "authentic" layer)** | stack grayscale-on-black/white clips: **screen** for light-leaks/bokeh/dust, **overlay/soft-light** for grain, **multiply** for vignettes/paper. Keep palette-independent. **Animated grain must move** — offset `background-position` per frame or jitter SVG `feTurbulence` `seed`. |
|
||||
| **Per-aspect crop** | `objectFit:"cover"` + center-safe framing; branch focal point on `L.isWide/isSquare/isTall` (or the proposed `L.pick(wide,square,tall)`) so the subject never crops out. |
|
||||
|
||||
HDRIs/GLBs: feed `staticFile("hdri/…")` into `three-kit`'s `StudioEnv`; load models with `useGLTF(staticFile("models/…glb"))`, idle-bob with `Math.sin(frame/fps)` (driven by `useCurrentFrame`, **not** `useFrame`).
|
||||
|
||||
## Library structure + attribution firewall
|
||||
Create under `public/`: `footage/{nature,business,abstract}/`, `overlays/`, `images/`, `textures/`, `hdri/`, `models/`, `icons/`, `illustrations/`, plus **`assets.json`** + **`ASSETS.md`**. Lowercase-kebab names, no spaces. Every asset gets one `assets.json` row **at download time**:
|
||||
|
||||
```json
|
||||
{ "file": "footage/nature/forest-loop.mp4", "source": "Pexels",
|
||||
"url": "https://www.pexels.com/video/...", "author": "Name",
|
||||
"license": "Pexels", "attribution_required": false, "commercial_ok": true,
|
||||
"acquired": "2026-06-21", "notes": "1080p H.264, loops clean" }
|
||||
```
|
||||
Sidecar `.license.txt` next to AI assets (prompt + tool + date) and paid receipts. A **CI validation script** asserts every file in the media folders has a matching row with `commercial_ok:true`, else fails the build — this is the firewall. `ASSETS.md` is the generated human/legal-readable table. `attribution_required:true` must surface a credit on a shippable surface (end-card/footer). If the repo bloats, move large media to MinIO (already in stack) with a `predeploy` sync into `public/` — but present at render time.
|
||||
|
||||
## Checklist (before committing an asset / shipping a template)
|
||||
- [ ] Vendored in `public/…` and referenced via `staticFile()` — **no external URL** anywhere in the template.
|
||||
- [ ] `assets.json` row added with `commercial_ok:true`; `.license.txt`/receipt for AI/paid; CC-BY credits surfaced.
|
||||
- [ ] Right-sized (don't ship 4K into a 1080p comp); video is H.264 MP4 played via `<OffthreadVideo>`; images via `<Img>`.
|
||||
- [ ] GLB (not glTF+loose textures), Draco-compressed, low-poly; HDRI 1k–2k.
|
||||
- [ ] Footage graded through `lib/grades.ts`; overlay grain/light-leak **moves** per frame; loops are seamless.
|
||||
- [ ] Ken-Burns overscans (start scale ≥ 1.05) and `objectFit:cover` crops cleanly in 16:9 / 1:1 / 9:16 with subject in frame.
|
||||
- [ ] Re-render twice → identical (deterministic; nothing pulled from network/random/date).
|
||||
|
||||
Related: `remotion-design-styles`, `remotion-template-composition`, `remotion-aspect-ratios`, `remotion-character-design`, `remotion-svg-colors`, `remotion-music-picker`, `remotion-sound-effects`, `persian-fonts`, `flatrender-template-seo`.
|
||||
@@ -0,0 +1,95 @@
|
||||
---
|
||||
name: kinetic-typography
|
||||
description: How to build animated-text systems for FlatRender Remotion templates — word/line/char reveals, mask wipes, typewriter, scale-pops, highlight sweeps, text-on-path, and number counters — Persian/RTL-aware and reusable. Use whenever a template's hero, caption, quote, title, price, or any text is the thing that moves. Persian is the priority; split by WORD, never by character.
|
||||
---
|
||||
|
||||
# Kinetic typography (animated text systems)
|
||||
|
||||
Type is a first-class motion element here, not a label. A masterpiece text shot is ~5 layers: the right split, eased per-unit timing, a hold sized to a real read, legibility over the background, and a single hero word. Amateurs stop at "the text fades in."
|
||||
|
||||
## The one rule
|
||||
Every value is a pure function of `useCurrentFrame()`. **Never** `useFrame`, `Math.random()`, `Date.now()`, `setState`, or `useEffect`-driven motion — the headless renderer samples frames out of order. For "random" jitter use `rand(seed)` from `lib/anim.ts`. Drive timing off `useVideoConfig().fps`; define `const sec = (s: number) => Math.round(s * fps)` — never hardcode `30`.
|
||||
|
||||
## Persian / RTL — get this right first (it's an Iran-facing product)
|
||||
- **Split by WORD, not character.** Persian script is connected/cursive — splitting on chars shatters letterforms and joins. Latin char-reveals are fine; Persian is word- or line-only. A safe split is `text.split(/\s+/).filter(Boolean)` — this **preserves ZWNJ** (نیمفاصله, ``) inside words like «میشود» because ZWNJ is not whitespace. Never `.split("")` or `.replace(//g, …)` on Persian.
|
||||
- Every text node: `fontFamily: FONT` (Vazirmatn, from `lib/fonts.ts`), `direction: "rtl"`, align right or center. The existing `KineticQuote.tsx` hardcodes Georgia/serif + pixel sizes + no RTL — **do not copy that**; it's a Latin-only relic.
|
||||
- Persian needs weight (headings 700–900) and `lineHeight: 1.4–1.6`. Numerals: pick Persian (۱۲۳ via `toLocaleString('fa-IR')`) or Latin and stay consistent; prices/years are usually Persian digits. See `persian-fonts`.
|
||||
- For RTL word reveals, the wrapping container does the ordering — keep `flexWrap: "wrap"` + `direction: "rtl"` and let words flow; don't manually reverse the array.
|
||||
|
||||
## Size & position from layout tokens, never pixels
|
||||
Read `useLayout()` from `lib/aspect.ts`: `vmin(n)`, `unit`, `isWide/isSquare/isTall`. Hero type ≈ `vmin(80–110)`, body ≈ `vmin(28–40)`. Tune timing/scale per aspect — wider reads faster (tighter stagger), tall reads slower (looser). Add this `pick` helper to `Layout` (per R&D Tier-0) and use it:
|
||||
```ts
|
||||
const pick = <T,>(w: T, s: T, t: T) => (L.isWide ? w : L.isSquare ? s : t);
|
||||
const stagger = pick(2, 3, 4); // frames between units
|
||||
```
|
||||
|
||||
## Animation patterns (all driven by `frame - start`)
|
||||
|
||||
| Pattern | Recipe | Persian-safe? |
|
||||
|---|---|---|
|
||||
| **Word reveal** (default) | split words; per word `start = i*stagger`; `spring({frame: frame-start, fps})` → `translateY(vmin)` + `opacity` | ✅ word-split |
|
||||
| **Line reveal** | wrap by line in `<Sequence>`s; each line springs up behind a `clip-path` edge | ✅ |
|
||||
| **Char reveal / scatter** | split chars, per-char delay; rotate/scale in | ❌ Latin only |
|
||||
| **Mask wipe** | `clipPath: inset(0 ${100-p}% 0 0)` (RTL: wipe from right → `0 0 0 ${100-p}%`); `p = interpolate(frame,[a,b],[0,100],{extrapolateRight:"clamp", easing: Easing.out(Easing.cubic)})` | ✅ |
|
||||
| **Typewriter** | `text.slice(0, Math.floor(interpolate(frame,[a,b],[0, words.length])))` joined — **slice by WORD for Persian**, by char only for Latin; add a blinking caret `frame % sec(0.8) < sec(0.4)` | ✅ word-slice |
|
||||
| **Scale-pop ("ta-da")** | `scale = spring({config:{damping:12,mass:0.6,stiffness:180}})` or `Easing.bezier(0.34,1.56,0.64,1)` overshoot→settle | ✅ |
|
||||
| **Highlight sweep** | gradient bar/`background-clip:text` shifting `background-position` per frame, or an accent rect growing under a key word | ✅ |
|
||||
| **Text-on-path** | SVG `<textPath href="#p">`; animate `startOffset` by frame — Latin/numeric only (RTL on a path is unreliable) | ❌ |
|
||||
| **Number counter** | `Math.round(interpolate(frame,[a,b],[0, target],{extrapolateRight:"clamp", easing: Easing.out(Easing.cubic)}))` then `toLocaleString('fa-IR')` | ✅ (format fa) |
|
||||
| **Variable-weight pulse** | Vazirmatn ships a variable axis: `fontVariationSettings: \`'wght' ${interpolate(frame,[a,b],[300,900])}\`` (needs the variable woff2 registered in `fonts.ts`) | ✅ |
|
||||
|
||||
### Reusable word-reveal component (the workhorse — Persian-correct, aspect-aware)
|
||||
```tsx
|
||||
const RevealText: React.FC<{ text: string; start: number; color: string }> = ({ text, start, color }) => {
|
||||
const frame = useCurrentFrame();
|
||||
const { fps } = useVideoConfig();
|
||||
const L = useLayout();
|
||||
const pick = <T,>(w: T, s: T, t: T) => (L.isWide ? w : L.isSquare ? s : t);
|
||||
const stagger = pick(2, 3, 4);
|
||||
const words = text.split(/\s+/).filter(Boolean); // keeps ZWNJ
|
||||
return (
|
||||
<div style={{ display: "flex", flexWrap: "wrap", justifyContent: "center",
|
||||
direction: "rtl", fontFamily: FONT, fontWeight: 800, fontSize: L.vmin(96),
|
||||
lineHeight: 1.4, color, gap: `0 ${L.vmin(18)}px`, maxWidth: "86%",
|
||||
textShadow: `0 ${L.vmin(2)}px ${L.vmin(20)}px rgba(0,0,0,.6)` }}>
|
||||
{words.map((w, i) => {
|
||||
const s = spring({ frame: frame - start - i * stagger, fps,
|
||||
config: { damping: 16, mass: 0.7, stiffness: 120 } });
|
||||
return (
|
||||
<span key={i} style={{ display: "inline-block", opacity: s,
|
||||
transform: `translateY(${interpolate(s, [0, 1], [L.vmin(28), 0])}px)` }}>
|
||||
{w}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
```
|
||||
Follow-through upgrade: give a trailing accent word a *looser* spring (`damping: 6`) so it settles last.
|
||||
|
||||
## Easing & spring (linear is the sound of an amateur)
|
||||
- Entrances → **ease-out** default (`Easing.out(Easing.cubic)`); hero titles → `Easing.bezier(0.16,1,0.3,1)`. Exits → **ease-in, sharper than the entrance**. Snappy pop → back bezier `(0.34,1.56,0.64,1)`.
|
||||
- `interpolate` for exact marks — **always `extrapolateLeft/Right: "clamp"`** (forgetting it is the #1 drift bug). `spring` for organic feel. Combine: `interpolate(spring(...), [0,1], [vmin(28), 0])`.
|
||||
- Spring cheats: clean reveal `{damping:200,mass:0.5,stiffness:200}` · default pop `{damping:12,mass:0.6,stiffness:180}` · bouncy `{damping:8,mass:1,stiffness:120}` · trailing wobble `{damping:6,mass:1,stiffness:80}`.
|
||||
|
||||
## Timing budgets (@ whatever `fps` is)
|
||||
Micro pop 8–14f · word stagger 2–4f · standard reveal 18–28f · hero entrance 28–40f · **hold = a comfortable read** (≥ `sec(0.7)` per text element before the next competes). Cut frames before adding them — over-animating reads as amateur. Anticipation: dip below start before launch (`interpolate(frame,[0,6,30],[0,-0.12,1])`).
|
||||
|
||||
## Legibility over busy / 3D / video backgrounds
|
||||
- Scrim or `textShadow: 0 0 vmin(20) rgba(0,0,0,.7)`, or a semi-transparent panel behind text.
|
||||
- Gradient text: `WebkitBackgroundClip: "text"`, transparent fill, plus a `drop-shadow` for edge separation.
|
||||
- Colors come from `colorSchema` props (`accentColor/secondaryColor/backgroundColor/textColor` via `lib/branding.ts`) — pass user hex through `mixHex`/`hexToRgba` so a garish value doesn't break the look. Never hardcode `#fff`.
|
||||
- Captions (TikTok/Reels/Shorts) = high-contrast white/yellow + black outline, lower-middle third, inside the tightest safe zone. See `remotion-aspect-ratios`.
|
||||
|
||||
## Checklist
|
||||
- [ ] Persian text split by WORD; ZWNJ preserved; `direction:"rtl"` + `fontFamily: FONT`.
|
||||
- [ ] All sizes via `vmin`/`unit`; timing/stagger via `pick(...)` per aspect — verified in 16:9, 1:1, 9:16.
|
||||
- [ ] No linear easing; ≥1 overshoot-and-settle; staggered, not all on frame 0.
|
||||
- [ ] Every `interpolate` clamps both ends; no `useFrame`/`random`/`Date.now`; `fps` not `30`.
|
||||
- [ ] Numbers formatted (`fa-IR`) and consistent; counter eases out.
|
||||
- [ ] Legible over the background (scrim/shadow); colors from props.
|
||||
- [ ] A real hold sized to reading; longest Persian string doesn't overflow, shortest doesn't look empty.
|
||||
- [ ] Re-render twice → identical pixels (deterministic).
|
||||
|
||||
Related: `remotion-template-composition`, `persian-fonts`, `remotion-aspect-ratios`, `remotion-design-styles`, `remotion-svg-colors`, `remotion-sound-effects`, `remotion-music-picker`, `flatrender-template-seo`.
|
||||
@@ -0,0 +1,167 @@
|
||||
---
|
||||
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`). **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`), type (`persian-fonts`), composition (`remotion-template-composition`), per-aspect rules (`remotion-aspect-ratios`). 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`) + music sync (`remotion-music-picker`) 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`, `remotion-aspect-ratios`, `remotion-template-composition`, `remotion-character-design`, `remotion-sound-effects`, `remotion-music-picker`, `persian-fonts`, `flatrender-template-seo`.
|
||||
@@ -0,0 +1,120 @@
|
||||
---
|
||||
name: particles-and-effects
|
||||
description: How to add production-value FX — confetti, sparkles, bokeh, light leaks, dust, smoke, glow, lens flare, film grain, chromatic aberration, vignette, camera shake — to FlatRender Remotion templates, in both 2D (SVG/CSS) and 3D (@remotion/three). Use when a template needs atmosphere, finishing texture, particle systems, or a celebratory/cinematic hit. Every effect is a deterministic function of useCurrentFrame() — never Math.random.
|
||||
---
|
||||
|
||||
# Particles & effects for Remotion
|
||||
|
||||
Project: `services/remotion/` (Remotion 4 + `@remotion/three`, R3F v9, `gl="angle"`). Effects are the **8th finishing layer** — the thing that separates "made in a tool" from "made by a studio." A flat, ungrainy, perfectly-locked frame reads as AI/template. Imperfect-by-design wins.
|
||||
|
||||
## The one non-negotiable rule
|
||||
Render is headless Chrome sampling frames out of order, in parallel. **Every particle position, every grain offset, every flicker MUST derive from `useCurrentFrame()`.** Never `Math.random()`, `Date.now()`, `useFrame` (R3F), `useState`, or `useEffect` motion. Use `rand(seed)` from `src/lib/anim.ts` for stable per-index pseudo-randomness, and `rand(i + frame)`-style offsets when you want it to *move*. Re-render twice → identical bytes, or it's wrong.
|
||||
|
||||
Helpers you build on:
|
||||
- `anim.ts` — `rand(i)` (deterministic 0..1), `hexToRgba(hex,a)`, `mixHex(a,b,t)`.
|
||||
- `aspect.ts` — `useLayout()` → `isWide/isSquare/isTall`, `vmin(n)`, `unit`, and `pick(wide,square,tall)`. **Scale particle COUNT and SIZE per aspect** — a tall 9:16 needs fewer, bigger sparkles than a wide 16:9.
|
||||
- `branding.ts` — `colorSchema` props are `accentColor / secondaryColor / backgroundColor / textColor`. FX color comes from these so the studio recolors them.
|
||||
- `three-kit.tsx` — `StudioEnv`, `StudioLights`, `StudioFloor`, `StudioEffects` (bloom+DOF+vignette), `Confetti3D`.
|
||||
|
||||
## 2D vs 3D — pick per effect
|
||||
- **2D (SVG/CSS)** is the default: cheap, crisp, no WebGL. Confetti, sparkles, grain, light leaks, vignette, aberration, camera shake — all better/cheaper in 2D as an `<AbsoluteFill>` overlay, even on top of a 3D scene.
|
||||
- **3D (@remotion/three)** when the effect must respond to scene lighting/depth: volumetric bloom, real bokeh/DOF, `emissive` glow that bloom picks up, 3D confetti with perspective. Let `StudioEffects` do bloom/DOF/vignette in ONE component — don't re-roll them.
|
||||
- Persian text NEVER goes in 3D — keep it as a 2D overlay above `<ThreeCanvas>`.
|
||||
|
||||
## Effect → recipe table
|
||||
|
||||
| Effect | Layer | Core technique | Determinism |
|
||||
|---|---|---|---|
|
||||
| **Confetti (2D)** | overlay | N `<rect>`/`<path>`, `rand(i)` for x/rot/color; `y` = `(frame*speed + rand(i)*span) % span` | `rand(i)` seed |
|
||||
| **Confetti (3D)** | scene | reuse `Confetti3D` from three-kit | built-in |
|
||||
| **Sparkles / shine** | overlay | 4-point star SVG, twinkle `opacity = abs(sin((frame+rand(i)*60)/12))`, scale pulse | `rand(i)` |
|
||||
| **Bokeh** | bg | big blurred radial-gradient circles drifting on `sin(frame/period)`, low opacity, `mix-blend:screen` | per-circle seed |
|
||||
| **Light leaks** | overlay | warm radial/linear gradient sweeping across via `interpolate(frame,...)` translate, `mix-blend:screen` | frame |
|
||||
| **Dust motes** | overlay | tiny dim dots, slow upward drift + lateral `sin`, `rand` size/speed | `rand(i)` |
|
||||
| **Smoke / fog** | bg/3D | 2D: layered blurred blobs drifting+scaling; 3D: stacked transparent planes | frame |
|
||||
| **Glow** | any | 2D `filter:drop-shadow(0 0 Npx accent)` / `textShadow`; 3D `emissive`+`emissiveIntensity`, `toneMapped={false}`, let bloom bloom it | static |
|
||||
| **Lens flare** | overlay | bright core + chromatic ring sprites along a line from a light point, opacity by angle/frame | frame |
|
||||
| **Film grain** | top | SVG `feTurbulence` with per-frame `seed`, `mix-blend:overlay`, low opacity — MUST animate or it looks frozen | frame |
|
||||
| **Chromatic aberration** | top | duplicate layer, offset R/B channels ±1–3px, strongest at impact frames | frame |
|
||||
| **Vignette** | top | `boxShadow: inset 0 0 vmin(600) rgba(0,0,0,.6)` or `StudioEffects` in 3D | static |
|
||||
| **Camera shake** | root | translate whole frame by `rand(frame)`-driven jitter, decaying after an impact | `rand(frame)` |
|
||||
|
||||
## Deterministic particle field (the pattern to memorize)
|
||||
```tsx
|
||||
const frame = useCurrentFrame();
|
||||
const { vmin, pick } = useLayout();
|
||||
const count = pick(60, 48, 36); // fewer on tall
|
||||
{Array.from({ length: count }).map((_, i) => {
|
||||
const x = rand(i) * 100; // % of width
|
||||
const drift = Math.sin((frame + rand(i + 9) * 200) / 40) * 3;
|
||||
const fall = (frame * (0.3 + rand(i + 1) * 0.5) + rand(i + 5) * 120) % 120;
|
||||
const twinkle = Math.abs(Math.sin((frame + rand(i + 2) * 60) / 12));
|
||||
return <div key={i} style={{
|
||||
position: "absolute", left: `${x + drift}%`, top: `${fall - 10}%`,
|
||||
width: vmin(6), height: vmin(6), opacity: twinkle,
|
||||
background: i % 2 ? accentColor : secondaryColor,
|
||||
transform: `rotate(${frame * 2 + rand(i) * 360}deg)`,
|
||||
}} />;
|
||||
})}
|
||||
```
|
||||
Notice: `rand(i)` = stable identity per particle; `frame` = motion; `% span` = seamless wrap; aspect drives count via `pick`.
|
||||
|
||||
## Animated film grain (SVG — the cheapest authenticity layer)
|
||||
```tsx
|
||||
<svg style={{ position: "absolute", inset: 0, mixBlendMode: "overlay", opacity: 0.08 }}>
|
||||
<filter id="grain">
|
||||
<feTurbulence type="fractalNoise" baseFrequency="0.9"
|
||||
numOctaves="2" seed={frame % 100} stitchTiles="stitch" />
|
||||
</filter>
|
||||
<rect width="100%" height="100%" filter="url(#grain)" />
|
||||
</svg>
|
||||
```
|
||||
`seed={frame % 100}` is what makes it crawl. Keep opacity 0.05–0.12. For paper/vignette use `mix-blend:multiply` instead.
|
||||
|
||||
## Chromatic aberration & impact-driven FX
|
||||
Aberration should be **strongest at impacts** (a hard cut, the hero reveal, a confetti burst) and near-zero otherwise:
|
||||
```tsx
|
||||
const ab = interpolate(frame, [hit - 2, hit, hit + 8], [0, vmin(4), 0], { extrapolateLeft: "clamp", extrapolateRight: "clamp" });
|
||||
```
|
||||
Render the content twice, offset the red copy `translateX(-ab)` `mix-blend:screen` and the blue copy `translateX(+ab)`. Same `interpolate` curve also drives a one-shot camera-shake amplitude — things calm down fast.
|
||||
|
||||
## Camera shake (subtle continuous + impact)
|
||||
```tsx
|
||||
// continuous "frame alive" drift — tiny, always on
|
||||
const driftX = Math.sin(frame / 50) * vmin(3) + (rand(frame) - 0.5) * vmin(1);
|
||||
// impact shake — decays
|
||||
const amp = interpolate(frame, [hit, hit + 12], [vmin(14), 0], { extrapolateLeft: "clamp", extrapolateRight: "clamp" });
|
||||
const shake = (rand(frame * 7) - 0.5) * amp;
|
||||
// apply to a root <AbsoluteFill style={{ transform: `translate(${driftX+shake}px, ${...}px)` }}>
|
||||
```
|
||||
A locked, perfectly-still frame reads amateur. A *tiny* always-on drift makes it feel hand-held and alive — keep it under ~`vmin(4)` or it's distracting.
|
||||
|
||||
## 3D glow & bloom
|
||||
Make a material glow into bloom: `<meshStandardMaterial emissive={accentColor} emissiveIntensity={2} toneMapped={false} />`, then mount `<StudioEffects bloom={0.9} />`. For sparkly metal confetti raise `metalness`. Drive every transform off `useCurrentFrame()` (deterministic under ANGLE), rotation = `linear` (mechanical), entrances = `spring` with high mass.
|
||||
|
||||
## Reusable components — make these, don't inline
|
||||
Put shared FX in `src/lib/fx.tsx` so every template gets the same texture:
|
||||
- `<GrainOverlay opacity? blend? />` — animated `feTurbulence`.
|
||||
- `<Vignette strength? />` — inset boxShadow.
|
||||
- `<Confetti2D colors count? burstFrame? />` — burst (spring spread) vs rain (continuous fall) modes.
|
||||
- `<Sparkles colors count? area? />` — twinkling 4-point stars.
|
||||
- `<Bokeh colors count? />` + `<LightLeak color from to />` — bg/overlay atmosphere.
|
||||
- `<Aberration amount /> <CameraShake amount />` — finishing pair, wrap the whole comp.
|
||||
Each takes `colorSchema` colors so the studio picker recolors the FX, and reads `useLayout()` for per-aspect count/size.
|
||||
|
||||
## Restraint — FX amplify a hero, they are not the show
|
||||
- One celebratory burst on the **hero moment**, not raining the whole video. Often **silence before** + confetti + sparkle SFX on the same frame (see `remotion-sound-effects`).
|
||||
- Finishing texture (grain, vignette, drift) is *subtle and always-on*; spectacle (confetti, flare, big aberration) is *brief and on a beat*.
|
||||
- Don't stack 6 effects at full strength — that reads as a tool preset. Grain at 0.08, vignette at 0.5, aberration only at impacts.
|
||||
- All FX color from `colorSchema`; pass a user's garish hex through `mixHex(hex, background, 0.2)` so it doesn't blow out.
|
||||
|
||||
## Pre-ship checklist
|
||||
- [ ] Zero `Math.random` / `Date.now` / `useFrame` — only `rand()` + `frame`. Re-render twice → identical.
|
||||
- [ ] Grain is *animated* (per-frame seed), not frozen.
|
||||
- [ ] Particle count & size scale per aspect via `pick`/`vmin` — verified in 16:9, 1:1, 9:16; particles stay in the safe zone, never crop Persian text.
|
||||
- [ ] Every `interpolate` has `extrapolateLeft/Right: "clamp"` — no drift, no negative opacity.
|
||||
- [ ] Spectacle FX land on a beat / the hero; texture FX are subtle & continuous.
|
||||
- [ ] FX colors read from `colorSchema`; a continuous camera drift keeps the frame alive.
|
||||
- [ ] 3D glow uses `emissive`+`toneMapped={false}` + `StudioEffects` (not hand-rolled bloom).
|
||||
|
||||
Related: `remotion-design-styles`, `remotion-character-design`, `remotion-aspect-ratios`, `remotion-template-composition`, `remotion-sound-effects`, `remotion-music-picker`, `remotion-svg-colors`, `persian-fonts`, `remotion-template-catalog`, `flatrender-template-seo`.
|
||||
@@ -0,0 +1,244 @@
|
||||
# FlatRender Design & Motion R&D — Trends + Professional Craft
|
||||
|
||||
> Single-source R&D brief for the FlatRender Remotion engine (`services/remotion`). Stack: **Remotion 4 + `@remotion/three`** (R3F v9, `gl="angle"`), Persian-first (Vazirmatn, RTL), three mandatory aspects (16:9 / 1:1 / 9:16), color-customizable templates driven by `colorSchema` props. Operating context: **Iran** — geo-blocked CDNs, sanctioned SaaS dashboards, reachable Nexus mirror (`mirror.soroushasadi.com`). Render is headless Chrome in Docker, so **every value must be a pure function of `useCurrentFrame()`** and **every asset must be vendored** into `public/`.
|
||||
>
|
||||
> Existing grounding files: `src/lib/anim.ts`, `aspect.ts`, `branding.ts`, `fonts.ts`, `three-kit.tsx`, `kit.tsx`; templates in `src/templates.tsx`; `public/` currently holds only `fonts/`.
|
||||
|
||||
---
|
||||
|
||||
## The two meta-truths to keep over everything
|
||||
|
||||
1. **Imperfect-by-design beats glossy.** As feeds fill with AI-perfect imagery, deliberate imperfection — grain, texture, hand-rendered type, natural-feeling motion — now signals "a real human made this." Even fully-rendered templates win by *adding back* texture and human-feeling motion. Apply this lens to every trend below.
|
||||
2. **A masterpiece is ~8 finishing layers, not one big thing.** Sound design, micro-easing, a design system, depth/lighting, color grade, pacing, a clear hero moment, and subtle texture. Amateurs stop at "the text animates in." We must finish all eight.
|
||||
|
||||
---
|
||||
|
||||
## 1) Design trends to adopt (each with a concrete how-to in our stack)
|
||||
|
||||
Every trend below survives all three aspects only if you anchor to safe-zone **percentages / `layout.vmin()`**, never absolute pixels. Read `remotion-aspect-ratios` before positioning anything.
|
||||
|
||||
### Typography (type is a first-class motion element, not a label)
|
||||
|
||||
| Trend | When to use | How in our stack |
|
||||
|---|---|---|
|
||||
| **Bold / oversized hero type** (fills 60–90% of frame, clipped by edges) | Logo reveals, promo hooks; strongest on 9:16 | `fitText` from `@remotion/layout-utils` to auto-scale a word to frame width; animate `scale`/`translateY` with `spring()`; parent `overflow:hidden` to clip. |
|
||||
| **Variable-font animation** (`wght`/`wdth`/`slnt` over time) | Premium beat-synced intros | `style={{ fontVariationSettings: \`'wght' ${interpolate(frame,[0,30],[100,900])}\` }}`. **Vazirmatn ships a variable build** — animate its weight axis for Persian hero type. Load via `@font-face` (vendored in `public/fonts/`, never Google CDN at render). |
|
||||
| **Kinetic typography** (word-by-word / line-by-line) | Quotes, captions, fast hooks | Split into spans; per-word `delay = i * staggerFrames`; drive each with `spring({frame: frame - delay, fps})`; combine `translateY` + `opacity` + slight `rotate`. `<Sequence>` per line for timeline clarity. |
|
||||
| **Anti-AI / hand-rendered / scribbled** | Grunge/street/youth/music/events | Pre-make rough-edged SVG/PNG lettering; "draw" it on with a `clipPath`/mask wipe; add `filter:url(#displace)` (SVG `feTurbulence` + `feDisplacementMap`) with a per-frame jitter for photocopy wobble. |
|
||||
| **Chrome / Y2K metallic type** | Hype, music, fashion, "premium" reveals | CSS: layered `linear-gradient` text fill via `background-clip:text` cycling silver→steel→highlight by shifting `background-position` per frame. For real reflections, use Three.js (see liquid-chrome below). |
|
||||
|
||||
### 3D / Blender-look (`@remotion/three`)
|
||||
|
||||
- **Real-time 3D logo reveals** — the default "premium intro." Render `<ThreeCanvas>`, drive camera/object rotation from `useCurrentFrame()` (**never `useFrame`**). Extrude logo via `TextGeometry`/extruded SVG shape, `meshStandardMaterial`, an HDRI `Environment`, `spring()`-driven entrance. Use our `StudioEnv/StudioLights/StudioFloor/StudioEffects` from `three-kit.tsx`.
|
||||
- **"Plushcore" / soft-3D / claymation** — friendly counter to hard chrome. Pre-render GLTF in Blender (subsurface/soft shaders), import via `useGLTF`; bobbing idle = `Math.sin(frame/fps)` on position. 2D fake-3D = big soft inner-shadows + highlight gradients in CSS.
|
||||
- **Mixed 2D/3D** — one of the strongest 2026 looks. Layer a `<ThreeCanvas>` behind/within absolutely-positioned 2D Remotion layers (SVG strokes, flat shapes); composite with blend modes.
|
||||
|
||||
### Surface & color treatments
|
||||
|
||||
- **Grain / texture / noise** — near-universal in 2026; add to almost everything, especially flat/gradient backgrounds. Cheap: tiling noise PNG overlay at low opacity with `mix-blend-mode: overlay`/`soft-light`. **Animated grain must move or it looks frozen** — offset `background-position` per frame, or SVG `feTurbulence` with a per-frame `seed`/`baseFrequency` jitter.
|
||||
- **Mesh gradients** — soft multi-point blends (not linear), the sophisticated 2026 background. Pre-bake a mesh PNG and slowly drift/scale it, or animate live with a fragment shader in Three.js driven by `frame`. **Always add grain on top.**
|
||||
- **Glassmorphism (evolved)** — used *selectively* (cards, lower-thirds), multi-layer depth, dynamic blur. `backdrop-filter: blur(16px) saturate(140%)`, semi-transparent `rgba` fill, 1px light top-border, soft shadow; animate blur radius + a moving specular highlight per frame; stack 2–3 panels at different parallax depths.
|
||||
- **Retro / Y2K** — chrome + iridescent mesh gradient + occasional pixelate (`image-rendering:pixelated` on a downscaled layer) + sparkle SVG bursts. Music/fashion/Gen-Z/party greetings.
|
||||
- **Anti-design / tactile brutalism** — hard `1px solid #000` borders, no radius, pure-saturated bg, system/monospace fonts, deliberate overlap. Motion is blunt — **hard cuts and `step`/`linear` snaps, not smooth springs.**
|
||||
- **Mixed-media / collage** — PNG cutouts with rough edges as layers; "paper-drop" overshoot springs on slight rotation/scale; tape/staple SVGs, paper-grain overlay, occasional 1-frame jump-cut jitter.
|
||||
- **Isometric** — `transform: rotateX(60deg) rotateZ(-45deg)` on stacked layers with `transform-style:preserve-3d`; stagger layer entrances; move elements along iso axes (matched X/Y deltas). Great for SaaS/feature explainers.
|
||||
- **Kinetic / liquid / morph** — morphing blobs (animate SVG `path d` with `flubber`; gooey via `feGaussianBlur` + `feColorMatrix`). **Liquid chrome (top-5 2026 animation trend):** Three.js `meshStandardMaterial` `metalness:1, roughness:~0.05` + HDRI `Environment` on a morphing/`MeshDistort`-style geometry — color shifts as the camera moves.
|
||||
- **AI-aesthetic vs anti-AI** — AI stills/clips as `<Img>`/`<OffthreadVideo>` backgrounds with Ken-Burns for surreal promos; lean into hand-type + grain + collage for trust/authenticity brands.
|
||||
|
||||
### Color direction 2026
|
||||
|
||||
- **Dopamine / electric accents** (electric blue, neon coral, acid yellow, vivid teal, cobalt) — as high-energy *accents* to guide the eye, never whole palettes.
|
||||
- **Tech pastels** (lavender haze, powder blue, digital pink) — calm/mature, great for SaaS/UI promos.
|
||||
- **Warm earth neutrals** (mocha/espresso/caramel/tan) — "quiet luxury," premium and human.
|
||||
- **Strategy:** bold dopamine accent + neutral/pastel base + grain on top. **Avoid all-flat saturated fields — they read as AI/template.** All color comes from `colorSchema` props so the studio can recolor; pass a user's hex through a grade so a garish value doesn't break the look.
|
||||
|
||||
---
|
||||
|
||||
## 2) Animating anything (craft + the pro workflow)
|
||||
|
||||
### The one rule everything hangs on
|
||||
A frame in Remotion is **pure**: `frame → pixels`. Motion is *evaluated* at an arbitrary frame, exactly the After Effects mental model (a keyframe graph sampled at time `t`). **Never** use `useFrame`, `Math.random()`, `Date.now()`, `setState`, or `useEffect`-driven motion — the renderer samples frames out of order and in parallel. Use `rand(seed)` from `anim.ts` for deterministic "randomness." If a value can't be derived from `frame`, it doesn't belong in the render.
|
||||
|
||||
### The 12 principles → Remotion (the four you reach for every shot in bold)
|
||||
|
||||
| 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 |
|
||||
| Straight-ahead vs pose-to-pose | `interpolate` between frames (keyed) vs per-frame formula (sim, e.g. `Confetti3D`) |
|
||||
| **Follow-through & overlapping** | Same motion, **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 a `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 + shadows (3D) |
|
||||
| Appeal | Choreography + `StudioEffects` (bloom/DOF/vignette) + good type |
|
||||
|
||||
### `spring()` vs `interpolate()`
|
||||
- **`interpolate`** — *you* author the curve. Use when a value must hit an exact mark on an exact frame (storyboard reveals, crossfades, value remaps). **Always set `extrapolateLeft/Right: "clamp"`** — forgetting this is the #1 cause of elements drifting off-screen or opacity going negative.
|
||||
- **`spring`** — *physics* authors the curve. Use for organic entrances, pops, bounces.
|
||||
- **Combine:** spring drives the *feel*, interpolate *remaps* its 0→1 output to real px/units: `const y = interpolate(spring(...), [0,1], [vmin(80), 0])`.
|
||||
|
||||
**Spring config cheat-sheet:**
|
||||
|
||||
| Feel | mass | damping | stiffness | Use for |
|
||||
|---|--:|--:|--:|---|
|
||||
| Snappy, no overshoot | 0.5 | 200 | 200 | Clean UI/logo reveals |
|
||||
| Natural pop (slight overshoot) | 0.6 | 12 | 180 | **Default** 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 |
|
||||
|
||||
Lower damping = more overshoot; higher mass = heavier/slower; higher stiffness = faster snap.
|
||||
|
||||
### Easing cheat-sheet (linear is the sound of an amateur)
|
||||
- **Entrances → ease-out** (`Easing.out(Easing.cubic)`), your default. Hero titles → `Easing.out(Easing.quint)` or `Easing.bezier(0.16,1,0.3,1)`.
|
||||
- **Exits → ease-in** (`Easing.in(Easing.cubic)`), and **always sharper than the entrance** — things leave faster than they arrive.
|
||||
- **A→B on-screen moves / camera → ease-in-out.**
|
||||
- **Snappy "ta-da" → back/overshoot** `Easing.bezier(0.34,1.56,0.64,1)`.
|
||||
- **Wind-up → anticipate** `Easing.bezier(0.36,0,0.66,-0.56)`.
|
||||
- **Linear ONLY for mechanical continuous motion** — rotation, scroll, conveyor, marquee.
|
||||
|
||||
### Timing & frame budgets (30fps default — but use `sec(s)=Math.round(s*fps)`, never hardcode 30)
|
||||
Micro pop 8–14f · standard reveal 18–28f · hero entrance 28–40f · scene transition 12–20f · hold = a comfortable read of the text. **Cut frames before you add them — amateurs over-animate.** Robotic = linear spacing; floaty/late = timing too long.
|
||||
|
||||
### The four quality multipliers (concrete patterns)
|
||||
- **Anticipation:** `interpolate(frame,[0,6,30],[0,-0.12,1])` — small negative dip before launch.
|
||||
- **Overshoot+settle:** the back bezier, or a low-damping spring. Ensure the curve *reaches and holds* the target (clamp) or it micro-drifts forever.
|
||||
- **Follow-through:** drive children from the same trigger frame, **delay each** (`frame - start - i*stagger`) with a *looser* spring so they settle after the parent. Biggest "feels professional" upgrade for grouped elements.
|
||||
- **Staggering / choreography:** default to a **cascade**; tune per aspect via a `pick(wide,square,tall)` helper (wider reads faster → tighter stagger; tall reads slower → looser). Patterns: cascade (lists), center-out (logos/hero rows), random-but-deterministic via `rand(i)` (particles), beat-synced (snap `start` to music beats). **One thing enters the eye at a time** — staging.
|
||||
|
||||
### 3D motion
|
||||
Drive every transform off `useCurrentFrame()` (deterministic under ANGLE). Rotation/orbit = `linear` (mechanical); entrances/landings = `spring` with high mass for weight. Let `StudioEffects` (bloom + DOF + vignette) do the cinematic polish in one component.
|
||||
|
||||
### The pro workflow (5 passes, IN ORDER — polishing before timing is locked wastes the most time)
|
||||
1. **Reference** — decide the feel before code; pull an AE template/Dribbble loop; pick style (`remotion-design-styles`), type (`persian-fonts`), composition (`remotion-template-composition`), per-aspect rules (`remotion-aspect-ratios`); write the beat list ("logo in → tagline → 3 features cascade → CTA → out").
|
||||
2. **Blocking** — every element on screen at 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`) and music sync (`remotion-music-picker`) to the locked frames.
|
||||
5. **Review** — scrub frame-by-frame + full speed. Nothing pops without an ease; nothing leaves slower than it arrived; the eye always knows where to look; reads in all three aspects; Persian RTL not clipped; colors from `colorSchema`; re-render twice → identical (deterministic). Then run `flatrender-template-seo`.
|
||||
|
||||
### 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` · `useFrame`/`random`/`Date.now` → `useCurrentFrame` + `rand` · pixel-hardcoded sizes → `vmin`/`unit` + branch on `isWide/isSquare/isTall` · over-animating → one idea per beat · no hold → real hold sized to reading · exit = entrance speed → exits sharper · dead holds → `sin` bob/breathe/shimmer · flat 3D lighting → `StudioLights` + floor shadows + `StudioEffects` · color hardcoded → read from props.
|
||||
|
||||
---
|
||||
|
||||
## 3) Asset pipeline (collecting + designing footage, Iran-aware)
|
||||
|
||||
### The Iron Rule
|
||||
The Iran environment punishes runtime dependencies. **All assets are vendored** — download once (over VPN if needed), commit into `services/remotion/public/`, reference with `staticFile()`. **Never** `<Video src="https://somecdn..." />` in a template — a geo-block or flaky tunnel kills the render mid-frame. Mirror npm/NuGet/Docker via Nexus; asset *binaries* are sourced manually and committed. **Track licenses at acquisition time, not later.**
|
||||
|
||||
### License taxonomy (know cold)
|
||||
- **Ship freely, no attribution:** CC0 / Public Domain, Pixabay License, Pexels License, Unsplash License. **Default target.**
|
||||
- **Allowed with a credit:** CC-BY (track per-asset; needs attribution UI/end-card).
|
||||
- **Off-limits:** CC-BY-SA (share-alike can "infect" our proprietary template), CC-BY-NC (we are a paid product = commercial), editorial/rights-managed.
|
||||
- **Paid stock (Envato/Adobe/Shutterstock):** allowed per license; keep the receipt/license PDF. Note their dashboards/checkout often geo-block Iran at the account/payment layer even with a VPN — use a foreign-established account or a partner.
|
||||
|
||||
### Sourcing (CC0 / no-attribution first)
|
||||
- **Footage:** Pexels Videos (first stop), Pixabay Video, Mixkit, Coverr (check AI badge), Free Nature Stock, Videvo (filter to CC0). Pick exact resolution (don't ship 4K into a 1080p comp), prefer **H.264 MP4** for `<OffthreadVideo>`, commit under `public/footage/`.
|
||||
- **Images:** Pexels, Pixabay, Unsplash, StockSnap, Burst. For Persian/Iran imagery search English terms ("Tehran", "Iranian food") + self-shot/local stock.
|
||||
- **Textures & overlays:** Poly Haven Textures (CC0), ambientCG (CC0); grain/light-leak/dust = CC0 video clips you screen-blend.
|
||||
- **HDRIs:** Poly Haven (1k–2k for render speed), ambientCG.
|
||||
- **3D (GLB):** Poly Haven Models (CC0, cleanest), Kenney.nl (CC0 low-poly), Khronos glTF samples, Sketchfab (**mixed — check each**, filter Downloadable + CC0/CC-BY). **Prefer GLB over glTF+separate-textures** (one file, no broken paths); Draco-compress with `gltf-pipeline -b`; keep low-poly for headless render speed.
|
||||
- **Icons (bundle via Nexus npm, never CDN):** Lucide, Tabler, Heroicons, Phosphor, Iconoir. **Illustrations (recolorable SVG):** unDraw, Open Peeps, Humaaans — ship as SVG so the studio color picker recolors them (`remotion-svg-colors`).
|
||||
|
||||
**Iran access:** generally reachable — Pixabay, Mixkit, Coverr, Poly Haven, ambientCG, npm via Nexus. VPN-recommended/intermittent — Pexels, Unsplash, Sketchfab, GitHub raw. Sanction-blocked at account/payment — Adobe Stock, Envato. **Mitigation: batch all sourcing in one "asset run" over a stable tunnel, commit binaries, render never touches the open internet again.**
|
||||
|
||||
### AI-generated assets — when it's right
|
||||
- **Use when:** the asset doesn't exist as stock (specific Persian cultural scene, branded mascot), you need consistency across a template set (reference-image controls), or it's faster than a 5-site license hunt.
|
||||
- **Don't use when:** clean CC0 stock already exists, you need photographic authenticity, or the **free-tier commercial license is unclear** (many free tiers forbid commercial use / watermark — a legal landmine for a paid product).
|
||||
- **Iran-pragmatic recommendation: locally-hosted open models** — **HunyuanVideo 1.5** (self-hosted, ~RTX 4090, no geo-block/payment/watermark, full commercial control) for video; **FLUX/SDXL** locally for image/texture/illustration. Use hosted SaaS (Runway Gen-4, Kling 3.0) only when local quality is insufficient and a VPN+foreign-account path exists. **Always record prompt + tool + plan-tier + date** in the asset's sidecar.
|
||||
|
||||
### Designing & preparing footage in Remotion
|
||||
- **Media primitives:** `<OffthreadVideo>` = **default for all video in a render** (FFmpeg frame extraction, deterministic, no seek drift). `<Video>` = preview only. `<Img>` over raw `<img>` (waits for load → no half-loaded frames). `staticFile()` for every vendored asset; never an external URL in a shipped template.
|
||||
- **Color grading:** per-layer CSS `filter` + blend modes (`contrast/saturate/brightness/hue-rotate`). Build a shared `lib/grades.ts` preset set (`warm`, `teal-orange`, `mono`, `filmic`) so all templates grade consistently and the palette can drive `hue-rotate`/`saturate`. Heavy grading → pre-grade in DaVinci Resolve (free) before committing.
|
||||
- **Masking / keying:** no native keyer — pre-key in Resolve/AE and export **alpha** (ProRes 4444 or WebM/VP9 alpha), then `<OffthreadVideo>` it. Shape/gradient masks via CSS `maskImage`/`clipPath` or SVG `<mask>`.
|
||||
- **Seamless loops:** source loop-designed clips (Coverr/Mixkit) or crossfade-to-self with overlapping `<Sequence>`s; mirror-pingpong for imperfect footage. `<OffthreadVideo loop>` once first/last frames match.
|
||||
- **Overlays (the cheap "authentic" layer):** stack grayscale-on-black/white clips — **screen** for light-leaks/bokeh/dust, **overlay/soft-light** for grain, **multiply** for vignettes/paper. Keep palette-independent.
|
||||
- **Ken-Burns:** `interpolate` scale (start ≥ 1 overscan so no edge reveals) + translate; ease with `spring`/bezier; `objectFit:cover` + center-safe framing so all three aspects crop cleanly.
|
||||
- **Performance (headless Docker):** right-sized media, H.264 + `<OffthreadVideo>`, 1k–2k HDRIs, Draco GLB; raise `concurrency` carefully and watch RAM.
|
||||
|
||||
### Library structure + attribution firewall
|
||||
Create under `services/remotion/public/` (today only `fonts/`): `footage/{nature,business,abstract}/`, `overlays/`, `images/`, `textures/`, `hdri/`, `models/`, `icons/`, `illustrations/`, plus **`assets.json` + `ASSETS.md`**. Lowercase-kebab names, no spaces.
|
||||
|
||||
**`assets.json` — one row per asset, added at download time** (`file, source, url, author, license, attribution_required, commercial_ok, acquired, notes`). Conventions: every asset gets a row (no row = "unknown license = do not ship"); `attribution_required:true` must surface its credit on a shippable surface; sidecar `.license.txt` for AI prompts / paid receipts. **CI validation script** asserts every file in the media folders has a matching row with `commercial_ok:true`, else fails the build — this is the license firewall. `ASSETS.md` is a generated readable table for humans/legal. If the repo bloats, move large media to Git LFS or MinIO (already in stack) with a `predeploy` sync into `public/` — but present at render time.
|
||||
|
||||
---
|
||||
|
||||
## 4) Masterpiece production + platform playbook
|
||||
|
||||
### The 8 production-value layers (what separates "made in a tool" from "made by a studio")
|
||||
1. **Sound design + beat-sync** — the fastest "professional" tell. Beat-sync every key reveal (map BPM, keyframe on beat boundaries — hero on a downbeat); layered SFX (whoosh on transitions, thump on hard cuts, sparkle on shine sweeps, riser into the hero, pop on icon entrances); **ducking** (music dips under VO/key sound); **silence before the hero reveal** makes the payoff hit harder.
|
||||
2. **Micro-detail** — easing never linear; overshoot & settle; staggered 2–5f entrances; secondary motion (shadow/contents react); anticipation.
|
||||
3. **Design system** — one type scale (4–5 sizes), one spacing rhythm, constrained palette (1 primary + 1–2 accents + neutrals), consistent radii/strokes/elevation; Persian-first type handled deliberately (Vazirmatn + matched Latin pairing, not one font stretched across scripts).
|
||||
4. **Depth & lighting** — layered parallax (bg/mid/fg different speeds), soft directional shadows with one consistent light direction, atmospheric depth (bg blur/desaturate, fg sharp/saturated), rim light on hero.
|
||||
5. **Color grade** — one unified grade over the whole comp (not per-element colors fighting); lifted/tinted shadows, controlled highlights, deliberate temperature; user hex still passes through the grade.
|
||||
6. **Pacing / rhythm** — vary cut length, build to a climax, match cut rhythm to music, trim ruthlessly.
|
||||
7. **A clear hero moment** — one designated peak (logo/price-drop/product/CTA) with the biggest motion, strongest hit, often silence before, most screen real-estate. Flat = nothing lands.
|
||||
8. **Finishing texture (subtle!)** — low-opacity film grain, gentle vignette, 1–2px chromatic aberration strongest at impacts, tiny continuous camera drift (frame alive, not locked), sparing light-leaks/bokeh, motion blur on fast elements (its absence is a classic amateur tell).
|
||||
|
||||
### Pre-ship polish checklist (if you can't tick it, it's not done)
|
||||
- **Motion:** no linear easing anywhere; staggered entrances; motion blur on fast elements; ≥1 overshoot-and-settle; nothing pops on/off without a transition.
|
||||
- **Audio:** BPM mapped, reveals on beat; whoosh on every scene change; accent SFX on hero; music ducks under VO, no clipping, clean end.
|
||||
- **Composition/design system:** verified in 16:9 / 1:1 / 9:16 (not a letterboxed 16:9); text in platform safe zones; consistent scale/spacing/radii/shadows; constrained palette; FA + EN both correct (RTL, font, numerals).
|
||||
- **Depth & grade:** consistent light direction; bg depth treatment; unifying grade over the comp.
|
||||
- **Pacing & hero:** one unmistakable hero; varied cuts matched to music; engaging first frame (it's the thumbnail).
|
||||
- **Finishing:** subtle grain OR vignette present; frame alive; aberration/light-leak at transitions if style allows.
|
||||
- **Technical/QA:** clean render at target res (no flicker/z-fighting/font fallback); all editable fields (text/logo/image/colors) swap without breaking layout; longest text doesn't overflow, shortest doesn't look empty; loops cleanly if meant to.
|
||||
|
||||
### Platform playbook (2026)
|
||||
All vertical = **1080×1920, 9:16**. **First frame = the hook = the cover.** High-contrast captions (white/yellow, black outline) in the lower-middle third are the cross-platform default.
|
||||
|
||||
| Platform | Length sweet spot | Hook / retention | Safe zone (1080×1920) | Template implication |
|
||||
|---|---|---|---|---|
|
||||
| **IG Reels** | 7–15s punchy; 30–90s for depth (up to 90s) | First 2–3s decide stay/swipe; cleaner/less-cluttered text than TikTok; rewards 3-sec view rate + completion | ~108 top, ~320 bottom, ~60 L, ~120 R; hook text Y≈200–600 | Cleaner kinetic type, mesh-gradient + glass lower-thirds, refined transitions |
|
||||
| **IG Story** | full-bleed | heavy UI chrome | avoid top ~250 (profile) + bottom ~250 (reply/link) | design poll/quiz/link sticker zones into layout |
|
||||
| **IG Feed post** | — | first caption line is the hook | — | Portrait **4:5 (1080×1350)** standard; 1:1 for grid consistency |
|
||||
| **TikTok** | 15–30s engagement; 11–18s virality; ≤60s educational | **3-second rule**; curiosity-gap / bold-claim hooks; word-by-word captions beat full sentences | keep right ~120, bottom ~320 clear of key content | calm neutral grain+warm-earth variant; **word-by-word captions as a first-class editable layer** |
|
||||
| **YT Shorts** | 15–35s | **no runway** — open on the most compelling moment; intro retention >70%, completion >60% (<30s) | center within middle ~1080×1350; clear bottom UI + right buttons | cinematic 3D logo reveals, graded looks |
|
||||
| **YT long-form intro** | — | cold-open hook in first 5–15s; branded sting <3s | — | state payoff first, brand second |
|
||||
| **YT end screen** | last 5–20s | — | leave clean plate (lower + right) for subscribe/next/playlist | reserve an end-card-safe zone |
|
||||
| **All three** | drifting to **60–90s** | authenticity > perfection, phone-feel > studio, natural light, cinematic grading | — | hook in first 1–2s; grain/texture everywhere; support longer durations; safe-zone all text |
|
||||
|
||||
**Cross-platform synthesis rules:** (1) design to the *tightest* safe zone (Story/TikTok), then it's safe everywhere; (2) first frame = the hook = the cover; (3) front-load the payoff, no preamble; (4) captions are a first-class editable layer (word-by-word), not an afterthought; (5) one template, **three real aspects** — re-flow, never letterbox.
|
||||
|
||||
---
|
||||
|
||||
## 5) Prioritized "level up our skills + templates" action list
|
||||
|
||||
Ordered by ROI. Each item ties to our stack and the relevant skill file.
|
||||
|
||||
**Tier 0 — foundation infra (do first; unblocks everything else)**
|
||||
1. **Establish the asset library + license firewall.** Create the `public/{footage,overlays,images,textures,hdri,models,illustrations}` tree, `assets.json` + `ASSETS.md`, and the **CI validation script** (`commercial_ok` + matching-row check). Do one batched VPN "asset run" of a CC0 starter pack (grain/light-leak/dust overlays, 3–4 mesh-gradient PNGs, a few Poly Haven HDRIs + GLBs, business/nature/abstract footage). *(Asset pipeline §3.)*
|
||||
2. **Promote shared helpers into `lib/`.** Add `pick(wide,square,tall)` onto `Layout` in `aspect.ts` (currently only `isWide/isSquare/isTall/vmin/unit`); create `lib/grades.ts` (warm/teal-orange/mono/filmic + palette-driven `hue-rotate/saturate`); confirm `rand`/`hexToRgba`/`mixHex` in `anim.ts` cover deterministic needs. *(Animation §2, Asset §3.)*
|
||||
3. **Stand up a local AI-asset box** (HunyuanVideo 1.5 + FLUX/SDXL) so bespoke Persian/branded assets don't depend on geo-blocked SaaS. *(Asset §3.)*
|
||||
|
||||
**Tier 1 — highest-ROI template work**
|
||||
4. **Build a "captions" engine as a reusable first-class layer** — word-by-word kinetic captions, high-contrast white/yellow + black outline, lower-middle-third, safe-zoned for all platforms, beat-syncable. This is currently an afterthought and is the biggest cross-platform win. *(Masterpiece §4, Animation §2.)*
|
||||
5. **Ship the "kinetic oversized type + grain" template** — every aspect, cheap (CSS), uses variable Vazirmatn weight-animation for Persian hero type. Highest ROI per the trends brief. *(Trends §1, Animation §2.)*
|
||||
6. **Codify the pre-ship polish checklist + 8 layers into a review gate** (extend `flatrender-template-seo`'s publish step, or a new lint pass) so no template ships without easing, beat-synced audio, three-aspect verification, and a hero moment. *(Masterpiece §4.)*
|
||||
7. **Sound-design pass on the existing pack** — wire `remotion-music-picker` BPM mapping + `remotion-sound-effects` placement + ducking into our current templates. Fastest "professional" upgrade to what already exists. *(Masterpiece §4.)*
|
||||
|
||||
**Tier 2 — premium differentiation**
|
||||
8. **3D logo-reveal template + a liquid-chrome variant** on `@remotion/three` + HDRI (`three-kit.tsx` `StudioEnv/Lights/Floor/Effects`), all motion off `useCurrentFrame()`. Premium tier. *(Trends §1, Animation §2.)*
|
||||
9. **Mesh-gradient + glass lower-third promo** — clean modern, IG-friendly, palette-driven, grain on top. *(Trends §1.)*
|
||||
10. **Grunge / collage / anti-AI pack** — rides the authenticity wave for youth/music; uses hand-type, paper overlays, deterministic jitter. *(Trends §1.)*
|
||||
|
||||
**Tier 3 — craft & process maturity**
|
||||
11. **Adopt the 5-pass workflow (reference → blocking → timing → polish → review) as the team norm**, and seed a reference library (AE templates / Dribbble loops) per template type. *(Animation §2.)*
|
||||
12. **Per-aspect tuning audit** of existing templates — stagger + scale via `pick`, re-flow not letterbox, confirm Persian RTL never clips. *(Animation §2, `remotion-aspect-ratios`.)*
|
||||
13. **Color system discipline** — enforce dopamine-accent-+-neutral/pastel-base + grain-overlay defaults; run user hex through the grade so no garish value breaks a look. *(Trends §1, Masterpiece §4.)*
|
||||
|
||||
---
|
||||
|
||||
## Sources
|
||||
|
||||
**Trends:** [Envato — Motion Trends 2026](https://elements.envato.com/learn/motion-design-trends) · [MonkyVision](https://monkyvision.com/blog/motion-design-trends/) · [GraphicDesignJunction](https://graphicdesignjunction.com/2026/01/video-and-motion-creative-trends-2026/) · [Krumzi](https://www.krumzi.com/blog/12-graphic-design-trends-shaping-2026-and-how-ai-is-changing-the-game) · [It's Nice That](https://www.itsnicethat.com/features/forward-thinking-graphic-trends-2026-graphic-design-120126) · [Fontfabric](https://www.fontfabric.com/blog/10-design-trends-shaping-the-visual-typographic-landscape-in-2026/) · [Kittl](https://www.kittl.com/blogs/graphic-design-trends-2026/) · [StudioMeyer](https://studiomeyer.io/en/blog/webdesign-trends-2026) · [Envato — 3D Trends](https://elements.envato.com/learn/3d-design-trends) · [Patata School](https://www.patataschool.com/blender-typography-in-motion) · [Lummi — Animation Trends](https://www.lummi.ai/blog/animation-trends-2026) · [Fireart — Tactile Brutalism](https://fireart.studio/blog/the-best-web-design-trends/) · [AND Academy — Color](https://www.andacademy.com/resources/blog/graphic-design/color-trends-for-designers/) · [Gelato — Colors](https://www.gelato.com/blog/trending-colors) · [Adobe Express — Color of Year](https://www.adobe.com/express/learn/blog/color-of-year-trends) · [ALM Corp — Short-form](https://almcorp.com/blog/short-form-video-mastery-tiktok-reels-youtube-shorts-2026/) · [ShortSync](https://www.shortsync.app/resources/short-form-video-trends-2026) · [Sprout Social](https://sproutsocial.com/insights/social-media-trends/) · [FrameFlow](https://frameflowedit.com/article/top-5-video-editing-trends-in-2026)
|
||||
|
||||
**Assets:** [Colorlib — Stock Video](https://colorlib.com/wp/best-free-stock-video-sites/) · [FreeConvert](https://www.freeconvert.com/blog/best-stock-video-sites/) · [Moonb](https://www.moonb.io/blog/best-stock-video-sites) · [awesome-cc0](https://github.com/madjin/awesome-cc0) · [Poly Haven](https://polyhaven.com/) · [Poly Haven License](https://polyhaven.com/license) · [Khronos glTF Samples](https://github.com/khronosgroup/gltf-sample-models) · [11 Free 3D Asset Sites](https://dev.to/markyu/11-free-3d-asset-sites-for-games-blender-and-webgl-ah2) · [Iran censorship (Wikipedia)](https://en.wikipedia.org/wiki/Internet_censorship_in_Iran) · [Iran tiered internet (Rest of World)](https://restofworld.org/2026/iran-blackout-tiered-internet/) · [Tech sanctions sheet](https://docs.google.com/spreadsheets/d/1b9tetXkMg4PB_XyWcsC_UWGv45MX3pZmasnDBhyQxlY/edit) · [Blocked in Iran](https://www.irun2iran.com/websites-and-social-media-blocked-in-iran/) · [AI Video 2026](https://aiunpacking.com/guides/ai-video-generation-sora-runway-kling-veo/) · [Best AI Video (PixVerse)](https://pixverse.ai/en/blog/best-ai-video-generators) · [Best AI Video (Pixflow)](https://pixflow.net/blog/best-ai-video-generator/)
|
||||
|
||||
**Masterpiece + platform:** [IG Safe Zone (Outfy)](https://www.outfy.com/blog/instagram-safe-zone/) · [Reels Safe Zones (TryMyPost)](https://www.trymypost.com/blog/instagram-reels-safe-zones-text-placement-2026) · [IG Reel Size (InVideo)](https://invideo.io/blog/instagram-reel-size-guide/) · [IG Story Dimensions (AdMake)](https://admakeai.com/blog/instagram-story-dimensions-2026) · [TikTok Length (Go-Viral)](https://www.go-viral.app/blog/tiktok-video-length/) · [TikTok 3-Second Rule (2Point)](https://www.2pointagency.com/glossary/tiktok-creative-best-practices-the-3-second-rule/) · [TikTok Hooks (Selfstorming)](https://www.selfstorming.com/guides/social-media-hooks/tiktok-video-hooks) · [TikTok Captions (Blitzcut)](https://blitzcutai.com/blog/best-caption-style-tiktok) · [Shorts Length (OpusClip)](https://www.opus.pro/blog/ideal-youtube-shorts-length-format-retention) · [Shorts Best Practices (Miraflow)](https://miraflow.ai/blog/youtube-shorts-best-practices-2026-complete-guide) · [Shorts Safe Zone (Kreatli)](https://kreatli.com/guides/youtube-shorts-safe-zone) · [Post Production (Balance Studio)](https://www.balancestudio.tv/blog/color-grading-sound-mixing-motion-graphics-what-youre-really-paying-for-in-post-production) · [Sound Design in Motion (GUVI)](https://www.guvi.in/blog/sound-design-in-motion-graphics/)
|
||||
|
||||
**Stack files referenced:** `D:\Projects\Flatrender2\services\remotion\src\lib\{anim,aspect,branding,fonts}.ts`, `three-kit.tsx`, `kit.tsx` · `src\templates.tsx` · asset root to create: `services\remotion\public\` (currently only `fonts\`) · manifest to create: `services\remotion\public\{assets.json,ASSETS.md}` · suggested: `src\lib\grades.ts`.
|
||||
@@ -0,0 +1,121 @@
|
||||
---
|
||||
name: scene-transitions
|
||||
description: How to choreograph transitions BETWEEN scenes (and shots within a scene) in FlatRender Remotion templates — cut, dissolve, wipe, clip-path mask, morph, match-cut, shape transition, camera push, zoom/whip-blur — built from primitives. Use whenever a template has more than one scene/beat, when one element must hand off to the next, or when stitching multi-scene sequences so they feel seamless instead of slideshow-y. Read before sequencing scenes.
|
||||
---
|
||||
|
||||
# Scene transitions for Remotion
|
||||
|
||||
A multi-scene template lives or dies on its *joins*. A hard slideshow of fades reads as "made in a tool"; a transition that carries motion, color, or a shape across the cut reads as "made by a studio". We have **no `@remotion/transitions` package** (not a dependency) and asset CDNs are geo-blocked — so every transition is built from primitives: `<Sequence>`, `interpolate`/`spring`, CSS `clipPath`/`maskImage`, blend modes, and (for 3D) a camera move driven by `useCurrentFrame()`. Everything is a pure function of `frame` — **never** `useFrame`, `Math.random`, `Date.now` (use `rand()` from `lib/anim.ts`).
|
||||
|
||||
## The one structural rule: overlap, don't abut
|
||||
A clean transition needs scenes to **overlap** for the transition window (12–20f). Don't place `<Sequence>`s back-to-back — give the outgoing scene a tail and the incoming a head that share the window.
|
||||
|
||||
```tsx
|
||||
import { Sequence, useVideoConfig } from "remotion";
|
||||
const { fps } = useVideoConfig();
|
||||
const sec = (s: number) => Math.round(s * fps); // never hardcode 30
|
||||
const T = sec(0.5); // transition window
|
||||
// Scene A holds frames 0..120, Scene B starts at 120-T so they cross-fade
|
||||
<Sequence from={0} durationInFrames={120}><SceneA /></Sequence>
|
||||
<Sequence from={120 - T} durationInFrames={120}><SceneB /></Sequence>
|
||||
```
|
||||
Inside each scene, derive a local progress from `useCurrentFrame()` (already 0-based inside a `Sequence`) for its *in* and *out* phases.
|
||||
|
||||
## Transition catalog — what to build, when, and how
|
||||
|
||||
| Transition | فارسی | Feel / when | Build (primitive) |
|
||||
|---|---|---|---|
|
||||
| **Cut** | برش | Hard, energetic, beat-synced, brutalist/anti-design | No overlap; `<Sequence>` ends, next begins. Snap a 1f flash or shake for punch. |
|
||||
| **Dissolve / crossfade** | محو | Calm, elegant, photo decks, luxury | Outgoing `opacity 1→0`, incoming `0→1` over the window, `clamp` both. |
|
||||
| **Wipe** | پاککن | Directional energy, news/promo | `clipPath: inset()` on the incoming layer sweeps a hard edge (see below). |
|
||||
| **Clip-path mask reveal** | ماسک | Premium reveals, shape brand moment | Animate a `circle()`/`polygon()` `clipPath` open over the new scene. |
|
||||
| **Morph** | ریختگردانی | Liquid/organic, kinetic trend | Animate SVG `path d` (`flubber`-style) or `feGaussianBlur`+`feColorMatrix` gooey merge. |
|
||||
| **Match-cut** | برش تطبیقی | Storytelling, "made by a studio" | A shape/element at the SAME position+size in both scenes; cut while it's identical. |
|
||||
| **Shape transition** | گذار شکلی | Brand mark grows into scene | A circle/blob scales up to fill frame (color = `accent`), then the new scene is revealed inside it. |
|
||||
| **Camera push / dolly** | حرکت دوربین (۳بعدی) | Cinematic, 3D logo/product | Move the R3F camera `position.z` / target between two staged setups by `frame`. |
|
||||
| **Zoom / whip blur** | زوم/تار حرکتی | Fast, hype, music, TikTok | Scale up + `filter: blur()` on out, scale-down + blur-out on in; peak blur ON the cut. |
|
||||
|
||||
### Wipe (clip-path inset)
|
||||
```tsx
|
||||
import { useCurrentFrame, interpolate, Easing } from "remotion";
|
||||
const f = useCurrentFrame(); // local frame in the incoming Sequence
|
||||
const p = interpolate(f, [0, sec(0.45)], [0, 100], {
|
||||
extrapolateLeft: "clamp", extrapolateRight: "clamp",
|
||||
easing: Easing.bezier(0.16, 1, 0.3, 1),
|
||||
});
|
||||
// RTL-aware: wipe in from the right for Persian (mirror the direction in `en`)
|
||||
<AbsoluteFill style={{ clipPath: `inset(0 0 0 ${100 - p}%)` }}><SceneB/></AbsoluteFill>
|
||||
```
|
||||
Soften the edge with a leading gradient strip (a thin `accent` bar riding `p`) for a "luminance wipe".
|
||||
|
||||
### Clip-path circle / shape reveal
|
||||
```tsx
|
||||
const r = interpolate(spring({ frame: f, fps, config: { mass: 0.6, damping: 14 } }),
|
||||
[0, 1], [0, 150]); // 150% covers corners
|
||||
<AbsoluteFill style={{ clipPath: `circle(${r}% at 50% 50%)` }}><SceneB/></AbsoluteFill>
|
||||
```
|
||||
For a brand shape transition: render a full-frame circle filled with `colorSchema.accent` scaling up over Scene A, then swap to Scene B *masked by the same circle* — the brand color carries the cut.
|
||||
|
||||
### Zoom / whip-blur
|
||||
```tsx
|
||||
// outgoing tail
|
||||
const out = interpolate(f, [0, T], [1, 1.4], { extrapolateRight: "clamp" });
|
||||
const blurOut = interpolate(f, [0, T], [0, vmin(24)], { extrapolateRight: "clamp" });
|
||||
<AbsoluteFill style={{ transform: `scale(${out})`, filter: `blur(${blurOut}px)` }}><SceneA/></AbsoluteFill>
|
||||
// incoming head (local frame): scale 1.25→1, blur 24→0 — peak blur of BOTH meets on the cut
|
||||
```
|
||||
`vmin` comes from `useLayout()` (`lib/aspect.ts`) so the blur reads the same in all three aspects.
|
||||
|
||||
### Camera push (3D, @remotion/three)
|
||||
```tsx
|
||||
// inside <ThreeCanvas> — drive the camera off frame, NOT useFrame
|
||||
const z = interpolate(spring({ frame: f, fps, config: { mass: 2.5, damping: 26 } }),
|
||||
[0, 1], [7, 3.2]); // dolly in, heavy = weight
|
||||
useThree(({ camera }) => { camera.position.z = z; camera.updateProjectionMatrix(); });
|
||||
```
|
||||
Use `StudioEnv/StudioLights/StudioFloor/StudioEffects` from `lib/three-kit.tsx`; let DOF + bloom + vignette sell the move. Camera moves use `ease-in-out`/heavy spring; never linear (linear is only for continuous orbit/rotation).
|
||||
|
||||
## Match-cut & seamless choreography (the studio-grade joins)
|
||||
The eye forgives a cut if **something continues across it**. Carry one of:
|
||||
- **Position+scale** — a circle bottom-left in Scene A is a circle bottom-left, same size, in Scene B. Cut while identical. (Classic match-cut.)
|
||||
- **Color** — Scene A ends on a full-frame `accent` wash; Scene B opens from that wash. Use `mixHex`/`hexToRgba` (`lib/anim.ts`) so it's palette-driven.
|
||||
- **Motion vector** — text exits stage-left at speed `v`; the next element enters from stage-right at the same `v`. Momentum reads as continuity.
|
||||
- **A mask** — the shape that wiped scene A out is the shape scene B wipes in with.
|
||||
|
||||
For a full template: write a **beat list first** (logo in → tagline → 3 features cascade → CTA → out), assign one transition per join, and make adjacent joins *differ* (don't dissolve every cut) but **rhyme** (reuse the brand shape/color). Vary cut length and build to the hero moment — pacing is a transition too.
|
||||
|
||||
## Timing & easing (the difference between pro and slideshow)
|
||||
- **Window:** scene transition **12–20f**; whip/cut feels best at the short end, dissolve/camera at the long end.
|
||||
- **Entrances ease-out** (`Easing.out(Easing.quint)` / `Easing.bezier(0.16,1,0.3,1)`); **exits ease-in and always SHARPER than the entrance** — scenes leave faster than they arrive.
|
||||
- **A→B on-screen / camera = ease-in-out.** **Linear ONLY** for continuous rotation/marquee.
|
||||
- Snap transition `from` frames to the **music beats** (`remotion-music-picker`) so cuts land on downbeats.
|
||||
- Per-aspect: tighten the window on `isWide` (reads faster), loosen on `isTall`. Use the proposed `pick(wide,square,tall)` helper on `Layout` when it lands; until then branch on `isWide/isSquare/isTall`.
|
||||
|
||||
## Reusable transition components
|
||||
Build these once in `lib/` and reuse across templates — each takes an `enter`/`exit` phase and a window:
|
||||
|
||||
```tsx
|
||||
// CrossFade.tsx — wrap any scene; computes its own in/out from frame + duration
|
||||
export const Dissolve: React.FC<{ children: React.ReactNode; win: number }> = ({ children, win }) => {
|
||||
const f = useCurrentFrame();
|
||||
const { durationInFrames } = useVideoConfig(); // length of THIS Sequence
|
||||
const o = Math.min(
|
||||
interpolate(f, [0, win], [0, 1], { extrapolateRight: "clamp" }),
|
||||
interpolate(f, [durationInFrames - win, durationInFrames], [1, 0], { extrapolateLeft: "clamp" }),
|
||||
);
|
||||
return <AbsoluteFill style={{ opacity: o }}>{children}</AbsoluteFill>;
|
||||
};
|
||||
```
|
||||
Make sibling wrappers `Wipe`, `CircleReveal`, `WhipZoom`, `ShapeWipe` with the same `(children, win, dir)` contract so a template can swap transitions by changing one wrapper. Keep the SFX hook in mind: a whoosh 2–3f before the cut + an impact ON it (`remotion-sound-effects`).
|
||||
|
||||
## Pre-ship transition checklist
|
||||
- [ ] No back-to-back `<Sequence>`s where a join should be smooth — scenes **overlap** the window.
|
||||
- [ ] Every join has a chosen transition with an *intent* (energy/calm/brand), not a default fade everywhere.
|
||||
- [ ] At least one join **carries** position, color, motion, or a mask across the cut (not all isolated fades).
|
||||
- [ ] All `interpolate` have `clamp` on both ends (the #1 drift bug).
|
||||
- [ ] Exits are sharper than entrances; nothing linear except continuous motion.
|
||||
- [ ] Cuts snapped to beats; whoosh-in + impact-on-cut wired.
|
||||
- [ ] Verified in 16:9 / 1:1 / 9:16 — wipe direction & blur amount read the same (`vmin`, not px); Persian RTL wipes from the right.
|
||||
- [ ] Colors via `colorSchema` (`mixHex`/`hexToRgba`), never hardcoded; deterministic (re-render twice → identical).
|
||||
|
||||
Related: `remotion-template-composition`, `remotion-aspect-ratios`, `remotion-design-styles`, `remotion-sound-effects`, `remotion-music-picker`, `remotion-character-design`, `remotion-svg-colors`, `persian-fonts`, `flatrender-template-seo`.
|
||||
@@ -0,0 +1,114 @@
|
||||
---
|
||||
name: video-hooks
|
||||
description: How to design the scroll-stopping first 1-3 seconds of a FlatRender Remotion template — hook archetypes, pattern interrupts, on-screen text hooks, curiosity gaps, and platform-specific (Instagram/TikTok/YouTube) hook norms — and bake them into the template's opening beats. Use whenever building or reviewing a template's first frames, the cover/first frame, the caption hook layer, or retention pacing of the open.
|
||||
---
|
||||
|
||||
# The hook (first 1-3 seconds — where templates are won or lost)
|
||||
|
||||
On a 9:16 feed the viewer decides **stay or swipe in 2-3 seconds** (TikTok's "3-second rule"; IG rewards 3-sec view rate). YouTube Shorts has **no runway** — open on the most compelling moment. So a FlatRender template doesn't get a polite logo intro: the **first frame is the cover/thumbnail and the hook**, and the first ~45-90 frames (@30fps) must arrest the eye. Everything here is a *pure function of `useCurrentFrame()`* — no `Math.random`/`Date.now`/`useFrame`; use `rand(seed)` from `lib/anim.ts`. Read `remotion-aspect-ratios` before positioning a single hook element.
|
||||
|
||||
## The frame budget for the open (30fps; use `sec(s)=Math.round(s*fps)`)
|
||||
| Beat | Frames | Job |
|
||||
|---|--:|---|
|
||||
| **f0 — cover** | 1 | Must already read as a finished, intriguing thumbnail. No black/empty frame 0. |
|
||||
| **Pattern interrupt** | 0-12 | One bold motion/sound jolt that breaks the scroll rhythm. |
|
||||
| **Hook text lands** | 6-30 | The promise/question/claim, big, high-contrast, lower-middle third. |
|
||||
| **Curiosity hold** | 30-75 | Pose an open loop the rest of the video closes. Don't resolve yet. |
|
||||
| **Hero handoff** | 60-90 | Flow into logo/headline (`remotion-template-composition`). |
|
||||
|
||||
Front-load the payoff — **no preamble, no slow brand sting first**. Brand comes *after* the hook earns the watch.
|
||||
|
||||
## Hook archetypes (Persian-first copy; pick ONE per template)
|
||||
| Archetype | Persian opener pattern | Best for | Motion signature |
|
||||
|---|---|---|---|
|
||||
| **Curiosity gap** | «اینو تا آخر ببین…» / «هیچکس اینو بهت نگفته» | tips, reveals, teasers | text snaps in, then a held pause (open loop) |
|
||||
| **Bold claim / contrarian** | «این روش رو فراموش کن» / «۹۰٪ اشتباه انجامش میدن» | how-to, product | hard cut + overshoot back-bezier |
|
||||
| **Question** | «دنبال … میگردی؟» | services, lead-gen | rise + tilt, then steady |
|
||||
| **Negativity / warning** | «این اشتباه رو نکن» | finance, health, safety | red accent flash + shake |
|
||||
| **Number / list** | «۳ دلیل که…» / «۵ نکته…» | listicles, carousels | counter ticks up, items pre-stack off-screen |
|
||||
| **Result-first** | show the after/price-drop/win immediately | promo, sale, before-after | hero appears f0, *then* explains |
|
||||
| **Direct address** | «تو که … هستی، اینو لازم داری» | niche/targeted | type fills 70-90% of frame |
|
||||
|
||||
Use Persian numerals (`۰-۹`) — never Latin digits — in hook copy and counters; `fa` is source of truth, `en` mirrors 1:1.
|
||||
|
||||
## Pattern interrupts (the scroll-breaking jolt in f0-12)
|
||||
The feed has a rhythm; a hook *breaks* it. Stack 1-2 of these, never all:
|
||||
- **Motion jolt** — whip-in with overshoot: `Easing.bezier(0.34,1.56,0.64,1)`, or a low-damping `spring({mass:0.6,damping:9,stiffness:200})`. Add motion blur on the fast frames (its absence is an amateur tell).
|
||||
- **Hard cut + flash** — a 1-2 frame white/accent wash: `opacity = frame < 2 ? 1 : 0` over a `hexToRgba(accentColor, …)` fill. Pair with a thump SFX (`remotion-sound-effects`).
|
||||
- **Scale punch** — start at `scale` 1.6→1.0 (clamp) so the subject "slams" toward camera.
|
||||
- **Color shock** — open on a dopamine accent (electric blue/coral/acid) on a neutral base; pull it from `accentColor` so the studio recolors it.
|
||||
- **Silence-then-hit** — a held silent f0-8, then riser+downbeat on the hook (`remotion-music-picker` BPM map). The pause *is* the interrupt.
|
||||
|
||||
```tsx
|
||||
// Pattern-interrupt whip-in for the hook line (deterministic, clamped)
|
||||
const f = useCurrentFrame();
|
||||
const { fps } = useVideoConfig();
|
||||
const intro = spring({ frame: f, fps, config: { mass: 0.6, damping: 9, stiffness: 200 } });
|
||||
const y = interpolate(intro, [0, 1], [L.vmin(60), 0]); // rises into place
|
||||
const flash = interpolate(f, [0, 2, 5], [1, 0.5, 0], { extrapolateRight: "clamp" });
|
||||
```
|
||||
|
||||
## On-screen text hooks (the highest-ROI layer)
|
||||
The hook text is a **first-class editable field**, not decoration — it is the captions/cover layer the whole brief calls the biggest cross-platform win.
|
||||
- **Placement:** lower-middle third, inside the *tightest* safe zone (Story/TikTok) so it's safe everywhere. For 1080×1920 keep hook Y ≈ `height*0.18-0.55`; clear top ~108 and bottom ~320 (UI chrome).
|
||||
- **Legibility:** high-contrast white or acid-yellow fill + **black outline** (`WebkitTextStroke` or layered `textShadow`), never thin grey on busy bg. Add a scrim if over media.
|
||||
- **Oversized & clipped:** the hook word can fill 60-90% of frame (`fitText` from `@remotion/layout-utils`); clip with `overflow:hidden`. Strongest on 9:16.
|
||||
- **Kinetic / word-by-word:** beats full sentences on TikTok. Split to spans, `delay = i*stagger`, drive each with `spring({frame: f - delay, fps})`. Stagger looser on tall, tighter on wide via `pick`.
|
||||
- **Variable weight pop:** Vazirmatn ships a variable build — animate `fontVariationSettings: "'wght' " + interpolate(f,[0,12],[300,900])` for a Persian hero hook.
|
||||
|
||||
```tsx
|
||||
// Word-by-word Persian hook, RTL, outlined, beat-staggered
|
||||
const words = hookText.split(" ");
|
||||
const stagger = L.pick(2, 3, 4); // wide reads faster → tighter
|
||||
return (
|
||||
<div style={{ direction: "rtl", fontFamily: FONT, display: "flex",
|
||||
gap: L.vmin(8), justifyContent: "center", flexWrap: "wrap",
|
||||
maxWidth: L.width * 0.86 }}>
|
||||
{words.map((w, i) => {
|
||||
const s = spring({ frame: f - i * stagger, fps, config: { damping: 12 } });
|
||||
return (
|
||||
<span key={i} style={{
|
||||
fontSize: L.pick(L.vmin(96), L.vmin(84), L.vmin(72)), fontWeight: 900,
|
||||
color: textColor, WebkitTextStroke: `${L.vmin(6)}px ${BRAND.ink}`,
|
||||
paintOrder: "stroke", transform: `translateY(${(1 - s) * L.vmin(40)}px)`,
|
||||
opacity: s,
|
||||
}}>{w}</span>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
```
|
||||
|
||||
## Curiosity & retention pacing across the open
|
||||
- **Open a loop, close it later** — the hook *promises*, the hero *pays off*. Never resolve the question in the first 2s or there's no reason to stay.
|
||||
- **One idea per beat** — staging: dim/blur everything but the hook; let it own the eye before the next element competes.
|
||||
- **Hold for the read** — a hook line needs ~0.6-0.8s minimum on screen before motion competes. Robotic = linear; floaty = held too long. Cut frames before adding.
|
||||
- **Tiny life in the hold** — a `sin(f/fps)` breathe/shimmer so the held hook isn't a frozen frame.
|
||||
- **Grain + texture** from f0 — even the cover frame should have animated grain (offset `background-position` per frame); flat-saturated = reads as AI/template.
|
||||
|
||||
## Platform hook norms → template implication
|
||||
| Platform | Hook window | Norm | Template move |
|
||||
|---|---|---|---|
|
||||
| **TikTok** | 3s | curiosity-gap / bold-claim; word-by-word captions | calm neutral grain + warm-earth variant; word-by-word hook as editable layer |
|
||||
| **IG Reels** | 2-3s | cleaner, less-cluttered than TikTok | refined kinetic type, glass lower-third, mesh-gradient bg, one clean interrupt |
|
||||
| **YT Shorts** | f0 | no runway — open on the peak | result-first / hero-at-f0; cinematic graded look |
|
||||
| **YT long-form intro** | 5-15s | cold-open hook, brand sting <3s | state payoff first, brand second |
|
||||
| **IG Story** | full-bleed | heavy UI chrome | keep hook clear of top ~250 / bottom ~250 |
|
||||
| **All three** | 1-2s | first frame = hook = cover; authenticity > gloss | hook prop in every aspect, re-flowed not letterboxed |
|
||||
|
||||
## Tie the hook into template structure
|
||||
- Make the hook copy a Zod prop (e.g. `hookText: z.string()`) + a seeded `Text` element whose `key` matches — same binding model as `remotion-template-composition`. Ship strong Persian default copy so it reads finished pre-edit.
|
||||
- Hook color = `accentColor`/`textColor` from `colorSchema`; pass user hex through a grade so a garish value doesn't break the open (`remotion-svg-colors`).
|
||||
- The hook is a `<Sequence from={0} durationInFrames={sec(2.5)}>`; the hero sequence overlaps its tail so the handoff is a flow, not a cut.
|
||||
- 3D hooks: keep the interrupt object filling the frame per aspect (tune `fov`/`position.z`), drive entrance from `useCurrentFrame()` with high `mass` for weight; let `StudioEffects` (bloom/DOF/vignette) finish it.
|
||||
|
||||
## Hook checklist (gate the open)
|
||||
- [ ] Frame 0 reads as a finished, intriguing cover — no black/empty/half-loaded frame.
|
||||
- [ ] A single clear pattern interrupt in f0-12 (motion / flash / scale / color / silence-then-hit) with SFX.
|
||||
- [ ] ONE hook archetype; Persian-first copy with Persian numerals; `en` mirror present.
|
||||
- [ ] Hook text is an editable prop, high-contrast + outlined, in the tightest safe zone, no clipping with long Persian strings.
|
||||
- [ ] An open loop is posed and NOT resolved in the first 2s; payoff lands at the hero.
|
||||
- [ ] Eased/overshoot motion (no linear), held for the read, with a tiny live shimmer; animated grain from f0.
|
||||
- [ ] Verified the open in all three aspects (`pick`-tuned), recolors cleanly, re-renders identical (deterministic).
|
||||
|
||||
Related: `remotion-template-composition`, `remotion-aspect-ratios`, `remotion-design-styles`, `remotion-sound-effects`, `remotion-music-picker`, `remotion-svg-colors`, `persian-fonts`, `remotion-template-catalog`, `flatrender-template-seo`.
|
||||
Reference in New Issue
Block a user