feat: add FlatRender Remotion template-creation skill suite

10 skills for building high-quality video templates with Remotion/Three.js:
design-styles, character-design, aspect-ratios, template-composition,
sound-effects, music-picker, template-catalog, svg-colors, persian-fonts,
and flatrender-template-seo (category/tags/SEO/related, code-verified).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Soroush Asadi
2026-06-21 18:50:25 +03:30
parent 136ed700dd
commit aea3b4f800
10 changed files with 691 additions and 0 deletions
+171
View File
@@ -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` (template TYPE → pattern), `remotion-template-composition`
(what's editable, used to write "what you can customize"), `persian-fonts` (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`)? 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`); 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.
+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`, `remotion-design-styles`.
+91
View File
@@ -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`, `remotion-design-styles`.
+51
View File
@@ -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` — characters must sit in the safe zone in 9:16 too).
Related: `remotion-design-styles`, `remotion-aspect-ratios`, `remotion-template-catalog`.
+49
View File
@@ -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`.
- 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`, `remotion-aspect-ratios`, `remotion-template-composition`, `remotion-svg-colors`.
+59
View File
@@ -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`, `remotion-template-catalog`.
+56
View File
@@ -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`); 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`, `remotion-template-composition`.
+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`, `remotion-design-styles`.
+54
View File
@@ -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`.
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`) → wire editable text/logo/colors (`remotion-template-composition`, `remotion-svg-colors`) → pick fonts (`persian-fonts`) → optional music/SFX → render thumbnails + preview → seed (`scripts/seed_remotion_templates.py`) → deploy.
Related: every other remotion-* skill.
+60
View File
@@ -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` 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`.
- Keep line length comfortable (`maxWidth ~ 86%`); never let text touch frame edges (see `remotion-aspect-ratios`).
- 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`, `remotion-aspect-ratios`, `persian-fonts`, `remotion-design-styles`.