refactor: bundle the whole template suite under flat-artist/ + fix references

flat-artist is now the single container: all 16 template skills + the R&D
references/ moved inside flat-artist/. Cross-references updated — the orchestrator
points to bundled `<name>/SKILL.md`, sub-skills point to `../<name>/SKILL.md`,
and the R&D report path is relative. README catalog updated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Soroush Asadi
2026-06-21 19:31:53 +03:30
parent bc778952ba
commit 4ffbcac9ee
19 changed files with 73 additions and 69 deletions
+24 -21
View File
@@ -19,9 +19,12 @@ and apply the sub-skills — never ship "basic". Stack: `services/remotion/` (Re
`@remotion/three`, R3F v9, `gl="angle"`). Persian (fa) is canonical; English mirrors it 1:1.
## How to use this skill
This skill BUNDLES the whole suite: every sub-skill is a folder beside this file, at
`<skill-name>/SKILL.md` (relative to this `flat-artist/SKILL.md`). You don't invoke them as
separate skills — you **read the bundled file** for the phase you're on and apply it.
1. Read this file's pipeline + rules.
2. At each phase, **invoke the named sub-skill** with the Skill tool (or apply its guidance if already loaded). They live in the Gitea `AISkills` repo and in this project's `.claude/skills/`.
3. The deep R&D reference is `AISkills/references/design-motion-rnd.md` (trends, craft, asset pipeline, masterpiece + platform playbook) consult it for art direction and the masterpiece bar.
2. At each phase, open the bundled sub-skill at **`<skill-name>/SKILL.md`** (e.g. `motion-design-principles/SKILL.md`) and apply it.
3. The deep R&D reference is **`references/design-motion-rnd.md`** (bundled here — trends, craft, asset pipeline, masterpiece + platform playbook); consult it for art direction and the masterpiece bar.
## Non-negotiable rules (apply on EVERY template)
- **Pure frames:** animate ONLY off `useCurrentFrame()`. Never `useFrame` (R3F), `Math.random()`, `Date.now()`, `setState`/`useEffect`-driven motion. Determinism via `rand(i)` (`lib/anim.ts`).
@@ -33,27 +36,27 @@ and apply the sub-skills — never ship "basic". Stack: `services/remotion/` (Re
- **Vendored assets (Iran iron rule):** every asset committed into `services/remotion/public/` + `staticFile()`; NEVER a CDN URL at render. License firewall (CC0/commercial-ok, tracked in `assets.json`).
- **Finish it:** a masterpiece = the 8 layers below, not one big move. Render → LOOK → refine.
## The pipeline (work in order; invoke the skill named at each step)
## The pipeline (work in order; read the bundled skill named at each step)
| # | Phase | Apply skill |
|---|---|---|
| 0 | **Scope & storyboard** — pick the template TYPE/pattern; confirm a storyboard with the user before building anything character- or scene-heavy | `remotion-template-catalog` |
| 1 | **Art direction** — choose ONE coherent style + palette from current trends | `remotion-design-styles` + R&D report |
| 2 | **Hook** — design the scroll-stopping first 13s (it's the cover frame) | `video-hooks` |
| 3 | **Characters** (if any) — build/rig from SVG or 3D primitives | `remotion-character-design` |
| 0 | **Scope & storyboard** — pick the template TYPE/pattern; confirm a storyboard with the user before building anything character- or scene-heavy | `remotion-template-catalog/SKILL.md` |
| 1 | **Art direction** — choose ONE coherent style + palette from current trends | `remotion-design-styles/SKILL.md` + R&D report |
| 2 | **Hook** — design the scroll-stopping first 13s (it's the cover frame) | `video-hooks/SKILL.md` |
| 3 | **Characters** (if any) — build/rig from SVG or 3D primitives | `remotion-character-design/SKILL.md` |
| 4 | **Build the composition** — lib helpers + `three-kit` for 3D | (this skill) |
| 5 | **Motion** — easing/timing/stagger/secondary motion (the foundation) | `motion-design-principles` |
| 6 | **Kinetic type** — animate the hero/caption text (Persian word-split) | `kinetic-typography` |
| 7 | **Transitions** — scene-to-scene choreography, seamless | `scene-transitions` |
| 8 | **Effects** — grain, bokeh, light-leaks, sparkles, glow, vignette (deterministic) | `particles-and-effects` |
| 9 | **Aspect re-flow** — make it truly fit 16:9/1:1/9:16 | `remotion-aspect-ratios` |
| 10 | **Composition & elements** — hierarchy, logo/image/copy, reveal pacing | `remotion-template-composition` |
| 11 | **Color / live recolor** — wire color props + SVG color preview | `remotion-svg-colors` |
| 12 | **Fonts** — pick Persian-first type by role | `persian-fonts` |
| 13 | **Assets / footage** — source, license, prepare, composite | `asset-sourcing` |
| 14 | **Music + SFX** — beat-sync reveals, place SFX, duck | `remotion-music-picker` + `remotion-sound-effects` |
| 5 | **Motion** — easing/timing/stagger/secondary motion (the foundation) | `motion-design-principles/SKILL.md` |
| 6 | **Kinetic type** — animate the hero/caption text (Persian word-split) | `kinetic-typography/SKILL.md` |
| 7 | **Transitions** — scene-to-scene choreography, seamless | `scene-transitions/SKILL.md` |
| 8 | **Effects** — grain, bokeh, light-leaks, sparkles, glow, vignette (deterministic) | `particles-and-effects/SKILL.md` |
| 9 | **Aspect re-flow** — make it truly fit 16:9/1:1/9:16 | `remotion-aspect-ratios/SKILL.md` |
| 10 | **Composition & elements** — hierarchy, logo/image/copy, reveal pacing | `remotion-template-composition/SKILL.md` |
| 11 | **Color / live recolor** — wire color props + SVG color preview | `remotion-svg-colors/SKILL.md` |
| 12 | **Fonts** — pick Persian-first type by role | `persian-fonts/SKILL.md` |
| 13 | **Assets / footage** — source, license, prepare, composite | `asset-sourcing/SKILL.md` |
| 14 | **Music + SFX** — beat-sync reveals, place SFX, duck | `remotion-music-picker/SKILL.md` + `remotion-sound-effects/SKILL.md` |
| 15 | **QA — masterpiece gate** — the 8 layers + pre-ship checklist (below) | (this skill) |
| 16 | **SEO & taxonomy** — category, tags, keywords, slug, copy, related | `flatrender-template-seo` |
| 16 | **SEO & taxonomy** — category, tags, keywords, slug, copy, related | `flatrender-template-seo/SKILL.md` |
| 17 | **Ship** — render 3 thumbnails + preview, seed, deploy (copy assets into the running container + restart so Next re-scans `public/`) | (this skill) |
## The masterpiece bar (8 production-value layers — finish ALL)
@@ -74,13 +77,13 @@ and apply the sub-skills — never ship "basic". Stack: `services/remotion/` (Re
- [ ] Subtle grain/vignette; frame feels alive; (audio: beat-synced + ducked if present).
- [ ] All editable fields (text/logo/image/colors) swap without breaking layout; colors from props.
- [ ] Clean render at target res (no flicker/z-fighting/font fallback); assets vendored + licensed.
- [ ] Category/tags/keywords/slug + fa+en copy set (`flatrender-template-seo`).
- [ ] Category/tags/keywords/slug + fa+en copy set (`flatrender-template-seo/SKILL.md`).
## Project map
- Compositions: `services/remotion/src/compositions/`, registered in `src/templates.tsx` (`TemplateDef`, all 3 aspects).
- Helpers: `lib/anim.ts`, `lib/aspect.ts` (`useLayout`/`pick`), `lib/branding.ts` (`colorSchema`/`BRAND`), `lib/fonts.ts` (`FONT`), `lib/three-kit.tsx`.
- Seed: `scripts/seed_remotion_templates.py` (containers/projects/scenes/colors + `MEDIA` for image fields).
- Render checks: `npx remotion still src/index.ts "<Comp>-16x9|1x1|9x16" out/x.png --frame=NN`.
- Skills + R&D: Gitea `AISkills` repo (`references/design-motion-rnd.md`).
- Bundled here: each sub-skill at `<name>/SKILL.md`; the deep R&D at `references/design-motion-rnd.md`.
Related (the suite this orchestrates): `remotion-template-catalog`, `remotion-design-styles`, `video-hooks`, `remotion-character-design`, `motion-design-principles`, `kinetic-typography`, `scene-transitions`, `particles-and-effects`, `remotion-aspect-ratios`, `remotion-template-composition`, `remotion-svg-colors`, `persian-fonts`, `asset-sourcing`, `remotion-music-picker`, `remotion-sound-effects`, `flatrender-template-seo`.
Bundled sub-skills (read each at its `SKILL.md`): `remotion-template-catalog/SKILL.md`, `remotion-design-styles/SKILL.md`, `video-hooks/SKILL.md`, `remotion-character-design/SKILL.md`, `motion-design-principles/SKILL.md`, `kinetic-typography/SKILL.md`, `scene-transitions/SKILL.md`, `particles-and-effects/SKILL.md`, `remotion-aspect-ratios/SKILL.md`, `remotion-template-composition/SKILL.md`, `remotion-svg-colors/SKILL.md`, `persian-fonts/SKILL.md`, `asset-sourcing/SKILL.md`, `remotion-music-picker/SKILL.md`, `remotion-sound-effects/SKILL.md`, `flatrender-template-seo/SKILL.md`.
+95
View File
@@ -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 (1k2k 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 1k2k.
- [ ] 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/SKILL.md`, `../remotion-template-composition/SKILL.md`, `../remotion-aspect-ratios/SKILL.md`, `../remotion-character-design/SKILL.md`, `../remotion-svg-colors/SKILL.md`, `../remotion-music-picker/SKILL.md`, `../remotion-sound-effects/SKILL.md`, `../persian-fonts/SKILL.md`, `../flatrender-template-seo/SKILL.md`.
@@ -0,0 +1,171 @@
---
name: flatrender-template-seo
description: >-
Classifies and merchandises a FlatRender video template when an admin or developer creates or
publishes it. Decides the single primary category, the faceted tags, the SEO title/description/
keywords/slug, the Persian-first presentation copy, and the related-templates set — every choice
driven by the template's actual design, editable content, and usability. Use whenever creating,
editing, publishing, or seeding a template (project_container) and you must fill category_ids,
tag_ids, keywords, slug, description, news_text, or write its catalog/detail copy. Persian (fa) is
the source of truth; English (en) mirrors it 1:1.
---
# FlatRender Template SEO & Taxonomy
Decide a template's category, tags, SEO, presentation copy, and related set — grounded in what the
template *actually is*, not what you wish it ranked for. **fa is canonical; en mirrors it.**
## When to use
- An admin creates/edits a template in `TemplatesAdmin.tsx`, or you set fields on `content.project_containers`.
- You run/extend `scripts/seed_remotion_templates.py` (which currently leaves SEO + taxonomy EMPTY — see below).
- You publish (`is_published = true`) and need the template to be findable and well-presented.
Cross-reference: `../remotion-template-catalog/SKILL.md` (template TYPE → pattern), `../remotion-template-composition/SKILL.md`
(what's editable, used to write "what you can customize"), `../persian-fonts/SKILL.md` (RTL copy & Persian numerals).
## The real data model (use these exact names — do not invent fields)
Template = `content.project_containers`. Admin sets via `TemplatesAdmin.tsx``POST/PUT /v1/templates`
(`Create/UpdateContainerRequest`). Settable, SEO/taxonomy-relevant fields:
| Purpose | Admin label | form key | DTO | DB column |
|---|---|---|---|---|
| Name (→ H1) | نام | `name` | `Name` | `name` |
| Slug | اسلاگ | `slug` | `Slug` | `slug` (CITEXT UNIQUE) |
| Description / presentation copy | توضیحات | `description` | `Description` | `description` |
| Keywords (opaque free text) | کلمات کلیدی (سئو) | `keywords` | `Keywords` | `keywords` (TEXT) |
| Announcement (NOT seo) | متن خبر | `news_text` | `NewsText` | `news_text` |
| Categories (M2M) | دسته‌بندی‌ها | `category_ids[]` | `CategoryIds` | `container_categories` |
| Tags (M2M) | برچسب‌ها | `tag_ids[]` | `TagIds` | `container_tags` |
| Primary mode | حالت اصلی | `primary_mode` | `PrimaryMode` | `primary_mode` enum |
**Hard constraints from the platform (work within these):**
- **No `meta_title` / `meta_description` on a template.** The only SEO string on a container is `keywords`.
Per-page `meta_title`/`meta_description`/`meta_keywords`/`bot_follow` exist ONLY on `content.categories`
(and on `blogs`). Workaround: put the page's discoverable meta intent into `keywords`; pack the human
title/description into `name` + `description`; lift the rest of the meta from the **assigned category's**
`meta_*` fields (so choose the category deliberately — its SEO is inherited).
- **The public detail page emits title-only metadata** (`templates/[id]/page.tsx``{ title: "${name} — FlatRender" }`),
and the public mapper (`admin-api.ts` `containerToAdminProject`) DROPS `keywords`, `coverImageUrl`→OG, and
hardcodes `categoryName: undefined`. So today your `keywords`/category never reach the public `<head>`.
Still fill them: they power the backend list filters and are the fix-point when the frontend SEO gap is closed.
When you can, also fix the mapper to surface `keywords` + cover OG; otherwise flag it.
- **No related-templates table/column/endpoint exists.** "Related" is computed by re-querying
`GET /v1/templates?categoryId={guid}` or `?tagSlug={slug}` and excluding the current template.
There is no precomputed set — pick the category/tags such that this query returns good neighbors.
- **Tags are not shown anywhere in the templates UI** today, and the list page ignores the real DB
categories: the sidebar is ~12 hardcoded buckets (11 real categories + `"all"`), and because the
public mapper hardcodes `categoryName: undefined`, every template defaults to `"social"`. Tags/categories
still matter for the backend filters and future UI — set them correctly regardless.
- `demo_script_tag` exists in DB + DTO but is NOT in the admin form — you cannot set it there.
- Categories are a tree (`parent_id`); `GET /v1/categories` only eager-loads one child level. Keep the
primary set flat (812). Tags have `applies_to_mode`; categories do not.
## Step-by-step decision process
Run this for every template, in order. Each step feeds the next.
**0. Read the design + content + usability.** What does it output (format)? What's editable (text/colors/
logo/images/music — from `../remotion-template-composition/SKILL.md`)? Who hires it and when? 2D or 3D (real Remotion
build: SVG vs `@remotion/three`)? Which aspects exist (16:9 / 1:1 / 9:16 child projects)?
**1. Category — pick exactly ONE primary.** Category = the *output format*, never the occasion/industry.
Test: which single shelf would a user expect this on? If the answer is a format (logo reveal, story, slideshow)
it's the category; if it's when/why/who (Nowruz, real estate, teens) that's a TAG. Use the flat set:
اینترو و لوگو / Intro & Logo · استوری و ریلز / Story & Reels · پست شبکه اجتماعی / Social Post ·
تبلیغ و پروموشن / Promo & Ad · اسلایدشو / Slideshow · معرفی محصول / Product Showcase ·
دعوت‌نامه و مناسبت / Invitation & Event · تیتراژ و زیرنویس / Titles & Lower-thirds ·
ارائه و اینفوگرافیک / Presentation & Infographic · یوتیوب و اینترو کانال / YouTube & Channel.
No "Other"; no category with <5 templates. → set `category_ids` (the primary first; its `sort` = index 0).
**2. Tags — 612, from a controlled fa↔en vocabulary**, each TRUE of this design, across facets:
use-case · occasion · industry · style/aesthetic · aspect ratio · color/mood · 2D-3D · audience.
Empty facets are fine — don't invent tags. Aspect ratio is a TAG (one template, three outputs), never a
separate catalog entry. Tag 3D only if it really renders 3D. Reuse existing `content.tags` (match by
`slug`/`latin_name`); create a new tag only as a deliberate dictionary edit. → set `tag_ids`.
**3. SEO title / description / keywords / slug.**
- **Title** (lives in `name`): `{Template Name} | {Category} {use/occasion}`. Keep the keyword in the
first ~30 chars (Persian runs long); brand is appended by the layout, don't double it.
- **Description** (lives in `description`, also reused as presentation copy): benefit + what + how-to-edit +
soft CTA. fa 120155 / en 120158 chars. One natural keyword, no stuffing.
- **Keywords** (lives in `keywords`): how Iranians actually search — «قالب آماده», «ساخت/دانلود استوری»,
«تیزر تبلیغاتی», «اینترو لوگو» — include loanword + Persian spelling both ways (استوری/Story, اینترو/Intro).
1 primary + 23 secondaries. Note: `keywords` is an opaque free-text column — the platform does NOT
tokenize, split, index, or search on it (no comma parsing anywhere in form or backend). Commas are
purely an authoring convention for human readability; write it however reads cleanest.
- **Slug** (`slug`): the slugifier KEEPS Persian (no transliteration). Prefer a curated **latin/transliterated**
keyword slug for shareable URLs (Persian slugs become `%D9%82...` when copied). Lowercase, hyphenated, short,
keyword-bearing. Never change a published slug.
**4. Presentation copy** (write into `description`; structure it so it doubles as SEO + usability):
H1 = human Persian `name` → one-line benefit hook → "این قالب برای چیست؟" (use-case+occasion+audience) →
"چه چیزهایی قابل تغییر است؟" (bullet the REAL editable fields — don't claim editable music if there's none) →
"چطور بسازم؟" (انتخاب → ویرایش متن/رنگ → دانلود) → spec strip (aspects, duration, 2D/3D). Benefit before
feature; second person; every claim true of THIS template.
**5. Related set** (caller-computed; pick category/tags so this works). Query
`GET /v1/templates?categoryId=...` then `?tagSlug=...`; rank occasion > category+use-case > style > industry;
show 68; **exclude this template's own aspect siblings** (dedupe by template id); cap same-style clones;
label pack-mates «از همین مجموعه».
## fa / en parity & RTL rules
- Fill fa first, then en as a faithful mirror. Two self-consistent pages — never a Persian page with an
English title. Same template id; localized labels resolve per current locale.
- Persian uses Vazirmatn / RTL (see `../persian-fonts/SKILL.md`); use Persian digits in user-facing copy; don't fight the
`[dir="rtl"]` block. Tag/category *slugs* stay latin for stability; *display names* are localized.
- hreflang fa↔en + x-default is the right target even though the detail page doesn't emit it yet — flag the gap.
## Worked examples
**A. Nowruz 3D greeting (تبریک نوروز سه‌بعدی)**
- Category: `دعوت‌نامه و مناسبت / Invitation & Event`.
- Tags: occasion=نوروز/Nowruz · use-case=تبریک/greeting · style=لاکچری/luxury · 2D-3D=سه‌بعدی/3D ·
aspect=۹:۱۶,۱:۱,۱۶:۹ · color=طلایی/gold · audience=کسب‌وکار کوچک/small business.
- name (fa): «قالب تبریک نوروز سه‌بعدی | مناسبت و تبریک» · (en): "Nowruz 3D Greeting Template | Invitation & Event"
- description (fa): «کارت تبریک نوروزی سه‌بعدی و لاکچری؛ نام برند، متن تبریک و رنگ طلایی را در چند دقیقه شخصی‌سازی کن و ویدیوی آماده انتشار بساز.»
- keywords: `قالب تبریک نوروز, تبریک عید, نوروز سه بعدی, دانلود کارت تبریک, Nowruz greeting`
- slug: `nowruz-3d-greeting` · Related: other نوروز items first, then Invitation & Event + greeting.
**B. Instagram sale promo (پروموشن فروش ویژه اینستاگرام)**
- Category: `استوری و ریلز / Story & Reels` (output is a 9:16 vertical — format wins over "sale").
- Tags: use-case=فروش ویژه/flash sale · industry=فروشگاه آنلاین/e-shop · style=نئون/neon · aspect=عمودی ۹:۱۶/vertical ·
color=انرژیک/energetic · audience=فروشگاه/store · 2D.
- name (fa): «قالب استوری فروش ویژه | استوری و ریلز» · (en): "Flash-Sale Story Template | Story & Reels"
- description (fa): «قالب آماده استوری برای اعلام تخفیف و فروش ویژه؛ متن، قیمت، رنگ و لوگوی فروشگاهت را جایگزین کن و استوری حرفه‌ای ۹:۱۶ بساز.»
- keywords: `قالب استوری فروش, تخفیف اینستاگرام, ساخت استوری تبلیغاتی, دانلود قالب استوری, flash sale reels`
- slug: `instagram-flash-sale-story` · Related: same use-case (flash sale) across categories, then Story & Reels.
**C. Corporate logo reveal (لوگو موشن شرکتی)**
- Category: `اینترو و لوگو / Intro & Logo`.
- Tags: use-case=معرفی برند/brand intro · style=مینیمال/minimal · audience=شرکتی/corporate ·
aspect=۱۶:۹,۱:۱,۹:۱۶ · color=آبی/blue · 2D (or 3D only if it truly is).
- name (fa): «قالب لوگو موشن شرکتی | اینترو و لوگو» · (en): "Corporate Logo Reveal Template | Intro & Logo"
- description (fa): «اینترو حرفه‌ای برای نمایش لوگوی شرکت؛ لوگو، نام برند و رنگ سازمانی را وارد کن و یک اینترو تمیز و مینیمال بساز.»
- keywords: `لوگو موشن, اینترو لوگو, ساخت اینترو, لوگو موشن شرکتی, logo motion intro`
- slug: `corporate-logo-reveal` · Related: other Intro & Logo + brand-intro use-case.
## Where each value goes
- **Admin UI** (`TemplatesAdmin.tsx`): name→`name`, slug→`slug`, presentation copy→`description`,
SEO keywords→`keywords`, categories→`category_ids` (multi-select chips), tags→`tag_ids`, mode→`primary_mode`.
Saves via `/api/admin/resource/templates``/v1/templates`.
- **Categories/Tags** are managed on their own admin pages (`admin-resources.tsx``/v1/categories`, `/v1/tags`);
category-level `meta_title`/`meta_description`/`meta_keywords`/`bot_follow` live there — set them once per category.
- **Seeder** (`scripts/seed_remotion_templates.py`): currently SETS only `name/slug/description/image/demo*/
is_published/primary_mode/sort` and inserts ZERO `container_categories` / `container_tags` and NO `keywords`.
To seed SEO+taxonomy: add `keywords` to the `project_containers` INSERT, and add INSERTs into
`content.container_categories` (with `sort` = index) and `content.container_tags`. Otherwise each seeded
template ships with no keywords, no category, no tags — invisible to backend category/tag filters until an
admin opens it and assigns them.
## Final checklist
- [ ] Exactly ONE primary category, format-based, from the flat set → `category_ids` (primary at index 0).
- [ ] 612 tags, controlled fa↔en vocab, each true of the design, across facets; aspect is a tag → `tag_ids`.
- [ ] `name` carries the SEO title (keyword in first ~30 chars; no doubled brand).
- [ ] `description` = benefit→for-what→customize(real fields)→how-to→specs; doubles as the meta description.
- [ ] `keywords` = 1 primary + 23 secondary, loanword + Persian spelling (free text — commas are just an authoring convention, not parsed).
- [ ] `slug` = curated latin keyword slug, stable, never changed after publish.
- [ ] fa filled first, en a 1:1 mirror; Persian digits + RTL in copy.
- [ ] Related works via category/tag query; own aspect siblings excluded.
- [ ] Gaps flagged: no template `meta_*`, public mapper drops `keywords`/cover OG/category, no related table,
no hreflang on detail page — note these where relevant rather than pretending they're set.
+95
View File
@@ -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 700900) and `lineHeight: 1.41.6`. Numerals: pick Persian (۱۲۳ via `toLocaleString('fa-IR')`) or Latin and stay consistent; prices/years are usually Persian digits. See `../persian-fonts/SKILL.md`.
- 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(80110)`, body ≈ `vmin(2840)`. 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 814f · word stagger 24f · standard reveal 1828f · hero entrance 2840f · **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/SKILL.md`.
## 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/SKILL.md`, `../persian-fonts/SKILL.md`, `../remotion-aspect-ratios/SKILL.md`, `../remotion-design-styles/SKILL.md`, `../remotion-svg-colors/SKILL.md`, `../remotion-sound-effects/SKILL.md`, `../remotion-music-picker/SKILL.md`, `../flatrender-template-seo/SKILL.md`.
@@ -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) | 814 |
| Standard reveal | 1828 |
| Hero entrance | 2840 |
| Scene transition | 1220 |
| Hold | a comfortable read of the text (size to the longest Persian string) |
Symptoms: robotic = linear spacing · floaty/late = timing too long · jittery = no hold between moves.
## The 12 principles → Remotion (the four in **bold** you reach for every shot)
| Principle | Remotion expression |
|---|---|
| Squash & stretch | `scaleX`/`scaleY` inversely around an impact frame, conserve volume (`sx = 1/sy`) |
| **Anticipation** | dip the value below its start before the main move |
| Staging | stagger reveals; dim/blur everything but the hero — one idea per beat |
| Straight-ahead vs pose-to-pose | `interpolate` between keyed frames vs per-frame formula (sim, e.g. `Confetti3D`) |
| **Follow-through & overlapping** | same trigger, **delayed per child** + a *looser* spring so parts settle later |
| **Slow in & slow out** | `Easing.bezier` / `spring()` — the single biggest quality lever |
| Arcs | drive `y` with `sin`/parabola while `x` moves linearly |
| Secondary action | a small `sin` bob/shimmer alongside the primary reveal |
| Timing | frame count + spring `mass`/`damping` = weight & mood |
| **Exaggeration / overshoot** | overshoot > 1.0, then settle to 1.0 |
| Solid drawing | `StudioLights` + reflective material + floor shadows (3D) |
| Appeal | choreography + `StudioEffects` (bloom/DOF/vignette) + good type |
## The four quality multipliers (concrete, reusable)
**Anticipation** — a small negative dip before launch:
```tsx
const scale = interpolate(frame, [start, start + 6, start + 30], [0, -0.12, 1],
{ extrapolateRight: "clamp", easing: Easing.bezier(0.36, 0, 0.66, -0.56) });
```
**Overshoot + settle** — reach past, then land. Ensure the curve *holds* the target (clamp) or it micro-drifts forever:
```tsx
const pop = interpolate(frame, [start, start + 18], [0, 1],
{ extrapolateRight: "clamp", easing: Easing.bezier(0.34, 1.56, 0.64, 1) });
// or: spring with low damping (config { mass: 0.6, damping: 10, stiffness: 170 })
```
**Follow-through** — drive children from the *same* trigger, delay each, looser spring so they settle after the parent. The biggest "feels professional" upgrade for grouped elements:
```tsx
function Child({ i, start }: { i: number; start: number }) {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const p = spring({ frame: frame - start - i * 4, fps, config: { mass: 1, damping: 6, stiffness: 80 } });
return <g style={{ transform: `translateY(${interpolate(p, [0, 1], [24, 0])}px)`, opacity: p }} />;
}
```
**Secondary motion** — never let a held element go dead. Add a tiny `sin` breathe/shimmer:
```tsx
const bob = Math.sin(frame / fps * Math.PI) * L.vmin(4); // gentle float during the hold
```
## Staggering & choreography
Default to a **cascade**, and **tune the stagger per aspect** — wider frames read faster (tighter stagger), tall frames read slower (looser):
```tsx
const L = useLayout();
const stagger = L.pick(/*wide*/ 3, /*square*/ 4, /*tall*/ 5); // pick(wide, square, tall)
const start = i * stagger;
```
Patterns: **cascade** (lists/features) · **center-out** (logo/hero rows: `delay = Math.abs(i - mid) * stagger`) · **deterministic random** (particles: `rand(i)` for delay/offset) · **beat-synced** (snap `start` to music beat frames — see `../remotion-music-picker/SKILL.md`). **One thing enters the eye at a time.**
> `pick` is the standard per-aspect selector on `useLayout()`. If it isn't on `Layout` yet, add it in `aspect.ts`: `pick: <T,>(wide: T, square: T, tall: T): T => kind === "wide" ? wide : kind === "tall" ? tall : square,`
## 3D motion (`@remotion/three`)
Drive every transform off `useCurrentFrame()` (deterministic under ANGLE) — **never `useFrame`**. Rotation/orbit = `linear` (mechanical); entrances/landings = `spring` with **high mass** for weight. Keep crisp Persian text as a 2D `<AbsoluteFill>` overlay above `<ThreeCanvas>`. Let `StudioEffects` (bloom + DOF + vignette) carry the cinematic polish in one component; tune `camera.fov`/`position.z` per aspect so the subject fills the frame.
## The pro workflow — 5 passes, IN ORDER
Polishing before timing is locked wastes the most time.
1. **Reference** — decide the feel before code; pick style (`../remotion-design-styles/SKILL.md`), type (`../persian-fonts/SKILL.md`), composition (`../remotion-template-composition/SKILL.md`), per-aspect rules (`../remotion-aspect-ratios/SKILL.md`). Write the beat list ("logo in → tagline → 3 features cascade → CTA → out").
2. **Blocking** — every element at its final position with crude `interpolate` fades, no easing. Fix off-screen/cropping in all three aspects NOW.
3. **Timing** — lock frame counts, stagger, beats, holds, transitions. Watch at full speed repeatedly. Mood lives here.
4. **Polish** — swap linear for easing/springs; add anticipation + overshoot/settle, follow-through, secondary motion, arcs, squash/stretch; `StudioEffects` for 3D; wire SFX (`../remotion-sound-effects/SKILL.md`) + music sync (`../remotion-music-picker/SKILL.md`) to the locked frames.
5. **Review** — scrub frame-by-frame + full speed against the checklist below.
## Top amateur mistakes → fixes (review gate)
- Linear motion → ease/spring · no anticipation/overshoot → dip-then-launch / back bezier
- Everything on one frame → stagger · forgot `clamp` → clamp both ends
- Hardcoded 30fps → `useVideoConfig().fps` + `sec()`
- `useFrame`/`random`/`Date.now()``useCurrentFrame` + `rand`
- Pixel-hardcoded sizes → `vmin`/`unit` + `pick`/`isWide/isSquare/isTall`
- Over-animating → one idea per beat · no hold → real hold sized to reading
- Exit speed = entrance speed → exits sharper · dead holds → `sin` bob/breathe/shimmer
- Color hardcoded → read from `colorSchema` props
## Pre-ship motion checklist
- [ ] No linear easing anywhere except mechanical continuous motion (rotation/marquee).
- [ ] Entrances ease-out; exits ease-in **and sharper** than entrances.
- [ ] Every `interpolate` that could overshoot has `extrapolateLeft/Right: "clamp"`.
- [ ] At least one anticipation (dip) and one overshoot-and-settle in the piece.
- [ ] Grouped elements stagger; trailing parts follow through (looser spring).
- [ ] No dead holds — held heroes have a subtle `sin` breathe/shimmer.
- [ ] Stagger/scale tuned per aspect via `pick`; verified in 16:9 / 1:1 / 9:16.
- [ ] All timing from `sec()`/`fps`; no hardcoded 30; no `useFrame`/`random`/`Date.now`.
- [ ] One clear hero moment with the biggest motion; the eye always knows where to look.
- [ ] Re-render twice → pixel-identical (deterministic).
Related: `../remotion-design-styles/SKILL.md`, `../remotion-aspect-ratios/SKILL.md`, `../remotion-template-composition/SKILL.md`, `../remotion-character-design/SKILL.md`, `../remotion-sound-effects/SKILL.md`, `../remotion-music-picker/SKILL.md`, `../persian-fonts/SKILL.md`, `../flatrender-template-seo/SKILL.md`.
+120
View File
@@ -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 ±13px, 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.050.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/SKILL.md`).
- 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/SKILL.md`, `../remotion-character-design/SKILL.md`, `../remotion-aspect-ratios/SKILL.md`, `../remotion-template-composition/SKILL.md`, `../remotion-sound-effects/SKILL.md`, `../remotion-music-picker/SKILL.md`, `../remotion-svg-colors/SKILL.md`, `../persian-fonts/SKILL.md`, `../remotion-template-catalog/SKILL.md`, `../flatrender-template-seo/SKILL.md`.
+55
View File
@@ -0,0 +1,55 @@
---
name: persian-fonts
description: Persian (Farsi) and Latin font selection for FlatRender — which font to use where, with a Persian-first priority. Use when choosing typefaces for a template or UI, pairing display vs body fonts, handling RTL/Persian numerals, or adding a new font to the Remotion project. Persian fonts are the priority.
---
# Persian-first typography
This is an Iran-facing product: **Persian (Farsi) is the default and the priority**. Latin is secondary. Get the Persian type right first.
## What the project already uses
- **Vazirmatn** — the default Persian face everywhere. Remotion bundles `services/remotion/public/fonts/vazirmatn-{400,600,700,800,900}.woff2` and exposes `FONT` from `src/lib/fonts.ts` (use `fontFamily: FONT` + `direction: "rtl"` in every composition).
- Web app: `globals.css` has a `[dir="rtl"]` block that FORCES Vazirmatn on all elements — don't fight it with utility classes; work with it.
- Latin pairing in the web app: **Plus Jakarta Sans** + **Inter**.
## Persian font palette (pick by role)
| Font | Character | Use for |
|---|---|---|
| **Vazirmatn** | clean, neutral, many weights | DEFAULT — body, UI, most template text |
| **Estedad** | modern, geometric, friendly | headings, modern brand templates |
| **Yekan Bakh** | contemporary sans, professional | corporate/business templates |
| **Shabnam / Sahel** | soft, readable | body text, calm/elegant designs |
| **IRANSans / IRANYekan** | familiar Iranian UI standard | UI-style promos, app mockups |
| **Morabba** | bold display, rounded | big punchy headlines, posters |
| **Gandom** | strong display | impactful titles, sale/sport |
| **Lalezar** | playful, heavy, fun | kids, party, birthday, casual |
| **Shekari / Vahid** | decorative/calligraphic | festive, traditional (Nowruz, wedding, Yalda) |
| **Nastaliq (e.g. IranNastaliq)** | classical calligraphy | very formal/traditional, invitations — use sparingly, hard to read small |
Pairing rule: one display face for the headline + Vazirmatn (or Shabnam) for everything else. Don't mix two display faces.
## Match font to template mood
- Corporate / SaaS → Estedad / Yekan Bakh + Vazirmatn.
- Festive (birthday, party) → Lalezar / Morabba + Vazirmatn.
- Traditional / occasion (Nowruz, wedding, Yalda, Eid) → a decorative/Nastaliq display for the greeting + Vazirmatn for details.
- Sale / bold promo → Gandom / Morabba (heavy) + Vazirmatn.
- Minimal / elegant → Shabnam / Sahel, lighter weights.
## Persian typesetting rules
- **RTL:** always `direction: "rtl"`; align right or center. Mixed Persian+Latin/numbers needs care (bidi) — test the actual string.
- **Numerals:** decide Persian (۱۲۳) vs Latin (123) and be consistent. For Persian digits, format with `toLocaleString('fa-IR')` or use a font that supports Persian numerals. Years/prices in templates are usually Persian digits (۱۴۰۶, ۲۹۹٬۰۰۰).
- **Weights:** Persian script needs a bit more weight to feel solid — headings 700-900, body 400-600. Avoid ultra-thin for small text.
- **Line-height:** Persian needs slightly more (`lineHeight` 1.4-1.6 for body) — descenders/diacritics need room.
- **ZWNJ (نیم‌فاصله, ``):** preserve it in words like «می‌شود», «نیم‌فاصله» — don't strip it.
- **No fake bold/italic:** use real weights; Persian has no italic — don't slant it.
## Adding a new font to a Remotion template
1. Get the woff2 (license-checked — Vazirmatn/Estedad/Shabnam are SIL OFL, free for commercial; verify others). Place in `services/remotion/public/fonts/`.
2. Register it (a `@font-face` injected via `<style>` in the composition, or `@remotion/fonts` `loadFont`) and expose a `FONT_X` const next to `FONT` in `lib/fonts.ts`.
3. Use `fontFamily: FONT_X` only for that template's display text; keep body on Vazirmatn.
4. Embed the actual weights you use (don't ship 9 weights if you use 2) to keep bundles small.
## Licensing
Prefer SIL OFL / free-for-commercial Persian fonts (Vazirmatn, Estedad, Shabnam, Sahel, Samim, Gandom, Morabba — most from the `font-store`/Google Persian sets are OFL). Verify each before shipping in an exported-video product; keep a license record.
Related: `../remotion-template-composition/SKILL.md`, `../remotion-design-styles/SKILL.md`.
+244
View File
@@ -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/SKILL.md` 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 6090% 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 23 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 814f · standard reveal 1828f · hero entrance 2840f · scene transition 1220f · 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/SKILL.md`), type (`../persian-fonts/SKILL.md`), composition (`../remotion-template-composition/SKILL.md`), per-aspect rules (`../remotion-aspect-ratios/SKILL.md`); write the beat list ("logo in → tagline → 3 features cascade → CTA → out").
2. **Blocking** — every element 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/SKILL.md`) and music sync (`../remotion-music-picker/SKILL.md`) 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/SKILL.md`.
### 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 (1k2k 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/SKILL.md`).
**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>`, 1k2k 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 25f entrances; secondary motion (shadow/contents react); anticipation.
3. **Design system** — one type scale (45 sizes), one spacing rhythm, constrained palette (1 primary + 12 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, 12px 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** | 715s punchy; 3090s for depth (up to 90s) | First 23s 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≈200600 | 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** | 1530s engagement; 1118s 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** | 1535s | **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 515s; branded sting <3s | — | state payoff first, brand second |
| **YT end screen** | last 520s | — | leave clean plate (lower + right) for subscribe/next/playlist | reserve an end-card-safe zone |
| **All three** | drifting to **6090s** | authenticity > perfection, phone-feel > studio, natural light, cinematic grading | — | hook in first 12s; 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, 34 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/SKILL.md`'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/SKILL.md` BPM mapping + `../remotion-sound-effects/SKILL.md` 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/SKILL.md`.)*
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,91 @@
---
name: remotion-aspect-ratios
description: How to design ONE Remotion template that genuinely fits all three FlatRender aspects — 16:9, 1:1, 9:16 — without text cropping, off-screen elements, or a layout that is really just the 16:9 version letterboxed. Use whenever building or reviewing a template's layout. Read this BEFORE positioning any text or element.
---
# Designing for 16:9 / 1:1 / 9:16 (do this right)
Every template registers in all three aspects (`ASPECTS` in `src/lib/aspect.ts`). A common mistake (made in early FlatRender templates) is to design for 16:9 and just let the same coordinates render in 9:16 — which **crops text, pushes elements off-screen, and looks broken**. A template must be *re-laid-out* per aspect, not scaled.
## Two strategies — responsive component OR per-aspect components
There are TWO legitimate ways to support the three aspects; pick per template:
1. **One responsive component** (default) — a single composition that adapts via `useLayout()` (`isWide/isSquare/isTall`, `pick()`). Use when the design is fundamentally the same and only positions/sizes change. Less code, stays in sync.
2. **A dedicated component per aspect** — when the design must differ STRUCTURALLY (different layout, different scene, different element set), not just reposition. e.g. a cinematic wide hero vs a stacked vertical story vs a centered square badge can be genuinely different scenes.
The registry supports both. In `services/remotion/src/templates.tsx` a `TemplateDef` has `component` (shared default) plus an optional `componentsByAspect` map keyed by aspect id:
```tsx
{
id: "MyTemplate",
component: MyTemplateWide, // fallback for any aspect not overridden
componentsByAspect: {
"1x1": MyTemplateSquare, // dedicated square design
"9x16": MyTemplateTall, // dedicated vertical design
},
schema, durationSec, defaultProps, // SHARED across aspects — keep the editable
} // fields, props and duration identical
```
`Root.tsx` picks `componentsByAspect[aspectId] ?? component`. **Keep `schema`, `defaultProps`, and `durationSec` shared** so the studio shows the same editable fields and the same composition ids (`${id}-${aspect}`) regardless — only the visual layout differs. Reuse shared sub-components (background, characters, text overlay) across the per-aspect files so they don't drift.
Guideline: start with one responsive component; split into per-aspect components only when responsive branching gets gnarly or the designs truly diverge. Don't duplicate three files when `pick()` would do.
## Why the naive approach breaks
`useLayout().vmin(n)` sizes off the SHORT side (1080 in all three aspects), so a `vmin(92)` font is the same pixel size everywhere. But the WIDTH differs hugely: **1920px (16:9) vs 1080px (9:16)**. A headline that fits 1920 wide overflows/crops at 1080 wide. Likewise positioning at `width*0.34` puts an element in a totally different place relative to its own size when width changes.
## The rules
1. **Design 9:16 (tall) first.** It's the tightest. If it fits there, widening to 1:1 and 16:9 is easy. Building 16:9-first guarantees the tall version breaks.
2. **Branch layout on `L.isWide / L.isSquare / L.isTall`** — don't just scale. Things that sit side-by-side in 16:9 should STACK vertically in 9:16:
```tsx
const L = useLayout();
// hero element position differs per aspect
const heroX = L.isTall ? L.width * 0.5 : L.width * 0.34; // centered in tall, left in wide
// layout direction
flexDirection: L.isTall ? "column" : "row"
```
Add a tiny helper to `aspect.ts` and use it everywhere:
```ts
pick: <T,>(wide: T, square: T, tall: T): T =>
kind === "wide" ? wide : kind === "tall" ? tall : square,
```
Then: `fontSize: L.pick(L.vmin(92), L.vmin(84), L.vmin(72))`.
3. **Cap font size to the WIDTH, not just the short side.** Headlines must wrap, never crop. Always set `maxWidth` and let text wrap:
```tsx
maxWidth: L.width * 0.86, // safe text column
// and scale type DOWN in tall:
fontSize: L.pick(L.vmin(90), L.vmin(80), L.vmin(64)),
wordBreak: "normal", lineHeight: 1.15,
```
Test with the LONGEST realistic Persian string for that field, not the short default.
4. **Respect SAFE ZONES.** Keep all meaningful content inside the central safe area; give tall more vertical margin:
- 16:9: ~5% horizontal / 8% vertical padding.
- 9:16: ~8% horizontal, and keep the hero in the middle 60% vertically (top/bottom of phones get UI chrome).
Anchor text blocks to a zone (top third / bottom third), put the hero visual in the center.
5. **Reposition the hero per aspect.** A character/object that's at `x=34%` and text on the right in 16:9 should become hero-centered with text above/below in 1:1 and 9:16. Use `pick()` for x/y and for `justifyContent`/`alignItems`.
6. **Scale element COUNT/spread, not just size.** A row of 5 floating shapes that spans 1920 looks sparse/clipped at 1080 — reduce spread radius or count in tall (`L.pick(...)`).
7. **3D:** adjust `camera.fov` / `position.z` per aspect so the subject fills the frame (a tall frame needs the camera pulled back or a narrower fov). Keep the 2D text overlay using the same `pick()` rules.
## Mandatory verification (the step that was skipped before)
Render a still in **all three aspects** at a frame where text is visible, with a LONG test string, and LOOK at each:
```
npx remotion still src/index.ts "<Comp>-16x9" out/_a.png --frame=NN
npx remotion still src/index.ts "<Comp>-1x1" out/_b.png --frame=NN
npx remotion still src/index.ts "<Comp>-9x16" out/_c.png --frame=NN
```
Reject if: text is clipped at any edge, an element is off-frame, the hero is tiny/huge, or the tall version is obviously "the wide one squished".
## Checklist
- [ ] Designed tall-first; used `pick()`/`isTall` to branch layout (not just scale).
- [ ] Headlines wrap with `maxWidth`; tested with long Persian text — no cropping.
- [ ] Hero repositioned/centered per aspect; content in safe zones.
- [ ] Spread/count adjusted for narrow frames; 3D fov/camera tuned per aspect.
- [ ] Eyeballed stills in ALL THREE aspects.
Related: `../remotion-template-composition/SKILL.md`, `../remotion-design-styles/SKILL.md`.
@@ -0,0 +1,51 @@
---
name: remotion-character-design
description: How to build and animate 2D (SVG) and 3D (@remotion/three) characters and mascots for FlatRender Remotion templates. Use when a template needs a character — a mascot, Haji Firuz, animals (goldfish, butterflies), people, or any articulated figure. Covers construction from primitives, rigging via grouped transforms, animation cycles (walk/dance/idle), facial expression, and the 2D-vs-3D / GLTF trade-offs.
---
# Character design for Remotion
We have NO rigged 3D model assets and the asset CDNs are geo-blocked, so characters are built from **primitives** — SVG shapes (2D) or Three.js geometries (3D) — and animated off `useCurrentFrame()`. Reference implementations: `NowruzGreeting.tsx` (2D Haji Firuz, goldfish, butterflies) and `Nowruz3D.tsx` (3D scene).
## Core principle: build in parts, rig with groups
A character = a tree of grouped parts, each animated by transforming its group around a PIVOT.
- 2D: nested `<g transform="translate/rotate/scale">`. Put the pivot at the joint (e.g. shoulder) by translating the group there, drawing the limb from origin, and rotating the group.
- 3D: nested `<group position rotation>`. Same idea — position the group at the joint, model the limb from there, rotate the group.
## 2D characters (SVG)
Construction kit:
- Head = `<circle>`; body = `<path>` trapezoid (`M.. Q..`); limbs = `<rect rx>` (rounded) or `<path>`; hands/feet = small ellipses; hat = `<path>` triangle/cone.
- Face (friendly/stylized): two dot eyes, a `Q` curve smile, rosy `circle` cheeks at low opacity. Keep it simple — over-detailing reads worse, not better.
- Skin/clothing: flat fills + one darker shade for a soft AO at edges (`fillOpacity` overlay).
Animating cycles (all from `frame`):
- **Idle/breathe:** body `scale.y = 1 + 0.02*sin(frame/30)`.
- **Dance/bounce:** whole body `translateY = -abs(sin(frame/7))*H`, plus a small `rotate(sin(frame/7)*4)` sway; legs counter-rotate.
- **Limb swing:** `rotate(amp*sin(frame/period))` around the joint pivot.
- **Hop-in entrance:** `spring()` from off-frame X to the target, then switch to the idle/dance loop.
- **Prop shake (tambourine, flag):** `rotate(sin(frame/3.2)*18)` + sparkle accents on peaks.
## 3D characters (primitives)
Build the figure from: `sphereGeometry` (head/joints), `cylinderGeometry`/`capsule` (limbs/torso), `coneGeometry` (hat/skirt), `RoundedBox` (blocky bodies), `torusGeometry` (rings/mouth). Group per limb for articulation. Material: `meshStandardMaterial` (roughness ~0.5 for cloth, lower for shiny). Light with `three-kit` StudioLights; add a soft contact shadow (a dark blurred plane or `shadows` + a floor).
- 3D animation is the SAME math (rotate/translate groups by `frame`), just in 3D space and you can also move the camera/scene for parallax.
- Faces in 3D are hard — keep them simple (sphere eyes, a small torus/curve mouth) or face the character slightly away.
## Animation principles (what makes it not look stiff)
- **Anticipation:** dip before a jump, wind-up before a throw.
- **Squash & stretch:** scale on impacts/landings (subtle: ±8%).
- **Overlap / secondary motion:** hat ball, scarf, string, ears, tambourine lag behind the body — offset their phase.
- **Easing:** `Easing.out(Easing.cubic)` for arrivals, `spring()` for bouncy pops, never linear for organic motion.
- **Hold:** let a pose read for a beat before the next move.
## Cultural / brand-safety notes
- Default to a **modern stylized** look (friendly, non-realistic) unless the user asks otherwise. Specifically for Haji Firuz: red costume + conical hat + tambourine, but a friendly non-blackface face (avoids the blackface connotation).
- Confirm a storyboard with the user BEFORE building complex characters (the project convention) — list the cast, the beats, and the look.
## When NOT to hand-build
- Photoreal humans or complex rigged motion → out of scope without GLTF assets; propose a stylized take or a non-character design instead.
- If a GLTF model IS provided, load with drei `useGLTF` and animate transforms by `frame` (no `useFrame`).
## Workflow
Storyboard → build parts → rig groups → animate cycles → **render stills at 3-4 beats and LOOK** → refine → render all 3 aspects (see `../remotion-aspect-ratios/SKILL.md` — characters must sit in the safe zone in 9:16 too).
Related: `../remotion-design-styles/SKILL.md`, `../remotion-aspect-ratios/SKILL.md`, `../remotion-template-catalog/SKILL.md`.
@@ -0,0 +1,49 @@
---
name: remotion-design-styles
description: Visual art-direction reference for building FlatRender video templates with Remotion (2D) and Three.js/@remotion/three (3D). Use when starting a new template, picking an art style, designing color palettes, or designing objects/scenes. Covers flat, gradient/mesh, glassmorphism, neon, 3D/cinematic, paper-cut, isometric, luxury looks plus color theory, lighting, and material design.
---
# Remotion design styles (2D + 3D)
Project location: `services/remotion/`. Shared helpers: `src/lib/anim.ts` (hexToRgba, mixHex, rand), `src/lib/branding.ts` (colorSchema + BRAND), `src/lib/aspect.ts` (useLayout), `src/lib/three-kit.tsx` (3D studio kit). Animate everything off `useCurrentFrame()` — never `Math.random()`/`Date.now()` (breaks determinism; use `rand(i)`), and in 3D never use R3F's `useFrame` (use `useCurrentFrame`).
## Pick a style first, then build
Each template should commit to ONE art style — mixing reads as "basic". Catalog:
| Style | Look | How (Remotion) | Best for |
|---|---|---|---|
| Flat / minimal | solid fills, generous whitespace, 1-2 accents | SVG shapes, simple springs | corporate, clean promos |
| Gradient / mesh | soft drifting color blobs | blurred radial-gradient divs animated by frame (see GradientPromo) | modern SaaS, backgrounds |
| Glassmorphism | frosted translucent cards | `backdrop-filter: blur`, rgba fills, thin borders | UI/app promos |
| Neon / glow | dark bg + luminous strokes | `drop-shadow`/`textShadow` glows, emissive in 3D | gaming, nightlife, tech |
| 3D / cinematic | real depth, reflections, bokeh | @remotion/three + three-kit (lighting, MeshReflectorMaterial, bloom/DOF) | premium logo/product reveals |
| Paper-cut / layered | stacked shapes with soft shadows | layered SVG + offset box-shadows | storytelling, kids, greetings |
| Isometric | 2.5D objects on a grid | SVG with skew/`rotateX` CSS, or true 3D ortho camera | explainer, product, city scenes |
| Luxury / gold | dark + metallic gold + serif | gold gradients, shine sweeps, slow easing | weddings, premium brands |
## 2D vs 3D — choose deliberately
- **2D (SVG/CSS):** fast to render, crisp text, full control, no WebGL. Use for flat/gradient/glass/neon/character scenes, anything text-heavy.
- **3D (@remotion/three):** depth, real lighting/reflections, bokeh, camera moves. Use for premium logo/product/abstract reveals. Costs render time. Setup is already done: R3F v9 + `Config.setChromiumOpenGlRenderer("angle")`. Reuse `three-kit.tsx` (StudioEnv, StudioLights, StudioFloor, StudioEffects, Confetti3D). Keep crisp Persian text as a 2D `<AbsoluteFill>` overlay ON TOP of `<ThreeCanvas>` — don't render Persian text in 3D.
## Color design
- Drive every colorable element from the `colorSchema` props (accent / secondary / background / text) so the studio can recolor it — see `../remotion-svg-colors/SKILL.md`.
- Build depth with VALUE, not just hue: dark bg → mid elements → bright accents/highlights. Add glow (`hexToRgba(accent, .6)` shadows) and a vignette (`inset 0 0 600px rgba(0,0,0,.6)`).
- Gradients: 2-3 stops max; blend related hues (`mixHex(a,b,.5)`). Mesh look = several large blurred radial-gradient circles drifting on `sin(frame/…)`.
- Contrast: text needs ≥ 4.5:1 over its backdrop — add a scrim/shadow when over busy/3D scenes.
- Default palettes per mood: tech = blue→violet on near-black; festive = warm gold/red/green on cream or turquoise; luxury = gold on charcoal; fresh = teal/green on light.
## Object design
- **2D:** compose from primitives — `<circle>`, `<rect rx>`, `<path>` (quadratic `Q` for organic curves). Reuse `rand(i)` for deterministic scatter (particles, confetti, petals). Add glow via SVG `filter: drop-shadow`.
- **3D:** primitives + good material = premium. `meshStandardMaterial` with `metalness` 0.3-0.7, `roughness` 0.15-0.35, `flatShading` for faceted gems, `emissive`+`emissiveIntensity` for glow that bloom picks up (set `toneMapped={false}` on flames/glows). Light with 3-point + colored rims (StudioLights). Faceted icosahedron/octahedron/dodecahedron read as "gems"; RoundedBox for gifts/cards.
## Motion = polish
Stagger entrances (don't reveal everything at once), use `spring()` for pops and `interpolate(..., Easing.out(Easing.cubic))` for slides, add secondary motion (breathe, drift, twinkle), and a subtle continuous camera/scene sway in 3D. Hold the final frame ~1s for readability.
## Quality checklist
- One coherent style; depth via value + glow + vignette.
- All colors come from props (recolorable).
- Staggered, eased motion with secondary detail.
- Renders deterministically (no random/date).
- Verify visually: render stills at 3-4 key frames and LOOK before shipping.
Related: `../remotion-character-design/SKILL.md`, `../remotion-aspect-ratios/SKILL.md`, `../remotion-template-composition/SKILL.md`, `../remotion-svg-colors/SKILL.md`.
@@ -0,0 +1,59 @@
---
name: remotion-music-picker
description: How to choose royalty-free background music for a FlatRender template and sync the animation to its beat/vibe. Use when picking a music bed for a template, matching mood and BPM to the visuals, syncing reveals to the beat, or sourcing free/royalty-free tracks.
---
# Music picker for templates
> Status: templates currently ship without a music bed. This is the playbook for adding one. The right track + beat-synced motion is what makes a template feel "produced".
## Match music to the template's job
| Template vibe | Genre / mood | Typical BPM |
|---|---|---|
| Corporate / SaaS logo | clean inspirational, soft piano + synth pluck | 90-110 |
| Energetic promo / sale | upbeat pop, four-on-the-floor, claps | 120-130 |
| Social / Insta / trendy | lo-fi or modern pop, punchy | 100-120 |
| Epic / product reveal | cinematic build, big drum, riser | 70-90 build → hit |
| Festive (birthday, Nowruz, party) | happy ukulele/marimba, bells | 110-128 |
| Emotional (wedding, tribute) | warm piano/strings | 60-80 |
| Tech / gaming | electronic, arpeggios, bass | 120-140 |
| Luxury | downtempo, jazzy, smooth | 80-100 |
| Minimal / explainer | light marimba/plucks, unobtrusive | 95-115 |
## Sync the animation to the beat (this is the magic)
1. Know the track's BPM → frames per beat = `fps * 60 / bpm` (e.g. 30fps, 120bpm → 15 frames/beat).
2. Land your hero reveals, pops, and cuts ON beats (multiples of frames-per-beat). Stagger small element pops on 1/2 or 1/4 beats.
3. Put the BIG reveal on a musical downbeat or right after a riser/drop.
4. For a known track, hardcode beat frames; for generic use, expose `bpm` as a prop and compute beat frames so motion stays in sync if the track changes.
```tsx
const FPS = 30, BPM = 120;
const beat = (FPS * 60) / BPM; // frames per beat
const onBeat = (n: number) => Math.round(n * beat);
// reveal hero on beat 4, CTA on beat 8
```
## Remotion wiring
```tsx
import { Audio, staticFile, interpolate, useCurrentFrame, useVideoConfig } from "remotion";
const { durationInFrames } = useVideoConfig();
const f = useCurrentFrame();
<Audio src={staticFile("music/upbeat-120.mp3")}
volume={(ff)=>interpolate(ff,[0,15,durationInFrames-20,durationInFrames],[0,0.7,0.7,0])} />
```
- Fade IN over ~0.5s and OUT over the last ~0.7s — never start/end abruptly.
- Trim/loop the track to the template length; cut on a bar boundary so the end feels intentional.
- If there's a voiceover, DUCK the music (~-12 dB) under it.
- Store beds in `services/remotion/public/music/`, named with BPM (`upbeat-120.mp3`).
## Free / royalty-free sources (verify license per track)
- **Uppbeat** (free tier, gives a clearance ID), **Pixabay Music** (CC0-ish), **Mixkit** (free for commercial), **Bensound** (free w/ attribution or licensed), **Free Music Archive** (per-track CC), **YouTube Audio Library** (downloadable, check terms), **Incompetech / Kevin MacLeod** (CC-BY).
- ALWAYS check: commercial use allowed? attribution required? Keep a per-file license/attribution record. Prefer CC0 / royalty-free-with-commercial. Avoid anything Content-ID-flagged for a product that exports user videos.
- For an Iran-facing product, also consider local royalty-free Persian/instrumental sources where licensing is clearer.
## Workflow
1. Pick vibe + BPM from the table for the template's purpose.
2. Source 2-3 candidate tracks (license-checked), audition against the animation.
3. Re-time the key reveals to the chosen BPM's beats.
4. Add `<Audio>` with fades; render and LISTEN; nudge beats until reveals hit on the downbeat.
Related: `../remotion-sound-effects/SKILL.md`, `../remotion-template-catalog/SKILL.md`.
@@ -0,0 +1,56 @@
---
name: remotion-sound-effects
description: Which sound effects (SFX) to use for FlatRender video templates and exactly where to place them in the timeline. Use when adding audio punch to a template — whooshes, impacts, sparkles, pops, risers, confetti — and syncing them to keyframes with Remotion's Audio component.
---
# Sound-effect design for templates
> Status: current FlatRender templates have NO audio yet. This skill is the playbook for adding it. SFX dramatically raise perceived quality — a logo "thud" + sparkle makes a reveal feel pro.
## How audio works in Remotion
Use `<Audio>` from `remotion`, placed in the composition tree, timed to frames:
```tsx
import { Audio, useVideoConfig } from "remotion";
// play a one-shot SFX starting at frame 55 (the logo-land beat)
<Audio src={staticFile("sfx/impact.mp3")} startFrom={0} volume={0.8}
// mount it only around its moment using a <Sequence from={55}> wrapper
/>
```
Patterns:
- Wrap one-shots in `<Sequence from={FRAME}>` so they trigger at the right beat.
- `volume` can be a function of frame for fades: `volume={(f)=>interpolate(f,[0,10],[0,1])}`.
- Layer SFX over the music bed (see `../remotion-music-picker/SKILL.md`); keep SFX ~ -6 dB under dialogue, on top of music.
- Put shared SFX in `services/remotion/public/sfx/` and load with `staticFile()`.
## SFX → moment mapping (sync to the KEYFRAME, not vaguely)
| Moment in the animation | SFX | Place at |
|---|---|---|
| Element/text flies in | **whoosh** (short, directional) | 2-3 frames BEFORE it lands |
| Logo / hero lands | **impact / thud / boom** | the exact land frame (spring settle) |
| Glitter / magic reveal | **sparkle / shimmer / chime** | over the particle gather (0.3-0.5s) |
| Small element appears | **pop / tick / blip** | each appearance (stagger to match) |
| Countdown ticking | **clock tick** per number, **ding/airhorn** on GO | each number frame |
| Birthday / party | **party horn + confetti rustle**, soft **chime** | greeting reveal + confetti burst |
| Sale / promo | **cash register "cha-ching" / coin**, **stamp** on the badge | badge pop |
| Shine sweep across logo | **soft shimmer swell** | sweep start→end |
| Transition between scenes | **whoosh + light riser** | on the cut |
| Build-up before reveal | **riser / uplifter** (0.5-1.5s) | leading INTO the hero moment |
## Placement principles
- **Anticipation:** risers and whooshes START before the visual peak and resolve ON it. A whoosh that lands with the logo sells the motion.
- **One hero hit:** the reveal gets ONE big impact — don't stack 3 booms; it muddies.
- **Match the motion curve:** fast spring = sharp transient; slow ease = soft swell.
- **Stagger to the visuals:** if 5 elements pop on different frames, 5 pops on those frames (vary pitch slightly so it's not robotic).
- **Less is more:** 3-6 well-placed SFX per template beats a wall of sound. Leave silence for contrast.
- **Loudness:** normalize SFX, peak ~ -3 dB, sit them under the music bed; the final mix shouldn't clip.
## Free / royalty-aware SFX sources
Mixkit, Pixabay (sound), Freesound (check each license — CC0 vs attribution), Uppbeat (free tier), and Remotion-safe CC0 packs. ALWAYS record the license per file; prefer CC0/royalty-free with commercial use. Keep an attributions file if any source requires it.
## Workflow
1. Identify the 3-6 key beats (reveal, pops, transitions, CTA).
2. Pick one SFX per beat from the table.
3. Place via `<Sequence from={frame}>` + `<Audio>`; tune volume + a short fade.
4. Render with audio and LISTEN — adjust timing by a few frames so hits land exactly on the visual.
Related: `../remotion-music-picker/SKILL.md`, `../remotion-template-composition/SKILL.md`.
+45
View File
@@ -0,0 +1,45 @@
---
name: remotion-svg-colors
description: How FlatRender makes template colors user-editable and generates per-scene SVG so the studio can recolor a template and preview it in real time. Use when wiring a template's colors to the studio color picker, choosing which elements are recolorable, or generating an SVG color-preview for a scene.
---
# SVG + color system (real-time recolor)
Goal: a user opens a template, changes its colors, and sees the result update live. This works because every colorable element reads from a NAMED color, those names are stored as editable color elements in the DB, and a lightweight SVG representation lets the studio recolor without a full re-render.
## The color data model
- **`content.shared_colors`** — project-wide colors (key = `element_key`, e.g. `accentColor`). Used by every scene.
- **`content.scene_color_elements`** — per-scene colors (key = `element_key`, e.g. `frl_c1t1` for AE, or a Remotion prop name).
- Studio copies these into `studio.saved_shared_colors` / `saved_scene_colors`; the render binder (`GetRenderBindings` in render-svc) returns them as `{element_key: hex}`.
- For **Remotion**, those keys must equal the composition's `colorSchema` props: `accentColor`, `secondaryColor`, `backgroundColor`, `textColor` (from `src/lib/branding.ts`). The node-agent passes them as `--props`.
- For **AE**, colours bind into the `frshare` comp's text layers (`bind.jsx`).
**Rule:** design every template so EVERY colorable element's color comes from a named prop — never a hardcoded hex for anything the user should be able to change. Seed a `shared_colors` row per color prop (the seed script already does accent/secondary/background/text).
## The SVG color-preview (live recolor without re-rendering)
A full Remotion/AE re-render is too slow for a color picker. So a scene is also represented as an **SVG** whose shapes' `fill`/`stroke` reference the SAME color keys. The studio swaps the SVG's colors instantly as the user drags the picker.
- AE pipeline: `content.projects.shared_colors_svg` + the `template_svg_previews` table + per-scene snapshots.
- For a Remotion template, generate an SVG snapshot of a representative frame where colorable regions are tagged with their key, e.g.:
```svg
<rect ... fill="var(--accentColor)" data-color-key="accentColor"/>
<text ... fill="var(--textColor)" data-color-key="textColor">...</text>
```
The studio sets CSS variables (`--accentColor: #...`) or rewrites `fill` by `data-color-key` to recolor live; on export the real props go to the renderer.
## How to author a recolorable template
1. Use the 4 `colorSchema` props for all themeable color (add more named props only if a template genuinely needs them — and seed a matching `shared_colors` row for each).
2. Keep colorable regions as FLAT fills/strokes that map cleanly to one key (gradients = blend of two named props via `mixHex`, still derived from props).
3. Produce an SVG preview of the key frame with each region tagged `data-color-key` = the prop name, so the studio can map picker → region.
4. Verify: changing a prop changes exactly the intended regions and nothing hardcoded stays the wrong color.
## Generating the SVG
- Simple/flat scenes: hand-author or script an SVG mirroring the composition's shapes, tagging fills with the color keys.
- 3D / complex scenes: an SVG can't represent them faithfully — fall back to a rendered key-frame thumbnail per color theme, or a simplified 2D SVG stand-in for the picker (note this limitation to the user).
- Store alongside the template (the AE path uses `shared_colors_svg` / `template_svg_previews`; mirror that for Remotion).
## Pitfalls
- A hardcoded hex on a "should-be-editable" element = the picker silently does nothing there. Audit for stray hexes.
- Key mismatch (SVG `data-color-key` ≠ schema prop ≠ seeded `element_key`) breaks the binding — keep ONE naming source of truth.
- Contrast: when users can recolor text + background, enforce/encourage a min contrast or the text can vanish.
Related: `../remotion-template-composition/SKILL.md`, `../remotion-design-styles/SKILL.md`.
@@ -0,0 +1,54 @@
---
name: remotion-template-catalog
description: A taxonomy of video template TYPES to build for FlatRender, with the purpose, key editable elements, suggested style/aspect, and 2D-vs-3D recommendation for each. Use when deciding what template to create next, planning a content batch, or scoping a requested template into a known pattern.
---
# Template catalog — what to build
FlatRender already has these (services/remotion `TEMPLATES`): IlluminatedCircles, KineticQuote, GradientPromo, VerticalStory, LogoMotion, Opener, InstaPromo, YouTubeIntro, Slideshow, HappyBirthday, SalePromo, QuoteCard, EventInvite, Countdown, GlitterReveal (editable logo), NowruzGreeting (2D characters), + 3D: Hero3D, Nowruz3D, Birthday3D, Promo3D.
Use this map to pick the NEXT one and to scope a request into a pattern. Each row: key editable elements · suggested style · best aspect(s) · 2D/3D.
## Brand / logo
- **Logo reveal** — logo + tagline · any style · all aspects · 2D or 3D. (have: IlluminatedCircles, LogoMotion, GlitterReveal, Hero3D)
- **Opener / intro sting** — title + subtitle · cinematic/kinetic · 16:9, 9:16 · 2D/3D. (have: Opener)
- **Outro / subscribe / end-card** — CTA + socials + logo · flat/neon · 16:9, 9:16 · 2D. (gap)
- **Lower-third / name tag** — name + role · clean/glass · 16:9 · 2D. (gap)
## Social / marketing
- **Instagram post/story promo** — headline + image + CTA · gradient/bold · 1:1, 9:16. (have: InstaPromo, VerticalStory)
- **YouTube intro/outro** — channel + subscribe · energetic · 16:9. (have: YouTubeIntro)
- **TikTok/Reels hook** — big kinetic text · trendy · 9:16 · 2D. (gap)
- **Sale / discount** — badge + headline + CTA · bold/3D gifts · all. (have: SalePromo, Promo3D)
- **Product showcase / turntable** — product image/3D + specs · 3D cinematic · 16:9, 1:1. (gap — high value)
- **Testimonial / review** — quote + stars + name/photo · clean · 1:1, 9:16. (gap)
- **Explainer / feature list** — icon + text steps · isometric/flat · 16:9. (gap)
- **Real-estate / listing** — photos + price + details · elegant · 16:9, 1:1. (gap)
- **Restaurant / menu / food** — dish image + price · warm/appetizing · 1:1, 9:16. (gap)
## Greetings / occasions (great for characters + 3D)
- **Birthday** — name + message · party/3D cake · all. (have: HappyBirthday, Birthday3D)
- **Nowruz (نوروز)** — greeting + year · spring characters/3D Haft-Sin · all. (have: NowruzGreeting, Nowruz3D)
- **Yalda (یلدا)** — pomegranate/watermelon, candles, warm night · 2D/3D · all. (gap — high value, Persian)
- **Wedding / engagement** — names + date · luxury gold/floral · all. (gap)
- **Eid / Ramadan / Mehregan** — lantern/crescent/autumn motifs · ornate · all. (gap, Persian/region)
- **New Year / holidays** — countdown + fireworks · festive 3D · 16:9, 9:16. (gap)
- **Condolence / tribute** — respectful, minimal, slow · muted palette · all. (gap)
## Content / text
- **Quote card** — quote + author · kinetic/typographic · 1:1, 9:16. (have: QuoteCard, KineticQuote)
- **Countdown** — target + number · energetic riser · all. (have: Countdown)
- **Event invite** — title + date/place + RSVP · elegant · 1:1, 9:16. (have: EventInvite)
- **Slideshow / photo gallery** — N images + captions · clean transitions · all. (have: Slideshow)
- **Music visualizer** — audio-reactive bars + cover · neon/3D · 1:1, 9:16. (gap)
## How to choose next
1. Prefer **gaps** with high value for an Iran-facing product: Yalda, wedding, product showcase, testimonial, lower-thirds, outro/subscribe, music visualizer.
2. Pick a STYLE that differs from neighbors (don't ship five dark-particle reveals) — see `../remotion-design-styles/SKILL.md`.
3. Decide 2D vs 3D by subject (logos/products/abstract → 3D shines; text/character/illustrative → 2D or hybrid).
4. Confirm a storyboard with the user for anything character- or scene-heavy.
## Per-template build steps
Storyboard (confirm) → build composition (lib helpers, design-styles, character-design) → make it fit all aspects (`../remotion-aspect-ratios/SKILL.md`) → wire editable text/logo/colors (`../remotion-template-composition/SKILL.md`, `../remotion-svg-colors/SKILL.md`) → pick fonts (`../persian-fonts/SKILL.md`) → optional music/SFX → render thumbnails + preview → seed (`scripts/seed_remotion_templates.py`) → deploy.
Related: every other remotion-* skill.
@@ -0,0 +1,60 @@
---
name: remotion-template-composition
description: How to compose the editable elements of a FlatRender template — text, logo, image/media, and supporting copy — into a clear, well-paced presentation. Use when laying out what goes where, deciding the visual hierarchy, wiring editable fields, or timing the reveal sequence of a template.
---
# Composing a template (text / logo / image / copy)
A template is not just a nice animation — it's a *fill-in-the-blanks* product. Users edit a few fields and it must look great with THEIR text/logo. Design for editability + clarity.
## The binding model (how editable fields work)
Editable elements live in the DB and bind to Remotion props by KEY (see `../remotion-svg-colors/SKILL.md` for the full pipeline):
- **Text** → `content.scene_content_elements` of type `Text`; the element `key` MUST equal the composition's Zod schema field (e.g. `headline`, `tagline`). Studio shows a text input.
- **Logo / image / media** → a `scene_content_elements` of type `Media`; key = a `z.string()` prop (e.g. `logoUrl`). Studio shows upload/replace. In the composition: `logoUrl ? <Img src={logoUrl}> : <DefaultMark/>`. See `GlitterReveal.tsx`.
- **Colors** → `shared_colors` / `scene_color_elements`, key = a `colorSchema` prop.
- Seed all of these via `scripts/seed_remotion_templates.py` (it has a `MEDIA` dict for image fields).
**Rule:** every visible piece of copy or media a user would want to change MUST be a prop + a seeded element. Don't hardcode the brand name, date, price, etc.
## Visual hierarchy (most templates need 2-4 tiers)
1. **Primary** — the logo OR the headline/hero. Biggest, highest contrast, center of attention.
2. **Secondary** — tagline / subtitle. ~35-45% of primary size.
3. **Tertiary** — CTA, date, price badge, handle. Small but distinct (pill, badge, accent color).
4. **Ambient** — decorative scene (particles, 3D, characters) — supports, never competes.
Size ratios that read well: primary `vmin(80-110)`, secondary `vmin(26-40)`, tertiary `vmin(24-32)`. Weight: primary 800-900, secondary 500-700, tertiary 600-900.
## Text legibility (critical over busy/3D backgrounds)
- Add a scrim or shadow: `textShadow: 0 0 vmin(20) rgba(0,0,0,.7)`, or a semi-transparent panel behind text.
- Persian is RTL: set `direction: "rtl"`, use `FONT` (Vazirmatn) from `lib/fonts.ts`. See `../persian-fonts/SKILL.md`.
- Keep line length comfortable (`maxWidth ~ 86%`); never let text touch frame edges (see `../remotion-aspect-ratios/SKILL.md`).
- For gradient text use `backgroundClip: text` + a `drop-shadow` for separation.
## Logo placement
- Center-stage for logo reveals; corner/lockup for promos.
- Always provide a branded DEFAULT (the FlatRender mark) so the template looks finished before the user uploads.
- Constrain with `maxWidth/maxHeight` + `objectFit: contain` so any uploaded logo fits without distortion.
## Image / media
- `objectFit: cover` for fullscreen backdrops (with a gradient scrim for text), `contain` for product/logo.
- Add motion: slow Ken-Burns (`scale 1→1.08` + slight pan) so stills feel alive.
- Mask into shapes (rounded rect, circle) for polish.
## Timing & pacing (the reveal sequence)
Stagger — never reveal everything at frame 0. A typical 6s (180f @30) beat sheet:
- 0-30: scene/background establishes, ambient starts.
- 20-55: hero/logo springs in (`spring()`), optional flash/impact.
- 55-90: headline rises/fades in.
- 90-120: tagline fades, letter-spacing settles.
- 120-160: CTA/date pops (`spring`, glow pulse).
- last ~30f: hold everything still for readability.
Give each text element ~0.6-0.8s on screen minimum before the next competes.
## Editability checklist
- [ ] Every changeable text/logo/image/color is a prop + seeded element (key == schema field).
- [ ] Branded default for logo + sensible default copy.
- [ ] Clear 2-4 tier hierarchy; text legible over the background.
- [ ] Staggered, eased reveal with a final hold.
- [ ] Looks good with long Persian text and a tall logo (test it).
Related: `../remotion-svg-colors/SKILL.md`, `../remotion-aspect-ratios/SKILL.md`, `../persian-fonts/SKILL.md`, `../remotion-design-styles/SKILL.md`.
+121
View File
@@ -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 (1220f). 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 **1220f**; 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/SKILL.md`) 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 23f before the cut + an impact ON it (`../remotion-sound-effects/SKILL.md`).
## 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/SKILL.md`, `../remotion-aspect-ratios/SKILL.md`, `../remotion-design-styles/SKILL.md`, `../remotion-sound-effects/SKILL.md`, `../remotion-music-picker/SKILL.md`, `../remotion-character-design/SKILL.md`, `../remotion-svg-colors/SKILL.md`, `../persian-fonts/SKILL.md`, `../flatrender-template-seo/SKILL.md`.
+114
View File
@@ -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/SKILL.md` 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/SKILL.md`). |
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/SKILL.md`).
- **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/SKILL.md` 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/SKILL.md`. 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/SKILL.md`).
- 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/SKILL.md`, `../remotion-aspect-ratios/SKILL.md`, `../remotion-design-styles/SKILL.md`, `../remotion-sound-effects/SKILL.md`, `../remotion-music-picker/SKILL.md`, `../remotion-svg-colors/SKILL.md`, `../persian-fonts/SKILL.md`, `../remotion-template-catalog/SKILL.md`, `../flatrender-template-seo/SKILL.md`.