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:
@@ -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 (8–12). 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 — 6–12, 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 120–155 / en 120–158 chars. One natural keyword, no stuffing.
|
||||||
|
- **Keywords** (lives in `keywords`): how Iranians actually search — «قالب آماده», «ساخت/دانلود استوری»,
|
||||||
|
«تیزر تبلیغاتی», «اینترو لوگو» — include loanword + Persian spelling both ways (استوری/Story, اینترو/Intro).
|
||||||
|
1 primary + 2–3 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 6–8; **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).
|
||||||
|
- [ ] 6–12 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 + 2–3 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.
|
||||||
@@ -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`.
|
||||||
@@ -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`.
|
||||||
@@ -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`.
|
||||||
@@ -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`.
|
||||||
@@ -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`.
|
||||||
@@ -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`.
|
||||||
@@ -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`.
|
||||||
@@ -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.
|
||||||
@@ -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`.
|
||||||
Reference in New Issue
Block a user