Compare commits

...

44 Commits

Author SHA1 Message Date
soroush.asadi 62ea110605 feat(payment): admin-editable ZarinPal settings + in-panel test payment
CI/CD / CI · Web (tsc) (push) Successful in 1m33s
CI/CD / Deploy · full stack (push) Failing after 20s
Lets the broker's ZarinPal merchant / sandbox / amount-unit be set from
Admin → درگاه پرداخت (persisted in payment.settings) instead of env +
redeploy, and adds a per-app "test payment" button that mints a real
ZarinPal StartPay link straight from the panel — no site wiring needed.

- migration 33_payment_settings.sql: singleton payment.settings + a
  transactions.is_test column. (33, not 32 — 32 is content_render_engine.)
- broker read-path precedence: per-client override > DB settings > env.
- POST /v1/admin/clients/:id/test-payment + GET/PUT /v1/admin/settings.
- admin UI: «تنظیمات زرین‌پال» tab + «پرداخت آزمایشی» button.

Adversarial-review fixes (2 confirmed HIGH):
- do NOT pre-seed the settings row — a seeded sandbox=TRUE default would
  override a production ZARINPAL_SANDBOX=false env and silently route real
  payments to sandbox.zarinpal.com until an admin untouched the toggle.
  No row → env governs until an admin saves.
- test transactions are tagged is_test and the webhook dispatcher skips
  them, so an admin smoke-test can never notify (or credit) a real client,
  regardless of metadata. Broker-authoritative, not consumer-dependent.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 00:47:10 +03:30
soroush.asadi 3748b1c8d8 fix(payment): send result redirects to the frontend + add /payment/result page
CI/CD / CI · Web (tsc) (push) Successful in 1m26s
CI/CD / Deploy · full stack (push) Failing after 28s
2026-06-25 13:17:21 +03:30
soroush.asadi dc1fe11604 feat(remotion): player default demo = IG promo (bare /player/ URL renders it)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 12:46:53 +03:30
soroush.asadi dc5ff09b67 feat(remotion): isolated client-side player (Approach A) — templates render in-browser
Render the React-Three-Fiber-v9 (React 19) templates client-side without touching the
React-18 Next host: a standalone Vite app (services/remotion/player) mounts
@remotion/player with the real FlexStory composition. The studio will embed it via an
iframe and feed scene data (URL hash for first paint, postMessage for live edits).

- player/main.tsx: reads {props, aspect, watermark}, computes duration via
  calcFlexStoryMetadata, renders <Player>. Free tier shows a watermark overlay
  (preview only — clean export stays server-authorized).
- vite.config.player.ts: builds to player-dist/ with relative base (servable at /player/).
- @remotion/player + vite added.

Verified: vite build bundles FlexStory + three.js (672 modules → 1.3MB) and serves
at /player/index.html (200). Browser render to be confirmed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 12:11:27 +03:30
soroush.asadi 40fdcf280f feat(render): always-available, fully-cancel render controls
The backend cancel was solid (CancelJob/StopJob; the dev worker abandons a cancelled
job, a real node kills its process) — but the UI couldn't reach it: the render page
had NO cancel button, and the global progress pill's X only HID the pill (the job kept
running). So a render couldn't actually be stopped from where you watch it.

- Render page: a prominent «لغو رندر» button while a render is in flight (Queued or
  Running); cancelRender() calls /renders/:id/cancel and returns to config optimistically.
  The poll now also handles a `cancelled` status (when stopped from another surface).
- Global pill: the X now CANCELS the render (with confirm) instead of just hiding it —
  so any in-flight render is cancellable from any page.
- (Dashboard MyRenders already had a working cancel.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 11:31:56 +03:30
soroush.asadi 6814e64593 fix(studio): responsive scene-preview placeholders that fit the still
The hotspot overlay used fixed percentages on a w-full/h-auto image, so a 9:16 scene
ballooned vertically and the placeholders (tuned for landscape) floated off the image.

Now the still is CONTAIN-fit inside the measured area (portrait + landscape both fit,
no overflow) and the hotspot overlay is anchored to the fitted image rectangle, so
placeholders always track and scale with the image. Hotspot positions are aspect-aware
(tall vs wide) and clamped to stay on the still.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 10:51:12 +03:30
soroush.asadi a36e96d933 fix(templates): real scene count on template pages (was always 0)
The card + detail read template.sceneCount, but the API never sent one — so the
frontend mapper hardcoded sceneCount:0 for every DB-backed template.

- content-svc: ContainerSummaryResponse + ContainerDetailResponse now carry
  SceneCount. The list computes it with one grouped query (scenes per aspect project,
  max across aspects); the detail loads scenes and counts them.
- frontend: V2ContainerSummary.scene_count → AdminProject.sceneCount → the catalog
  card/detail (adminProjectToCatalogTemplate no longer hardcodes 0).

Verified on the live local API: fr-instagram-promo → 5, single-scene templates → 1.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 10:51:12 +03:30
soroush.asadi 21b6a30f08 feat(scripts): portable template import/export (bundles)
Move a template fully between environments (local → live): container, projects, all
scenes + editable fields, shared colours/layers, its categories & tags, and the asset
files.

- export_template.py <slug> → a self-contained bundle (template.json + assets/). One
  SQL query captures the whole tree as JSON; assets are resolved from template-media
  references and copied in. Source DB via PSQL env (default = local docker).
- import_template.py <bundle> → idempotent SQL (pipe to target psql). Replaces by slug
  via one cascading delete (all content.* FKs are ON DELETE CASCADE), recreates rows
  verbatim (UUIDs preserved → FKs intact), merges categories/tags BY SLUG so they line
  up across DBs. --assets-to copies media; docker cp / mc cp hints for remote.
- TEMPLATE_BUNDLES.md documents it.

Round-trip tested on fr-instagram-promo: DB → bundle → DB restores identical
3 projects / 15 scenes / 138 fields and field values.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 10:09:41 +03:30
soroush.asadi 7725c13771 feat(seed): add Instagram channel-promo template to FlatRender (local)
- scripts/seed_instagram_promo.py — dedicated seeder for the 5-scene FlexStory IG
  promo (the generic seeder only handles CharacterStory's 2-text-per-scene shape).
  Scenes keyed `<BlockId>__<n>` (render decodes the block from the key); each scene's
  content-elements are that block's real fields (Text + Media); colours as shared.
  render_remotion_comp = FlexStory-<asp> so GetFlexStoryProps routes it.
- public/template-media/InstagramPromo* — thumbnails, per-scene stills, preview MP4.

Applied to local fr2-postgres + fr2-frontend: container fr-instagram-promo (3 aspects,
15 scenes, 138 fields), served by the gateway and the /templates detail page (200).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 09:56:40 +03:30
soroush.asadi 38229185a7 feat(remotion): IG promo posts accept images AND video
- igkit: a Media component that detects video by extension and renders a frame via
  OffthreadVideo (muted), else an Img — so any post slot takes images or reels.
- IGProfile: the profile-page grid is now editable — 6 post media fields (was static
  colour placeholders); videos get a ▶ reel badge.
- IGFeed: post slots now accept video too; labels say «عکس/ویدیو».

Verified: a profile still with an image cell + a video cell + avatar image renders
both correctly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 09:37:34 +03:30
soroush.asadi 7ed2ccc414 feat(remotion): Instagram channel-promo template + taste system + design-quality kit
The reference-round workflow, run end to end for a real template:

Taste system (how we learn the user's taste, persisted):
- references/TASTE_PROFILE.md (living design contract) + references/README.md (the
  daily loop) + a "reference round" stage in docs/TEMPLATE_BRIEF.md (provide refs or
  I suggest+mock directions).

Design-quality before/after:
- HeroDemo — the fix recipe vs the faint default: layered-depth background, a proper
  big video type scale, and a bold composed focal object. (Backgrounds were naked,
  text too small, scenes had no objects.)
- YaldaSofreh3D + IGPromoDirections + IGProfileMock — reference-match proofs
  (low-poly 3D, 3 IG-promo style directions, the realistic IG-light page).

Instagram channel-promo template (the deliverable — a flexible 5-scene FlexStory):
- igkit + 5 blocks: IGIntro, IGProfile (realistic IG-light profile, scales to all
  aspects), IGFeed (post grid), IGStats (animated count-up), IGFollowCTA (Follow taps
  to "Following").
- FlexStory gains a `finish` toggle so the IG-light scenes render clean (no brand
  grade). INSTAGRAM_PROMO preset + 3 aspect comps in Root.

Verified: a still of every scene at 9:16 renders clean; full preview MP4 rendering.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 09:16:31 +03:30
soroush.asadi 8c4bc2c626 feat(remotion): craft kit (stop-motion + paper-cut) + PaperCut block
The real visible quality leap — a handmade craft aesthetic code can't fake by being
smooth:
- craft.ts: useStopMotion (quantize the frame to "on twos/threes" + per-step jitter
  → choppy handmade motion), paperShadow (layered cast shadows for paper depth),
  PAPER_TEXTURE (procedural fibrous paper grain).
- PaperCut block: a layered paper-cut landscape — sun + 4 brand-coloured paper hills
  with real cast shadows + paper grain, rising into place on stop-motion timing with
  an idle wobble, + paper-cut title/subtitle. Re-flows 16:9/1:1/9:16.

Registry now has 13 blocks. Verified: warm Yalda render (fits the Persian/seasonal
moat) + a stop-motion demo clip showing the on-threes choppy rise.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 07:26:01 +03:30
soroush.asadi b1a51cb01b feat(remotion): shared FinishPass cinematic grade (quality floor) + @remotion/lottie
The single highest-ROI quality lift — one finish applied at the FlexStory level
lifts all 12 blocks at once, no per-block change:
- GRADE_FILTER: a headless-safe colour grade (contrast/saturation/lift) applied as
  a CSS `filter` on the content root — backdrop-filter does NOT render in headless
  Chrome, so the grade lives on the content, not an overlay.
- FinishPass: split-tone (cool-shadows multiply + warm-highlights screen) + a soft
  brand duotone + top light-bloom, layered over each scene.
- Installed @remotion/lottie@4.0.290 (artist-made animations — next lever).

Verified: visible richer/graded look on CharacterScene + Slideshow, subtle enough
to suit the muted palette, consistent across blocks.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 23:35:08 +03:30
soroush.asadi 8f34c3175f feat(remotion): +3 scene blocks (BarChart, Stomp, DeviceMockup) + catalog/toolchain docs
Unlocks the biggest catalog gaps by composition:
- BarChart: animated infographic bars (value + label, normalized, staggered grow).
- Stomp: punchy beat-synced typography — words slam in with overshoot + shake +
  accent impact bar (titles / fashion / openers).
- DeviceMockup: phone/browser frame holding the user's screenshot + title/caption
  (app / website promo); placeholder when no image.
Registry now has 12 blocks. All verified via FlexStory props-override stills.

docs: CATALOG_PLAN.md (the full template taxonomy + production map + build waves;
the Persian/Islamic occasions = the moat) and PREMIUM_TOOLCHAIN.md (the stop-motion/
paper-cut/premium tool plan; editable-backdrop architecture; Iran/OFAC reality).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 23:16:44 +03:30
soroush.asadi 866edbff8c feat(studio): scene-engine preview editor — scene image + clickable field hotspots
Replaces the misleading flat Konva canvas (for FLEXIBLE/Remotion templates) with a
real preview the user can edit against:
- ScenePreview shows the scene's rendered still (scene.image) centred, and overlays
  labelled, clickable HOTSPOTS over each editable field (logo / text), positioned by
  a layout heuristic tuned to our blocks (visual centred, text stacked below).
- Clicking a hotspot selects that field; BlockFieldForm highlights + scrolls to the
  matching field (and focusing a field highlights its hotspot) — "click the logo to
  edit it" works both ways.
- CanvasEditor branches to ScenePreview when isFlexStoryProject(); AE/Konva
  templates keep the full editor.

Fixes: (1) clicking a scene now shows its real image centre-screen; (2)/(3) the logo
and text are visible placeholders you can click to edit.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 22:09:36 +03:30
soroush.asadi 055d8365fe feat(studio): per-scene loop plays on hover (scene.demo end-to-end)
Wires the per-scene loop video all the way to the scene card:
- studio-svc: SavedSceneResponse now includes Demo (was stored + copied but never
  serialized); MapSceneResponse passes s.Demo.
- Scene type gains image?/demo?; parseScene reads them from the loaded scene data.
- SceneThumbnailBlock shows scene.image as the still and plays scene.demo (muted,
  looped) on hover, resetting on mouse-leave.

Existing projects backfilled (saved_scenes.image/demo from content.scenes). Both
services rebuilt + deployed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 21:46:07 +03:30
soroush.asadi e4fd936953 feat(seed): per-scene loop video + thumbnail for every scene
Each scene now carries both a still (content.scenes.image) AND a short ~1.5s LOOP
video (content.scenes.demo), so the studio scene cards show a looping preview, not
a static swatch.

- LOOP_SCENES = {CharacterStory, LogoMotion3D}: their scenes get a dedicated
  per-scene loop ({tid}-{asp}-c{n}-loop.mp4 / {tid}-{asp}-loop.mp4); other
  templates fall back to their full preview MP4.
- Renders 42 loops: LogoMotion3D ×3 (frames 30–74) + CharacterStory 13 scenes ×3
  aspects (frames (n-1)*90+20 … +64), each a 45-frame / 1.5s clip mid-scene.
- Seed sets image + snapshot_url + demo per scene; verified all 42 serve 200 and
  the DB wires each scene to its own still + loop.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 21:09:04 +03:30
soroush.asadi 825f25be55 fix(studio): lock the canvas for scene-engine templates (no drag/resize)
For FLEXIBLE (Remotion / FlexStory) templates the render uses fixed positions —
dragging or resizing a layer on the Konva canvas does nothing to the output, which
is confusing. Make the canvas a read-only PREVIEW for those projects: the Konva
Layer is listening=false (no drag/select/transform), the Transformer is hidden, and
the auto-thumbnail capture is skipped so the flat Konva snapshot can't overwrite the
real rendered per-scene image. Editing happens only through the field form
(BlockFieldForm). AE/Konva templates are unchanged. Gated on isFlexStoryProject().

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 09:33:20 +03:30
soroush.asadi 4bac5154ed fix(seed): real per-scene images for every scene (were only colour swatches)
Scenes were seeded with just a scene_color_svg swatch — content.scenes.image /
snapshot_url were empty, so the studio/admin scene previews showed swatches, not
the actual scene. Now every scene gets a real rendered image:
- single-scene templates → their per-aspect thumbnail;
- multi-scene templates → one still per scene, captured at that scene's own frame.

Adds the 39 CharacterStory per-scene stills (13 scenes × 3 aspects), each rendered
at (sceneIndex*90 + 45). LogoMotion3D's single scene now points at its thumbnail.
Verified: DB image/snapshot_url populated, all per-scene images serve 200, and the
stills are distinct per scene (c7 = «یک مانع», kicker ۰۷/۱۳).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 09:07:48 +03:30
soroush.asadi de8849bd94 feat(remotion): +LogoMotion3D template (Tech/3D cinematic logo reveal)
First template built through the new flow (brief → quality-gate approval → build →
seed → deploy). Tech/3D logo motion: a 3D metallic card + radial light rays + lens
flare + bloom (genuine @remotion/three), with the user's uploaded logo composited
on the card as a reliable HTML <Img> (renders any SVG/PNG/data-URI; static camera
keeps it aligned), brand text + tagline, grain. Falls back to a branded play-mark
when no logo is set. Re-flows across 16:9/1:1/9:16.

- LogoMotion3D.tsx registered per aspect in Root.tsx.
- Seeded as fr-logo-motion-3d: text fields (brandText, tagline) + a logoUrl image
  upload field + the dark-tech palette (light text) + per-aspect previews.
- 3 thumbnails + 3 preview MP4s rendered, deployed; detail page + assets serve 200.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 00:13:26 +03:30
soroush.asadi 7394c5ce78 feat(remotion): +ProductShowcase block (phone/browser device mockup)
Adds the product/app-showcase template type the engine was missing: a 2.5D device
frame (rounded phone with notch, or a browser window with traffic-lights + URL bar)
holding an uploaded screenshot, with title/subtitle and the shared Three backdrop.
Fields: screenshot, title, subtitle, device (phone|browser). Registry now 9 blocks.
Verified via FlexStory props-override stills (both device modes).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 20:18:06 +03:30
soroush.asadi a48633741e docs(remotion): audio sourcing catalog (CC0 music/SFX, Iran-aware)
From the audio-sourcing-sweep (45 sources verified). The load-bearing test —
can a paid SaaS render the audio into customers' MP4s AND vendor the file —
rules out almost all "royalty-free" libraries; only CC0/PD passes cleanly.

- USE (CC0, vendorable): FreePD (music; site dead → archive.org/details/freepd),
  Kenney.nl (SFX; the one clean-from-Iran source), Freesound-CC0, OpenGameArt-CC0.
- CAUTION: incompetech CC-BY (needs attribution pipeline), aggregators (verify
  per-track), Sonniss/Pixabay (render-input-only, never vendor raw).
- AVOID/reference-only: Mixkit/Uppbeat/Bensound/Envato/Zapsplat/… (clauses + OFAC).
- Persian = no clean CC0 bulk source → commission + self-CC0 long-term.

Real files need a VPN/non-Iran fetch (acquire-once-then-vendor makes the licence
perpetual); only the 4 self-authored ffmpeg stubs are vendored today. Firewall
rules mirror the illustration assets.json + check-assets guard (already scans audio).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 20:03:50 +03:30
soroush.asadi 3eab1056c8 feat(remotion): audio layer — self-authored music bed + transition SFX in FlexStory
Adds audio to the scene engine without any third-party/geo-blocked sourcing: the
beds + SFX are synthesized with ffmpeg, so they're license-free (CC0, self-authored)
and need no acquisition — the same play as self-authoring Lottie.

- public/audio/: music-ambient.mp3 (soft 3-tone pad, looped) + sfx-whoosh/pop/chime.
- FlexStory: optional music/musicVolume/sfx props (optional so the existing render
  binding needs no change). Renders <Audio loop> for the bed + a whoosh at each
  scene start and a chime on the final scene, driven by precomputed scene starts.
- check-assets: now also scans public/audio (+ lottie) with folder-prefixed keys;
  assets.json ledgers the 4 audio files (CC0 self-authored).

Verified: tsc clean; a 6s FlexStory render produces an MP4 with a real audio stream
(ffprobe: codec_type=audio). NOTE: these are placeholder/SFX-grade; a premium
curated music library (by vibe) is a separate sourcing sweep, and the studio music
picker → FlexStory `music` prop is a follow-up wiring.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 17:31:19 +03:30
soroush.asadi c0d04fa855 feat(studio+render): wire theme picker → saved_shared_colors → FlexStory render
Closes the theme→render gap: the studio theme picker now actually drives a
FlexStory render's colours. GetFlexStoryProps reads saved_shared_colors by
element_key (accentColor/secondaryColor/backgroundColor/textColor), but the studio
only wrote the theme into scene_data — so the picker never reached the MP4.

- studio-svc: UpdateSharedColorsAsync upserts saved_shared_colors by (project,
  element_key) + PATCH /v1/saved-projects/{id}/shared-colors endpoint +
  UpdateColorsRequest/UpdateColorItem. Mirrors UpdateSceneContentsAsync. (dotnet
  build: 0 errors.)
- gateway already wildcard-routes /v1/saved-projects/*path → studio-svc (no change).
- Next: /api/projects/[id]/colors route → gateway; project-api patchProjectColors
  + themeColorsFromSceneData (maps scene_data sceneAccentColor… → the colorSchema
  keys); performSave best-effort pushes the 4 colours alongside contents.

Chain: theme picker → store → scene_data → performSave → patchProjectColors →
gateway → studio-svc upsert → saved_shared_colors → GetFlexStoryProps → render.
Verified: Next build + dotnet build both clean; theme presets render cohesively
across all 6 (incl. dark Midnight). End-to-end studio→render needs the live stack.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 17:04:47 +03:30
soroush.asadi c1747167f3 feat(studio): Phase 4 v1 — FlexStory block-field editor
Scene-engine (FLEXIBLE) projects now get a clean per-field content editor instead
of the Konva layer panel. The scoping confirmed content VALUES already flow to
saved_scene_contents via the existing `c-`-layer + updateLayer + autosave path —
so this is purely a cleaner presentation over the working save path, no new
persistence.

- isFlexStoryProject(chooseMode) helper (FLEXIBLE → scene engine).
- BlockFieldForm: renders one labelled field per content layer (label from
  layer.name — the field's Persian label, already preserved from the content
  title), text→textarea, image→upload; writes back via the unchanged
  updateLayer(props) call. No Konva geometry/layer chrome.
- StudioSidebarContent: the "scenes" tool branches on chooseMode — FlexStory →
  BlockFieldForm, AE/Konva → SceneEditSidebarContent (zero regression).
- i18n: componentsStudioSidebarBlockFieldForm in fa + en.

Verified `npm run build`. NOTE: preview stays the live Konva canvas for v1 (a true
@remotion/player embed is deferred — 8–12MB Three.js bundle). Remaining: confirm
the FlexStory render binder reads the 4 theme colours from scene_data (already
persisted) vs saved_shared_colors (would need a small colours endpoint).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 15:40:32 +03:30
soroush.asadi 383331e8f1 feat(remotion): +2 scene blocks — LogoReveal (logo motion) + StatCounter
Grows the scene-block library toward full template-type coverage:
- LogoReveal: premium logo-motion — spring scale-in + glint sweep over the logo
  (image upload or a branded play-mark placeholder) + brand text + tagline, on the
  shared 2.5D Three backdrop. Fields: logoUrl, brandText, tagline.
- StatCounter: animated count-up to a target (English-digit value → Persian
  display) + suffix + label. Fields: value, suffix, label.

Registry now has 8 blocks. Both verified via FlexStory props-override stills.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 15:05:15 +03:30
soroush.asadi 8582e956c9 feat(studio): theme picker — 4-color brand theme + curated preset swatches
Extends the studio's 2-color palette to the full 4-color brand theme
(accent / secondary / background / text) matching the Remotion SceneColors,
so the studio's colour state maps 1:1 to the scene engine.

- studio-store: add sceneSecondaryColor + sceneTextColor + their setters + an
  applySceneTheme(accent,secondary,background,text) action (sets all four +
  recolours canvas layers: bg→background, overlays→secondary, shapes→accent,
  text→text explicitly); persist both new fields in hydrate + getSceneDataForSave.
- studio-scene-data: carry sceneSecondaryColor + sceneTextColor through
  VideoPersistedSceneData / build / parse (with defaults).
- ColorsCustomTab: 6 one-click theme presets (Warm/Berry/Midnight/Ocean/Sunset/
  Mono) + 4 manual colour inputs + Apply.
- i18n: secondaryColor/textColor/themePresets/applyTheme(+preset) in fa + en.

Verified with `npm run build`. NOTE: the theme persists in scene_data and
recolours the canvas; wiring the 4 colours all the way to a FlexStory render's
saved_shared_colors depends on the studio-svc shared-colour sync (a small
follow-up). Block-FIELD editing remains the Phase 4 follow-up.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 14:56:14 +03:30
soroush.asadi 8ddca5647b feat(studio): Phase 3 — scene reorder + numeric duration + FIX/FLEXIBLE gating
Wires the scene-list operations users asked for into the existing timeline
(model-agnostic — works for any scene, layer- or block-based):

- SceneThumbnailBlock: now sortable (@dnd-kit useSortable) with a left-edge grip
  handle (listeners only on the handle so select/rename/resize still work); adds a
  numeric per-scene duration input (commit on blur/Enter, clamped 1–30s) next to
  the drag-resize; a `locked` prop makes it read-only.
- SceneThumbnailStrip: wraps the blocks in DndContext + SortableContext
  (horizontal, 6px pointer-activation so clicks/resize aren't hijacked) and calls
  the existing reorderScenes store action; gates add/browse + reorder/duplicate/
  delete/duration behind isFixedSceneMode(chooseMode).
- studio-store: isFixedSceneMode() helper (single source for FIX vs FLEXIBLE).
- i18n: reorderScene / durationLabel / secondsUnit in fa + en.

Verified with `npm run build` (rules-of-hooks clean). NOTE: a THEME PICKER and
FlexStory block-FIELD editing are deferred — the studio editor is Konva-layer-
centric, so both need a FlexStory-aware editing path (a follow-up), not this phase.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 14:18:00 +03:30
soroush.asadi f8ea9af3b6 feat(render): Phase 2 — FlexStory render passthrough + journey template seed
Closes the render boundary so a user's scene list (order, per-scene content,
per-scene duration, theme) actually drives the FlexStory engine — the one gap the
scene-engine mapping found.

- render-svc GetFlexStoryProps (db.go): structured per-scene query that groups
  saved_scene_contents BY scene (the flat GetRenderBindings union collides when
  scenes share keys like "title"), recovers blockId from the scene key
  ("<BlockId>__<n>"), and emits the FlexStory props object
  {scenes:[{blockId,durationSec,props}], accentColor, …}.
- render-svc Claim (internal.go): when the template is Remotion + comp starts with
  "FlexStory", send that object as a single "__flexprops__" binding (no protocol
  struct change).
- node-agent remotionProps (remotion.go): if "__flexprops__" is present, pass it
  to `remotion render --props` verbatim (it's the complete props object).
- scripts/seed_flexstory.py: seeds the CharacterJourney template (7 scenes, theme
  colours, FLEXIBLE) with blockId-encoded scene keys, so the studio's existing
  CopyTemplateGraphAsync copies them into saved_scenes with zero studio-svc change.

Both Go services compile; template is live in the catalog (detail 200, per-aspect
previews). End-to-end render verification needs a live Remotion render node.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 13:45:04 +03:30
soroush.asadi 2104dd3c84 feat(remotion): theme system + CharacterJourney pilot template
- src/scenes/themes.ts: 6 curated themes (the cohesion rail) — pick one, then
  tweak the 4 brand colors; every block derives its shades so a theme re-skins
  the whole video coherently (verified: same journey rendered in warm-editorial
  vs berry-pop by overriding only the 4 colors).
- src/scenes/presets.ts: CHARACTER_JOURNEY — the pilot template's scene list
  ("Idea → struggle → tool → win", 7 beats) as a FlexStory preset.
- briefs/character-journey.md: the filled Template Spec from the guided brief.
- Root.tsx: register CharacterJourney per aspect (FlexStory + the preset).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 13:19:51 +03:30
soroush.asadi d830c56ea0 feat(remotion): FlexStory scene engine — ordered editable scene-blocks (Phase 1)
Turns a template into an ordered list of editable scene blocks instead of one
monolithic composition — the foundation for the scene-based template engine
(all Renderforest-style types, per-scene editable duration, add/duplicate/
delete/reorder). Render-side only; backend wiring is Phase 2.

- src/scenes/types.ts: SceneInstance/BlockProps/SceneBlock + withDefaults/clamp.
- src/scenes/chrome.tsx: shared 2.5D Three.js backdrop (parallax camera, blobs,
  particles, optional 3D confetti) + grain/vignette/progress/kicker/transition.
- src/scenes/blocks/*: Core 6 blocks — TitleCard, CharacterScene (full room +
  vendored CC0 character behind a desk), ImageCaption, KineticQuote, Slideshow,
  OutroCTA — each with editable fields + its own duration range.
- src/scenes/registry.ts: the block registry (blockId -> block).
- src/compositions/FlexStory.tsx: the sequencer — stacks blocks in <Sequence>,
  clamps per-scene duration, and computes composition length dynamically via
  calculateMetadata (so add/delete/reorder/duration all flow to the render).
- StoryScenes.tsx: the 2.5D story proof this productizes; docs/TEMPLATE_BRIEF.md:
  the guided creator flow + Template Spec.

Verified: all 6 blocks render via FlexStory in 16:9/1:1/9:16; a custom props
override (reordered scenes, custom characters/durations/colors) renders correctly
and the total length tracks Σ per-scene durations.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 07:45:57 +03:30
soroush.asadi fd364209e7 feat(coming-soon): hard-lock the live curtain; closable only on local/dev hosts
The curtain was sessionStorage-dismissible everywhere. NODE_ENV can't tell the
live deploy from the local Docker site (both are prod builds), so gate on the
hostname instead: localhost + private LAN ranges (incl. 172.28.x) keep the
"view experimental (local)" close button; any public domain is hard-locked to
just the countdown. Starts the curtain up by default so the live site never
flashes a page before it mounts.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 07:45:16 +03:30
soroush.asadi cb6512fee3 feat(remotion): asset-library catalog + Phase 0 (license firewall, @remotion/lottie, 30 CC0 characters)
- docs/ASSET_LIBRARY.md: curated catalog from the asset sweep (91 sources -> 62
  usable) + completeness-critic reality check; clean CC0/MIT tier, license/geo
  traps, and the 2.5D layered-scene plan (sky->room->furniture->device->character
  ->grain) to fix the "naked scene".
- deps: add @remotion/lottie@4.0.290 (runtime) + DiceBear (build-time devDep).
- scripts/gen-dicebear.mjs: generate 30 CC0 Open-Peeps characters OFFLINE (no
  runtime CDN) into public/illustrations/dicebear/ + a per-file assets.json ledger.
- scripts/check-assets.mjs: license-firewall CI guard — fails on any un-ledgered
  vendored asset.
- AssetSheet dev composition: proves vendored SVG -> staticFile() -> Remotion render
  (30 real characters render cleanly).
- NOTE: GitHub (Open Peeps/IRA/Notion git clones) + Gumroad (Lukasz) are geo-blocked
  headless here; those + Humaaans (Figma export) need a manual/mirror fetch.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 18:59:03 +03:30
soroush.asadi a3152ee84f feat(remotion): premium CharacterStory template (13 flexible scenes) + fix detail-page SSR
- CharacterStory: refined flat-illustration character (gradient-shaded sweater,
  modern hair, calm minimal face), muted editorial palette (coral/teal/sand/navy),
  abstract environment (soft depth blobs, ground "stage", sparse particles,
  vignette + grain), scene-number kicker. Verified in 16:9/1:1/9:16 and all poses.
- seed: 13 editable scene cards (c1..c13, keys s{N}_title/s{N}_text) via new
  MULTISCENE path; per-aspect previews; muted defaults.
- assets: 3 thumbnails + 4 preview MP4s vendored into public/template-media.
- fix: load BrandedVideoPlayer (plyr-react) client-only via next/dynamic
  (ssr:false) — plyr touches `document` at import, which was 500-ing every
  template detail page during SSR.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 16:58:48 +03:30
soroush.asadi 863b9503b3 fix(detail+docker): per-aspect template preview + Debian frontend base
CI/CD / CI · Web (tsc) (push) Successful in 1m17s
CI/CD / Deploy · full stack (push) Failing after 15s
- Template detail page now shows the render matching the SELECTED aspect (poster +
  preview video) instead of the 16:9 cover cropped into a 9:16/1:1 box. TemplateVariant
  carries per-aspect image/previewVideo; fetchTemplateVariants + the detail page wire them.
- AppShowcase3D ships a distinct preview video per aspect (seed PERASPECT_VIDEO).
- Frontend Dockerfile: Alpine -> node:20-slim (glibc). Fixes next-swc ("ld-linux..."
  load failure that broke `next build` once libc6-compat was removed) AND the original
  CI Alpine-CDN issue. Healthcheck switched to node (slim has no wget).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 23:04:04 +03:30
soroush.asadi 60759f35b4 polish(remotion): shiny titanium finish on AppShowcase3D phone
CI/CD / CI · Web (tsc) (push) Successful in 1m22s
CI/CD / Deploy · full stack (push) Failing after 14s
Polished-metal look: low-roughness (0.15) titanium + contrasty studio Environment
(light bases + bright softbox strips) so the rounded edges catch hot reflection
streaks that sweep as the phone rotates; shinier side buttons. Re-rendered all
aspects + preview, redeployed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 22:00:26 +03:30
soroush.asadi 1795bc855b feat(remotion): premium 3D app-showcase template (AppShowcase3D)
CI/CD / CI · Web (tsc) (push) Successful in 1m19s
CI/CD / Deploy · full stack (push) Failing after 12s
- New @remotion/three template: titanium flagship phone (thin rim, glossy black
  glass, rounded-corner screen via ShapeGeometry, dynamic island, side buttons),
  light keynote studio (contact shadow + env reflections + DOF + soft bloom),
  film grain + entrance light-sweep. All 3 aspects re-flowed.
- Editable screenUrl (user app screenshot textured onto the screen via TextureLoader
  + delayRender), appName/tagline/cta, 4 colours (dark text on light bg).
- Add pick(wide,square,tall) helper to lib/aspect.ts (Tier-0 from the R&D).
- Seed: AppShowcase3D + per-template text colour; built with the flat-artist skill.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 21:28:49 +03:30
soroush.asadi f83d657844 chore(skills+remotion): add flat-artist skill bundle; register 3D templates
CI/CD / CI · Web (tsc) (push) Successful in 1m19s
CI/CD / Deploy · full stack (push) Failing after 12s
- .claude/skills/flat-artist: the bundled FlatRender template-creation suite
  (orchestrator + 16 sub-skills + design/motion R&D), mirrors the Gitea AISkills repo.
- services/remotion Root.tsx/templates.tsx: register the 3D templates + Three3DTest.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 19:39:25 +03:30
soroush.asadi cb11c177a7 fix(ci): stop pulling Alpine packages from the geo-blocked CDN
CI/CD / CI · Web (tsc) (push) Successful in 1m21s
CI/CD / Deploy · full stack (push) Failing after 15s
The CI server can't reach dl-cdn.alpinelinux.org (TLS error) — only the Nexus
mirror is reachable, and it proxies Docker images, not apk packages.

- frontend: drop `apk add libc6-compat` (vestigial Next.js-template line; the
  deps stage only runs `npm ci` and the build/runtime stages never had it).
- 5 Go services (file/gateway/notification/payment/render): replace
  `apk add ca-certificates tzdata` with copying ca-certificates.crt from the
  golang builder stage + embedding tzdata via `go build -tags timetzdata`.
  No more apk -> no dependency on the Alpine CDN.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 17:08:16 +03:30
soroush.asadi af3c73c560 feat(templates): branded Plyr video player for demos with download protection
CI/CD / CI · Web (tsc) (push) Successful in 1m24s
CI/CD / Deploy · full stack (push) Successful in 8m14s
- BrandedVideoPlayer (plyr-react) plays template demo videos with FlatRender
  blue branding (Plyr CSS vars) and NO download control.
- Download protection: no download button, native controls replaced, underlying
  <video> gets controlsList="nodownload" + disablePictureInPicture, and the
  right-click context menu is blocked.
- Template detail page uses it; gallery hover-preview cards get the same
  nodownload / no-context-menu / no-PiP guards.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 16:44:55 +03:30
soroush.asadi 4f04f6bf75 feat(render+templates): Remotion engine, 16 branded templates (incl. 3D), seconds pricing, coming-soon
CI/CD / CI · Web (tsc) (push) Successful in 1m21s
CI/CD / Deploy · full stack (push) Failing after 20s
Render engine
- Add Remotion (code-based) as a 2nd render engine alongside After Effects.
  node-agent dispatches on Job.Engine; RunRemotion maps bindings -> --props,
  renders native then ffmpeg-scales to the quality tier (aspect-preserving).
- content.projects.render_engine + render_remotion_comp (migration 32);
  render-svc claim resolves engine and routes (skips .aep for Remotion).
- Admin TemplatesAdmin gains an engine picker + Remotion composition id field.

Template pack (services/remotion)
- 16 branded, Persian (Vazirmatn), color- and text-editable templates, each in
  3 aspects (16:9 / 1:1 / 9:16): LogoMotion, Opener, InstaPromo, YouTubeIntro,
  Slideshow, HappyBirthday, SalePromo, QuoteCard, EventInvite, Countdown,
  GlitterReveal (editable logo image), NowruzGreeting (animated characters),
  and 4 cinematic 3D templates via @remotion/three (Hero3D, Nowruz3D,
  Birthday3D, Promo3D) with reflections + bloom/DOF/vignette.
- scripts/seed_remotion_templates.py seeds containers/projects/scenes/colors.

Pricing
- Rewrite /pricing to the seconds-based model (charge = length x resolution),
  data-driven from /v1/plans, Toman, broker checkout.

Coming-soon
- Persian experimental-build overlay on all pages (launch date + countdown).

Fixes
- middleware matcher bypasses all static asset paths; catalog mapping passes
  cover image + preview video so real thumbnails render.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 15:52:52 +03:30
soroush.asadi b9b91397b0 fix(deploy): configurable postgres host port (avoid 5432 conflict)
CI/CD / CI · Web (tsc) (push) Successful in 1m10s
CI/CD / Deploy · full stack (push) Successful in 3m58s
fr2-postgres failed to start after another local project's postgres grabbed host
port 5432 during downtime. The internal stack always connects via postgres:5432 on
the docker network, so the published host port is only for external tooling — make
it ${PG_HOST_PORT:-5532} to avoid the clash. (Also recovered from a stale bind-mount
where scripts/init-db.sh had become a directory; current compose mounts
deploy/postgres-initdb/.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 01:38:33 +03:30
soroush.asadi 6d79ddb8d1 feat(render): real progress %, ETA, and frequent preview during AE renders
The render page already displayed progress/ETA/preview — but the node agent never
fed real data: aeRender used fake +5%/10s increments, discarded aerender stdout,
and pushed a preview only every 30s. (Plus the deployed agent predated even the
progress-reporting wiring.)

node-agent (aeRender):
- Capture aerender stdout; parse "(N):" current frame + "N frames"/"to N" total.
- Real percentage when total is known (5–90%, headroom for transcode/upload),
  else a smooth time-asymptotic estimate that never sticks — message shows the
  live frame number either way.
- Push a preview frame ~every 8s (was 30s) so the box fills in quickly.

render-svc:
- GET /v1/renders/:id/progress now computes eta_seconds from started_at + progress
  (linear extrapolation) instead of returning null.

frontend:
- Thread eta_seconds → status route → render page; page prefers the server ETA and
  falls back to the client-observed rate.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 01:18:54 +03:30
soroush.asadi 23d1fd8fb1 fix(payment): default broker host port to 1607 (8090 was allocated on the server)
CI/CD / CI · Web (tsc) (push) Successful in 1m10s
CI/CD / Deploy · full stack (push) Successful in 3m6s
2026-06-16 07:28:05 +03:30
417 changed files with 20368 additions and 383 deletions
+89
View File
@@ -0,0 +1,89 @@
---
name: flat-artist
description: >-
The master entry point and professional motion-design persona for creating FlatRender video
templates with Remotion + Three.js. INVOKE THIS FIRST whenever you start, design, build, review,
or seed a FlatRender template — or whenever "Flat Artist", "flatrender template", "بساز/طراحی قالب",
a new logo reveal / promo / greeting / intro, or any template work is mentioned. It activates the
whole template-creation skill suite (design, animation, characters, aspect ratios, composition,
color/SVG, fonts, hooks, transitions, effects, music, SFX, assets, SEO) as ONE professional
pipeline and tells you which sub-skill to apply at each step, to a Renderforest / After-Effects
quality bar. Persian-first.
---
# Flat Artist — the FlatRender template studio
You are **Flat Artist**: a professional motion-design artist building FlatRender templates to a
Renderforest / After-Effects bar. When this skill is active, you ALWAYS work the full pipeline below
and apply the sub-skills — never ship "basic". Stack: `services/remotion/` (Remotion 4 +
`@remotion/three`, R3F v9, `gl="angle"`). Persian (fa) is canonical; English mirrors it 1:1.
## How to use this skill
This skill BUNDLES the whole suite: every sub-skill is a folder beside this file, at
`<skill-name>/SKILL.md` (relative to this `flat-artist/SKILL.md`). You don't invoke them as
separate skills — you **read the bundled file** for the phase you're on and apply it.
1. Read this file's pipeline + rules.
2. At each phase, open the bundled sub-skill at **`<skill-name>/SKILL.md`** (e.g. `motion-design-principles/SKILL.md`) and apply it.
3. The deep R&D reference is **`references/design-motion-rnd.md`** (bundled here — trends, craft, asset pipeline, masterpiece + platform playbook); consult it for art direction and the masterpiece bar.
## Non-negotiable rules (apply on EVERY template)
- **Pure frames:** animate ONLY off `useCurrentFrame()`. Never `useFrame` (R3F), `Math.random()`, `Date.now()`, `setState`/`useEffect`-driven motion. Determinism via `rand(i)` (`lib/anim.ts`).
- **3D:** `@remotion/three` only; reuse `lib/three-kit.tsx` (StudioEnv/Lights/Floor/Effects, Confetti3D); drive camera/objects from `frame`.
- **Three real aspects:** 16:9 / 1:1 / 9:16 must **re-flow, never letterbox**. Design tall-first; branch layout with `useLayout()` + `pick(wide,square,tall)` (`lib/aspect.ts`). Verify by rendering a still in ALL THREE.
- **Persian-first:** `FONT` (Vazirmatn), `direction:"rtl"`; split kinetic text by **word, not char**; preserve ZWNJ; Persian numerals where apt.
- **Recolorable:** every themeable color comes from `colorSchema` props (accent/secondary/background/text in `lib/branding.ts`) — no hardcoded hex on editable elements.
- **Editable = a prop + a seeded element:** text/logo/image keys MUST equal the Zod schema field; seed via `scripts/seed_remotion_templates.py`.
- **Vendored assets (Iran iron rule):** every asset committed into `services/remotion/public/` + `staticFile()`; NEVER a CDN URL at render. License firewall (CC0/commercial-ok, tracked in `assets.json`).
- **Finish it:** a masterpiece = the 8 layers below, not one big move. Render → LOOK → refine.
## The pipeline (work in order; read the bundled skill named at each step)
| # | Phase | Apply skill |
|---|---|---|
| 0 | **Scope & storyboard** — pick the template TYPE/pattern; confirm a storyboard with the user before building anything character- or scene-heavy | `remotion-template-catalog/SKILL.md` |
| 1 | **Art direction** — choose ONE coherent style + palette from current trends | `remotion-design-styles/SKILL.md` + R&D report |
| 2 | **Hook** — design the scroll-stopping first 13s (it's the cover frame) | `video-hooks/SKILL.md` |
| 3 | **Characters** (if any) — build/rig from SVG or 3D primitives | `remotion-character-design/SKILL.md` |
| 4 | **Build the composition** — lib helpers + `three-kit` for 3D | (this skill) |
| 5 | **Motion** — easing/timing/stagger/secondary motion (the foundation) | `motion-design-principles/SKILL.md` |
| 6 | **Kinetic type** — animate the hero/caption text (Persian word-split) | `kinetic-typography/SKILL.md` |
| 7 | **Transitions** — scene-to-scene choreography, seamless | `scene-transitions/SKILL.md` |
| 8 | **Effects** — grain, bokeh, light-leaks, sparkles, glow, vignette (deterministic) | `particles-and-effects/SKILL.md` |
| 9 | **Aspect re-flow** — make it truly fit 16:9/1:1/9:16 | `remotion-aspect-ratios/SKILL.md` |
| 10 | **Composition & elements** — hierarchy, logo/image/copy, reveal pacing | `remotion-template-composition/SKILL.md` |
| 11 | **Color / live recolor** — wire color props + SVG color preview | `remotion-svg-colors/SKILL.md` |
| 12 | **Fonts** — pick Persian-first type by role | `persian-fonts/SKILL.md` |
| 13 | **Assets / footage** — source, license, prepare, composite | `asset-sourcing/SKILL.md` |
| 14 | **Music + SFX** — beat-sync reveals, place SFX, duck | `remotion-music-picker/SKILL.md` + `remotion-sound-effects/SKILL.md` |
| 15 | **QA — masterpiece gate** — the 8 layers + pre-ship checklist (below) | (this skill) |
| 16 | **SEO & taxonomy** — category, tags, keywords, slug, copy, related | `flatrender-template-seo/SKILL.md` |
| 17 | **Ship** — render 3 thumbnails + preview, seed, deploy (copy assets into the running container + restart so Next re-scans `public/`) | (this skill) |
## The masterpiece bar (8 production-value layers — finish ALL)
1. Sound design + beat-sync (hero on a downbeat; whoosh/thump/sparkle/riser; ducking; silence before the hero hit).
2. Micro-detail (no linear easing; overshoot & settle; 25f staggered entrances; secondary motion; anticipation).
3. Design system (one type scale, one spacing rhythm, constrained palette, consistent radii/strokes/elevation).
4. Depth & lighting (parallax layers, one consistent light direction, atmospheric bg blur, rim light on hero).
5. Color grade (one unifying grade over the whole comp; user hex still passes through it).
6. Pacing/rhythm (vary cut length, build to a climax, trim ruthlessly).
7. A clear hero moment (one peak with the biggest motion + strongest hit).
8. Finishing texture (subtle grain, gentle vignette, 12px chromatic aberration at impacts, tiny continuous camera drift, motion blur on fast elements).
## Pre-ship checklist (if you can't tick it, it's not done)
- [ ] No linear easing; staggered entrances; ≥1 overshoot-and-settle; nothing pops on/off without a transition.
- [ ] Verified in 16:9 / 1:1 / 9:16 (re-flowed, not letterboxed); long Persian text doesn't crop; short text doesn't look empty.
- [ ] Hook lands in the first 12s; first frame works as the cover.
- [ ] One unmistakable hero moment; consistent light direction + a unifying grade.
- [ ] Subtle grain/vignette; frame feels alive; (audio: beat-synced + ducked if present).
- [ ] All editable fields (text/logo/image/colors) swap without breaking layout; colors from props.
- [ ] Clean render at target res (no flicker/z-fighting/font fallback); assets vendored + licensed.
- [ ] Category/tags/keywords/slug + fa+en copy set (`flatrender-template-seo/SKILL.md`).
## Project map
- Compositions: `services/remotion/src/compositions/`, registered in `src/templates.tsx` (`TemplateDef`, all 3 aspects).
- Helpers: `lib/anim.ts`, `lib/aspect.ts` (`useLayout`/`pick`), `lib/branding.ts` (`colorSchema`/`BRAND`), `lib/fonts.ts` (`FONT`), `lib/three-kit.tsx`.
- Seed: `scripts/seed_remotion_templates.py` (containers/projects/scenes/colors + `MEDIA` for image fields).
- Render checks: `npx remotion still src/index.ts "<Comp>-16x9|1x1|9x16" out/x.png --frame=NN`.
- Bundled here: each sub-skill at `<name>/SKILL.md`; the deep R&D at `references/design-motion-rnd.md`.
Bundled sub-skills (read each at its `SKILL.md`): `remotion-template-catalog/SKILL.md`, `remotion-design-styles/SKILL.md`, `video-hooks/SKILL.md`, `remotion-character-design/SKILL.md`, `motion-design-principles/SKILL.md`, `kinetic-typography/SKILL.md`, `scene-transitions/SKILL.md`, `particles-and-effects/SKILL.md`, `remotion-aspect-ratios/SKILL.md`, `remotion-template-composition/SKILL.md`, `remotion-svg-colors/SKILL.md`, `persian-fonts/SKILL.md`, `asset-sourcing/SKILL.md`, `remotion-music-picker/SKILL.md`, `remotion-sound-effects/SKILL.md`, `flatrender-template-seo/SKILL.md`.
@@ -0,0 +1,95 @@
---
name: asset-sourcing
description: How to source, license, AI-generate, prepare, and organize royalty-free assets (footage, images, textures, HDRIs, GLTF/GLB, icons, illustrations) for FlatRender Remotion templates — Iran-aware (geo-blocks), vendored-only, license-firewalled. Use when a template needs real media, when downloading/committing assets into public/, when grading/masking/looping footage or compositing it via Video/OffthreadVideo/Img/staticFile + Ken-Burns, or when generating bespoke assets with local AI models.
---
# Asset sourcing for templates
Project: `services/remotion/`. Helpers: `src/lib/anim.ts` (`hexToRgba`, `mixHex`, `rand`), `src/lib/aspect.ts` (`useLayout``isWide/isSquare/isTall`, `vmin`, `unit`), `src/lib/branding.ts` (`colorSchema`, `BRAND`), `src/lib/fonts.ts` (`FONT` = Vazirmatn, RTL), `src/lib/three-kit.tsx` (`StudioEnv/Lights/Floor/Effects`, `Confetti3D`). Render is **headless Chrome in Docker** — every value derives from `useCurrentFrame()` (never `Math.random`/`Date.now`/`useFrame`; use `rand(i)`).
## The Iron Rule — vendor everything
The Iran environment punishes runtime dependencies. **Download once (VPN if needed), commit into `public/`, reference with `staticFile()`.** Never put `https://…` in a shipped template — a geo-block or flaky tunnel kills the render mid-frame. Mirror npm/NuGet/Docker via Nexus (`mirror.soroushasadi.com`); asset *binaries* are sourced by hand. **Record the license at acquisition time, not later.** `public/` today holds only `fonts/` — you build the rest.
## License taxonomy (know cold — this is the firewall)
| Class | Examples | Ship? |
|---|---|---|
| CC0 / Public Domain / Pixabay / Pexels / Unsplash | Poly Haven, ambientCG, Kenney, Mixkit | ✅ free, no credit — **default target** |
| CC-BY | many Sketchfab, Bensound | ⚠️ ship only with a tracked on-screen/end-card credit |
| CC-BY-SA | some Wikimedia | ❌ share-alike can infect our proprietary template |
| CC-BY-NC | "free for personal" tiers | ❌ we are a **paid** product = commercial |
| Editorial / rights-managed | news/celebrity stock | ❌ |
| Paid stock | Envato, Adobe, Shutterstock | ✅ per license — **keep the receipt/PDF** |
No license row = unknown license = **do not ship**.
## Sourcing map (CC0 / no-attribution first) + Iran access
| Type | Best CC0 sources | Commit to | Iran access |
|---|---|---|---|
| Footage (H.264 MP4, right-sized) | Pexels Video, Pixabay Video, Mixkit, Coverr, Videvo (filter CC0) | `public/footage/{nature,business,abstract}/` | Pixabay/Mixkit/Coverr OK; Pexels VPN-ish |
| Images | Pexels, Pixabay, Unsplash, StockSnap, Burst | `public/images/` | Pixabay OK; Pexels/Unsplash VPN-ish |
| Textures / overlays | Poly Haven, ambientCG; grain/light-leak/dust CC0 clips | `public/textures/`, `public/overlays/` | OK |
| HDRIs (1k2k for render speed) | Poly Haven, ambientCG | `public/hdri/` | OK |
| 3D (**prefer GLB** over glTF+textures) | Poly Haven Models, Kenney, Khronos glTF samples, Sketchfab (check each) | `public/models/` | Poly Haven OK; Sketchfab VPN-ish |
| Icons (bundle via Nexus npm, **never CDN**) | Lucide, Tabler, Heroicons, Phosphor | npm dep | npm via Nexus OK |
| Illustrations (recolorable **SVG**) | unDraw, Open Peeps, Humaaans | `public/illustrations/` | OK |
For Persian/Iran imagery search English terms ("Tehran", "Iranian food") + self-shot/local stock. **Sanction-blocked at account/payment: Adobe Stock, Envato** — use a foreign account/partner or skip. **Mitigation: do one batched "asset run" over a stable tunnel, commit binaries, render never touches the open internet again.** Draco-compress GLBs (`gltf-pipeline -i in.glb -o out.glb -d`), keep low-poly for headless render speed.
## AI-generated assets — when it's right
- **Use when:** the asset doesn't exist as stock (specific Persian cultural scene, branded mascot), you need consistency across a template set (reference-image control), or it beats a 5-site license hunt.
- **Don't when:** clean CC0 already exists, you need photographic authenticity, or a free tier's **commercial license is unclear** (watermarks / non-commercial = legal landmine for a paid product).
- **Iran-pragmatic:** self-host open models — **HunyuanVideo 1.5** (~RTX 4090, no geo-block/payment/watermark) for video; **FLUX/SDXL** locally for image/texture/illustration. Hosted SaaS (Runway, Kling) only when local quality falls short and a VPN+foreign-account path exists. **Always record prompt + tool + plan-tier + date** in the asset's `.license.txt` sidecar.
## Preparing footage in Remotion (composite, grade, mask, loop)
**Primitives:** `<OffthreadVideo>` = default for **all** video in a render (FFmpeg extraction, deterministic, no seek drift). `<Video>` = preview only. `<Img>` over raw `<img>` (waits for load → no half-loaded frames). `staticFile()` for every vendored asset.
```tsx
import { OffthreadVideo, Img, staticFile, useCurrentFrame, interpolate, Easing } from "remotion";
import { useLayout } from "./lib/aspect";
const frame = useCurrentFrame();
const L = useLayout();
// Ken-Burns: overscan ≥1 so no edges reveal; cover + center crops cleanly in all 3 aspects.
const scale = interpolate(frame, [0, 150], [1.08, 1.2], { extrapolateRight: "clamp" });
const ty = interpolate(frame, [0, 150], [0, L.vmin(-30)], { easing: Easing.out(Easing.cubic), extrapolateRight: "clamp" });
<OffthreadVideo
src={staticFile("footage/nature/forest-loop.mp4")}
style={{ width: "100%", height: "100%", objectFit: "cover",
transform: `scale(${scale}) translateY(${ty}px)`,
filter: "contrast(1.08) saturate(1.15) brightness(0.96)" }} // grade
/>
```
| Job | Pattern |
|---|---|
| **Color grade** | per-layer CSS `filter` (`contrast/saturate/brightness/hue-rotate`); build a shared `lib/grades.ts` (`warm`, `teal-orange`, `mono`, `filmic`) so palette can drive `hue-rotate`/`saturate`. Heavy grade → pre-grade in DaVinci Resolve (free), then commit. |
| **Masking / keying** | no native keyer — pre-key in Resolve/AE, export **alpha** (ProRes 4444 or WebM/VP9 alpha), then `<OffthreadVideo>`. Shape masks via CSS `maskImage`/`clipPath` + `hexToRgba` gradients, or SVG `<mask>`. |
| **Seamless loop** | source loop-designed clips (Coverr/Mixkit) or mirror-pingpong; `<OffthreadVideo loop>` once first/last frames match; crossfade-to-self with overlapping `<Sequence>` for imperfect footage. |
| **Overlays (cheap "authentic" layer)** | stack grayscale-on-black/white clips: **screen** for light-leaks/bokeh/dust, **overlay/soft-light** for grain, **multiply** for vignettes/paper. Keep palette-independent. **Animated grain must move** — offset `background-position` per frame or jitter SVG `feTurbulence` `seed`. |
| **Per-aspect crop** | `objectFit:"cover"` + center-safe framing; branch focal point on `L.isWide/isSquare/isTall` (or the proposed `L.pick(wide,square,tall)`) so the subject never crops out. |
HDRIs/GLBs: feed `staticFile("hdri/…")` into `three-kit`'s `StudioEnv`; load models with `useGLTF(staticFile("models/…glb"))`, idle-bob with `Math.sin(frame/fps)` (driven by `useCurrentFrame`, **not** `useFrame`).
## Library structure + attribution firewall
Create under `public/`: `footage/{nature,business,abstract}/`, `overlays/`, `images/`, `textures/`, `hdri/`, `models/`, `icons/`, `illustrations/`, plus **`assets.json`** + **`ASSETS.md`**. Lowercase-kebab names, no spaces. Every asset gets one `assets.json` row **at download time**:
```json
{ "file": "footage/nature/forest-loop.mp4", "source": "Pexels",
"url": "https://www.pexels.com/video/...", "author": "Name",
"license": "Pexels", "attribution_required": false, "commercial_ok": true,
"acquired": "2026-06-21", "notes": "1080p H.264, loops clean" }
```
Sidecar `.license.txt` next to AI assets (prompt + tool + date) and paid receipts. A **CI validation script** asserts every file in the media folders has a matching row with `commercial_ok:true`, else fails the build — this is the firewall. `ASSETS.md` is the generated human/legal-readable table. `attribution_required:true` must surface a credit on a shippable surface (end-card/footer). If the repo bloats, move large media to MinIO (already in stack) with a `predeploy` sync into `public/` — but present at render time.
## Checklist (before committing an asset / shipping a template)
- [ ] Vendored in `public/…` and referenced via `staticFile()`**no external URL** anywhere in the template.
- [ ] `assets.json` row added with `commercial_ok:true`; `.license.txt`/receipt for AI/paid; CC-BY credits surfaced.
- [ ] Right-sized (don't ship 4K into a 1080p comp); video is H.264 MP4 played via `<OffthreadVideo>`; images via `<Img>`.
- [ ] GLB (not glTF+loose textures), Draco-compressed, low-poly; HDRI 1k2k.
- [ ] Footage graded through `lib/grades.ts`; overlay grain/light-leak **moves** per frame; loops are seamless.
- [ ] Ken-Burns overscans (start scale ≥ 1.05) and `objectFit:cover` crops cleanly in 16:9 / 1:1 / 9:16 with subject in frame.
- [ ] Re-render twice → identical (deterministic; nothing pulled from network/random/date).
Related: `../remotion-design-styles/SKILL.md`, `../remotion-template-composition/SKILL.md`, `../remotion-aspect-ratios/SKILL.md`, `../remotion-character-design/SKILL.md`, `../remotion-svg-colors/SKILL.md`, `../remotion-music-picker/SKILL.md`, `../remotion-sound-effects/SKILL.md`, `../persian-fonts/SKILL.md`, `../flatrender-template-seo/SKILL.md`.
@@ -0,0 +1,171 @@
---
name: flatrender-template-seo
description: >-
Classifies and merchandises a FlatRender video template when an admin or developer creates or
publishes it. Decides the single primary category, the faceted tags, the SEO title/description/
keywords/slug, the Persian-first presentation copy, and the related-templates set — every choice
driven by the template's actual design, editable content, and usability. Use whenever creating,
editing, publishing, or seeding a template (project_container) and you must fill category_ids,
tag_ids, keywords, slug, description, news_text, or write its catalog/detail copy. Persian (fa) is
the source of truth; English (en) mirrors it 1:1.
---
# FlatRender Template SEO & Taxonomy
Decide a template's category, tags, SEO, presentation copy, and related set — grounded in what the
template *actually is*, not what you wish it ranked for. **fa is canonical; en mirrors it.**
## When to use
- An admin creates/edits a template in `TemplatesAdmin.tsx`, or you set fields on `content.project_containers`.
- You run/extend `scripts/seed_remotion_templates.py` (which currently leaves SEO + taxonomy EMPTY — see below).
- You publish (`is_published = true`) and need the template to be findable and well-presented.
Cross-reference: `../remotion-template-catalog/SKILL.md` (template TYPE → pattern), `../remotion-template-composition/SKILL.md`
(what's editable, used to write "what you can customize"), `../persian-fonts/SKILL.md` (RTL copy & Persian numerals).
## The real data model (use these exact names — do not invent fields)
Template = `content.project_containers`. Admin sets via `TemplatesAdmin.tsx``POST/PUT /v1/templates`
(`Create/UpdateContainerRequest`). Settable, SEO/taxonomy-relevant fields:
| Purpose | Admin label | form key | DTO | DB column |
|---|---|---|---|---|
| Name (→ H1) | نام | `name` | `Name` | `name` |
| Slug | اسلاگ | `slug` | `Slug` | `slug` (CITEXT UNIQUE) |
| Description / presentation copy | توضیحات | `description` | `Description` | `description` |
| Keywords (opaque free text) | کلمات کلیدی (سئو) | `keywords` | `Keywords` | `keywords` (TEXT) |
| Announcement (NOT seo) | متن خبر | `news_text` | `NewsText` | `news_text` |
| Categories (M2M) | دسته‌بندی‌ها | `category_ids[]` | `CategoryIds` | `container_categories` |
| Tags (M2M) | برچسب‌ها | `tag_ids[]` | `TagIds` | `container_tags` |
| Primary mode | حالت اصلی | `primary_mode` | `PrimaryMode` | `primary_mode` enum |
**Hard constraints from the platform (work within these):**
- **No `meta_title` / `meta_description` on a template.** The only SEO string on a container is `keywords`.
Per-page `meta_title`/`meta_description`/`meta_keywords`/`bot_follow` exist ONLY on `content.categories`
(and on `blogs`). Workaround: put the page's discoverable meta intent into `keywords`; pack the human
title/description into `name` + `description`; lift the rest of the meta from the **assigned category's**
`meta_*` fields (so choose the category deliberately — its SEO is inherited).
- **The public detail page emits title-only metadata** (`templates/[id]/page.tsx``{ title: "${name} — FlatRender" }`),
and the public mapper (`admin-api.ts` `containerToAdminProject`) DROPS `keywords`, `coverImageUrl`→OG, and
hardcodes `categoryName: undefined`. So today your `keywords`/category never reach the public `<head>`.
Still fill them: they power the backend list filters and are the fix-point when the frontend SEO gap is closed.
When you can, also fix the mapper to surface `keywords` + cover OG; otherwise flag it.
- **No related-templates table/column/endpoint exists.** "Related" is computed by re-querying
`GET /v1/templates?categoryId={guid}` or `?tagSlug={slug}` and excluding the current template.
There is no precomputed set — pick the category/tags such that this query returns good neighbors.
- **Tags are not shown anywhere in the templates UI** today, and the list page ignores the real DB
categories: the sidebar is ~12 hardcoded buckets (11 real categories + `"all"`), and because the
public mapper hardcodes `categoryName: undefined`, every template defaults to `"social"`. Tags/categories
still matter for the backend filters and future UI — set them correctly regardless.
- `demo_script_tag` exists in DB + DTO but is NOT in the admin form — you cannot set it there.
- Categories are a tree (`parent_id`); `GET /v1/categories` only eager-loads one child level. Keep the
primary set flat (812). Tags have `applies_to_mode`; categories do not.
## Step-by-step decision process
Run this for every template, in order. Each step feeds the next.
**0. Read the design + content + usability.** What does it output (format)? What's editable (text/colors/
logo/images/music — from `../remotion-template-composition/SKILL.md`)? Who hires it and when? 2D or 3D (real Remotion
build: SVG vs `@remotion/three`)? Which aspects exist (16:9 / 1:1 / 9:16 child projects)?
**1. Category — pick exactly ONE primary.** Category = the *output format*, never the occasion/industry.
Test: which single shelf would a user expect this on? If the answer is a format (logo reveal, story, slideshow)
it's the category; if it's when/why/who (Nowruz, real estate, teens) that's a TAG. Use the flat set:
اینترو و لوگو / Intro & Logo · استوری و ریلز / Story & Reels · پست شبکه اجتماعی / Social Post ·
تبلیغ و پروموشن / Promo & Ad · اسلایدشو / Slideshow · معرفی محصول / Product Showcase ·
دعوت‌نامه و مناسبت / Invitation & Event · تیتراژ و زیرنویس / Titles & Lower-thirds ·
ارائه و اینفوگرافیک / Presentation & Infographic · یوتیوب و اینترو کانال / YouTube & Channel.
No "Other"; no category with <5 templates. → set `category_ids` (the primary first; its `sort` = index 0).
**2. Tags — 612, from a controlled fa↔en vocabulary**, each TRUE of this design, across facets:
use-case · occasion · industry · style/aesthetic · aspect ratio · color/mood · 2D-3D · audience.
Empty facets are fine — don't invent tags. Aspect ratio is a TAG (one template, three outputs), never a
separate catalog entry. Tag 3D only if it really renders 3D. Reuse existing `content.tags` (match by
`slug`/`latin_name`); create a new tag only as a deliberate dictionary edit. → set `tag_ids`.
**3. SEO title / description / keywords / slug.**
- **Title** (lives in `name`): `{Template Name} | {Category} {use/occasion}`. Keep the keyword in the
first ~30 chars (Persian runs long); brand is appended by the layout, don't double it.
- **Description** (lives in `description`, also reused as presentation copy): benefit + what + how-to-edit +
soft CTA. fa 120155 / en 120158 chars. One natural keyword, no stuffing.
- **Keywords** (lives in `keywords`): how Iranians actually search — «قالب آماده», «ساخت/دانلود استوری»,
«تیزر تبلیغاتی», «اینترو لوگو» — include loanword + Persian spelling both ways (استوری/Story, اینترو/Intro).
1 primary + 23 secondaries. Note: `keywords` is an opaque free-text column — the platform does NOT
tokenize, split, index, or search on it (no comma parsing anywhere in form or backend). Commas are
purely an authoring convention for human readability; write it however reads cleanest.
- **Slug** (`slug`): the slugifier KEEPS Persian (no transliteration). Prefer a curated **latin/transliterated**
keyword slug for shareable URLs (Persian slugs become `%D9%82...` when copied). Lowercase, hyphenated, short,
keyword-bearing. Never change a published slug.
**4. Presentation copy** (write into `description`; structure it so it doubles as SEO + usability):
H1 = human Persian `name` → one-line benefit hook → "این قالب برای چیست؟" (use-case+occasion+audience) →
"چه چیزهایی قابل تغییر است؟" (bullet the REAL editable fields — don't claim editable music if there's none) →
"چطور بسازم؟" (انتخاب → ویرایش متن/رنگ → دانلود) → spec strip (aspects, duration, 2D/3D). Benefit before
feature; second person; every claim true of THIS template.
**5. Related set** (caller-computed; pick category/tags so this works). Query
`GET /v1/templates?categoryId=...` then `?tagSlug=...`; rank occasion > category+use-case > style > industry;
show 68; **exclude this template's own aspect siblings** (dedupe by template id); cap same-style clones;
label pack-mates «از همین مجموعه».
## fa / en parity & RTL rules
- Fill fa first, then en as a faithful mirror. Two self-consistent pages — never a Persian page with an
English title. Same template id; localized labels resolve per current locale.
- Persian uses Vazirmatn / RTL (see `../persian-fonts/SKILL.md`); use Persian digits in user-facing copy; don't fight the
`[dir="rtl"]` block. Tag/category *slugs* stay latin for stability; *display names* are localized.
- hreflang fa↔en + x-default is the right target even though the detail page doesn't emit it yet — flag the gap.
## Worked examples
**A. Nowruz 3D greeting (تبریک نوروز سه‌بعدی)**
- Category: `دعوت‌نامه و مناسبت / Invitation & Event`.
- Tags: occasion=نوروز/Nowruz · use-case=تبریک/greeting · style=لاکچری/luxury · 2D-3D=سه‌بعدی/3D ·
aspect=۹:۱۶,۱:۱,۱۶:۹ · color=طلایی/gold · audience=کسب‌وکار کوچک/small business.
- name (fa): «قالب تبریک نوروز سه‌بعدی | مناسبت و تبریک» · (en): "Nowruz 3D Greeting Template | Invitation & Event"
- description (fa): «کارت تبریک نوروزی سه‌بعدی و لاکچری؛ نام برند، متن تبریک و رنگ طلایی را در چند دقیقه شخصی‌سازی کن و ویدیوی آماده انتشار بساز.»
- keywords: `قالب تبریک نوروز, تبریک عید, نوروز سه بعدی, دانلود کارت تبریک, Nowruz greeting`
- slug: `nowruz-3d-greeting` · Related: other نوروز items first, then Invitation & Event + greeting.
**B. Instagram sale promo (پروموشن فروش ویژه اینستاگرام)**
- Category: `استوری و ریلز / Story & Reels` (output is a 9:16 vertical — format wins over "sale").
- Tags: use-case=فروش ویژه/flash sale · industry=فروشگاه آنلاین/e-shop · style=نئون/neon · aspect=عمودی ۹:۱۶/vertical ·
color=انرژیک/energetic · audience=فروشگاه/store · 2D.
- name (fa): «قالب استوری فروش ویژه | استوری و ریلز» · (en): "Flash-Sale Story Template | Story & Reels"
- description (fa): «قالب آماده استوری برای اعلام تخفیف و فروش ویژه؛ متن، قیمت، رنگ و لوگوی فروشگاهت را جایگزین کن و استوری حرفه‌ای ۹:۱۶ بساز.»
- keywords: `قالب استوری فروش, تخفیف اینستاگرام, ساخت استوری تبلیغاتی, دانلود قالب استوری, flash sale reels`
- slug: `instagram-flash-sale-story` · Related: same use-case (flash sale) across categories, then Story & Reels.
**C. Corporate logo reveal (لوگو موشن شرکتی)**
- Category: `اینترو و لوگو / Intro & Logo`.
- Tags: use-case=معرفی برند/brand intro · style=مینیمال/minimal · audience=شرکتی/corporate ·
aspect=۱۶:۹,۱:۱,۹:۱۶ · color=آبی/blue · 2D (or 3D only if it truly is).
- name (fa): «قالب لوگو موشن شرکتی | اینترو و لوگو» · (en): "Corporate Logo Reveal Template | Intro & Logo"
- description (fa): «اینترو حرفه‌ای برای نمایش لوگوی شرکت؛ لوگو، نام برند و رنگ سازمانی را وارد کن و یک اینترو تمیز و مینیمال بساز.»
- keywords: `لوگو موشن, اینترو لوگو, ساخت اینترو, لوگو موشن شرکتی, logo motion intro`
- slug: `corporate-logo-reveal` · Related: other Intro & Logo + brand-intro use-case.
## Where each value goes
- **Admin UI** (`TemplatesAdmin.tsx`): name→`name`, slug→`slug`, presentation copy→`description`,
SEO keywords→`keywords`, categories→`category_ids` (multi-select chips), tags→`tag_ids`, mode→`primary_mode`.
Saves via `/api/admin/resource/templates``/v1/templates`.
- **Categories/Tags** are managed on their own admin pages (`admin-resources.tsx``/v1/categories`, `/v1/tags`);
category-level `meta_title`/`meta_description`/`meta_keywords`/`bot_follow` live there — set them once per category.
- **Seeder** (`scripts/seed_remotion_templates.py`): currently SETS only `name/slug/description/image/demo*/
is_published/primary_mode/sort` and inserts ZERO `container_categories` / `container_tags` and NO `keywords`.
To seed SEO+taxonomy: add `keywords` to the `project_containers` INSERT, and add INSERTs into
`content.container_categories` (with `sort` = index) and `content.container_tags`. Otherwise each seeded
template ships with no keywords, no category, no tags — invisible to backend category/tag filters until an
admin opens it and assigns them.
## Final checklist
- [ ] Exactly ONE primary category, format-based, from the flat set → `category_ids` (primary at index 0).
- [ ] 612 tags, controlled fa↔en vocab, each true of the design, across facets; aspect is a tag → `tag_ids`.
- [ ] `name` carries the SEO title (keyword in first ~30 chars; no doubled brand).
- [ ] `description` = benefit→for-what→customize(real fields)→how-to→specs; doubles as the meta description.
- [ ] `keywords` = 1 primary + 23 secondary, loanword + Persian spelling (free text — commas are just an authoring convention, not parsed).
- [ ] `slug` = curated latin keyword slug, stable, never changed after publish.
- [ ] fa filled first, en a 1:1 mirror; Persian digits + RTL in copy.
- [ ] Related works via category/tag query; own aspect siblings excluded.
- [ ] Gaps flagged: no template `meta_*`, public mapper drops `keywords`/cover OG/category, no related table,
no hreflang on detail page — note these where relevant rather than pretending they're set.
@@ -0,0 +1,95 @@
---
name: kinetic-typography
description: How to build animated-text systems for FlatRender Remotion templates — word/line/char reveals, mask wipes, typewriter, scale-pops, highlight sweeps, text-on-path, and number counters — Persian/RTL-aware and reusable. Use whenever a template's hero, caption, quote, title, price, or any text is the thing that moves. Persian is the priority; split by WORD, never by character.
---
# Kinetic typography (animated text systems)
Type is a first-class motion element here, not a label. A masterpiece text shot is ~5 layers: the right split, eased per-unit timing, a hold sized to a real read, legibility over the background, and a single hero word. Amateurs stop at "the text fades in."
## The one rule
Every value is a pure function of `useCurrentFrame()`. **Never** `useFrame`, `Math.random()`, `Date.now()`, `setState`, or `useEffect`-driven motion — the headless renderer samples frames out of order. For "random" jitter use `rand(seed)` from `lib/anim.ts`. Drive timing off `useVideoConfig().fps`; define `const sec = (s: number) => Math.round(s * fps)` — never hardcode `30`.
## Persian / RTL — get this right first (it's an Iran-facing product)
- **Split by WORD, not character.** Persian script is connected/cursive — splitting on chars shatters letterforms and joins. Latin char-reveals are fine; Persian is word- or line-only. A safe split is `text.split(/\s+/).filter(Boolean)` — this **preserves ZWNJ** (نیم‌فاصله, ``) inside words like «می‌شود» because ZWNJ is not whitespace. Never `.split("")` or `.replace(//g, …)` on Persian.
- Every text node: `fontFamily: FONT` (Vazirmatn, from `lib/fonts.ts`), `direction: "rtl"`, align right or center. The existing `KineticQuote.tsx` hardcodes Georgia/serif + pixel sizes + no RTL — **do not copy that**; it's a Latin-only relic.
- Persian needs weight (headings 700900) and `lineHeight: 1.41.6`. Numerals: pick Persian (۱۲۳ via `toLocaleString('fa-IR')`) or Latin and stay consistent; prices/years are usually Persian digits. See `../persian-fonts/SKILL.md`.
- For RTL word reveals, the wrapping container does the ordering — keep `flexWrap: "wrap"` + `direction: "rtl"` and let words flow; don't manually reverse the array.
## Size & position from layout tokens, never pixels
Read `useLayout()` from `lib/aspect.ts`: `vmin(n)`, `unit`, `isWide/isSquare/isTall`. Hero type ≈ `vmin(80110)`, body ≈ `vmin(2840)`. Tune timing/scale per aspect — wider reads faster (tighter stagger), tall reads slower (looser). Add this `pick` helper to `Layout` (per R&D Tier-0) and use it:
```ts
const pick = <T,>(w: T, s: T, t: T) => (L.isWide ? w : L.isSquare ? s : t);
const stagger = pick(2, 3, 4); // frames between units
```
## Animation patterns (all driven by `frame - start`)
| Pattern | Recipe | Persian-safe? |
|---|---|---|
| **Word reveal** (default) | split words; per word `start = i*stagger`; `spring({frame: frame-start, fps})``translateY(vmin)` + `opacity` | ✅ word-split |
| **Line reveal** | wrap by line in `<Sequence>`s; each line springs up behind a `clip-path` edge | ✅ |
| **Char reveal / scatter** | split chars, per-char delay; rotate/scale in | ❌ Latin only |
| **Mask wipe** | `clipPath: inset(0 ${100-p}% 0 0)` (RTL: wipe from right → `0 0 0 ${100-p}%`); `p = interpolate(frame,[a,b],[0,100],{extrapolateRight:"clamp", easing: Easing.out(Easing.cubic)})` | ✅ |
| **Typewriter** | `text.slice(0, Math.floor(interpolate(frame,[a,b],[0, words.length])))` joined — **slice by WORD for Persian**, by char only for Latin; add a blinking caret `frame % sec(0.8) < sec(0.4)` | ✅ word-slice |
| **Scale-pop ("ta-da")** | `scale = spring({config:{damping:12,mass:0.6,stiffness:180}})` or `Easing.bezier(0.34,1.56,0.64,1)` overshoot→settle | ✅ |
| **Highlight sweep** | gradient bar/`background-clip:text` shifting `background-position` per frame, or an accent rect growing under a key word | ✅ |
| **Text-on-path** | SVG `<textPath href="#p">`; animate `startOffset` by frame — Latin/numeric only (RTL on a path is unreliable) | ❌ |
| **Number counter** | `Math.round(interpolate(frame,[a,b],[0, target],{extrapolateRight:"clamp", easing: Easing.out(Easing.cubic)}))` then `toLocaleString('fa-IR')` | ✅ (format fa) |
| **Variable-weight pulse** | Vazirmatn ships a variable axis: `fontVariationSettings: \`'wght' ${interpolate(frame,[a,b],[300,900])}\`` (needs the variable woff2 registered in `fonts.ts`) | ✅ |
### Reusable word-reveal component (the workhorse — Persian-correct, aspect-aware)
```tsx
const RevealText: React.FC<{ text: string; start: number; color: string }> = ({ text, start, color }) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const L = useLayout();
const pick = <T,>(w: T, s: T, t: T) => (L.isWide ? w : L.isSquare ? s : t);
const stagger = pick(2, 3, 4);
const words = text.split(/\s+/).filter(Boolean); // keeps ZWNJ
return (
<div style={{ display: "flex", flexWrap: "wrap", justifyContent: "center",
direction: "rtl", fontFamily: FONT, fontWeight: 800, fontSize: L.vmin(96),
lineHeight: 1.4, color, gap: `0 ${L.vmin(18)}px`, maxWidth: "86%",
textShadow: `0 ${L.vmin(2)}px ${L.vmin(20)}px rgba(0,0,0,.6)` }}>
{words.map((w, i) => {
const s = spring({ frame: frame - start - i * stagger, fps,
config: { damping: 16, mass: 0.7, stiffness: 120 } });
return (
<span key={i} style={{ display: "inline-block", opacity: s,
transform: `translateY(${interpolate(s, [0, 1], [L.vmin(28), 0])}px)` }}>
{w}
</span>
);
})}
</div>
);
};
```
Follow-through upgrade: give a trailing accent word a *looser* spring (`damping: 6`) so it settles last.
## Easing & spring (linear is the sound of an amateur)
- Entrances → **ease-out** default (`Easing.out(Easing.cubic)`); hero titles → `Easing.bezier(0.16,1,0.3,1)`. Exits → **ease-in, sharper than the entrance**. Snappy pop → back bezier `(0.34,1.56,0.64,1)`.
- `interpolate` for exact marks — **always `extrapolateLeft/Right: "clamp"`** (forgetting it is the #1 drift bug). `spring` for organic feel. Combine: `interpolate(spring(...), [0,1], [vmin(28), 0])`.
- Spring cheats: clean reveal `{damping:200,mass:0.5,stiffness:200}` · default pop `{damping:12,mass:0.6,stiffness:180}` · bouncy `{damping:8,mass:1,stiffness:120}` · trailing wobble `{damping:6,mass:1,stiffness:80}`.
## Timing budgets (@ whatever `fps` is)
Micro pop 814f · word stagger 24f · standard reveal 1828f · hero entrance 2840f · **hold = a comfortable read** (≥ `sec(0.7)` per text element before the next competes). Cut frames before adding them — over-animating reads as amateur. Anticipation: dip below start before launch (`interpolate(frame,[0,6,30],[0,-0.12,1])`).
## Legibility over busy / 3D / video backgrounds
- Scrim or `textShadow: 0 0 vmin(20) rgba(0,0,0,.7)`, or a semi-transparent panel behind text.
- Gradient text: `WebkitBackgroundClip: "text"`, transparent fill, plus a `drop-shadow` for edge separation.
- Colors come from `colorSchema` props (`accentColor/secondaryColor/backgroundColor/textColor` via `lib/branding.ts`) — pass user hex through `mixHex`/`hexToRgba` so a garish value doesn't break the look. Never hardcode `#fff`.
- Captions (TikTok/Reels/Shorts) = high-contrast white/yellow + black outline, lower-middle third, inside the tightest safe zone. See `../remotion-aspect-ratios/SKILL.md`.
## Checklist
- [ ] Persian text split by WORD; ZWNJ preserved; `direction:"rtl"` + `fontFamily: FONT`.
- [ ] All sizes via `vmin`/`unit`; timing/stagger via `pick(...)` per aspect — verified in 16:9, 1:1, 9:16.
- [ ] No linear easing; ≥1 overshoot-and-settle; staggered, not all on frame 0.
- [ ] Every `interpolate` clamps both ends; no `useFrame`/`random`/`Date.now`; `fps` not `30`.
- [ ] Numbers formatted (`fa-IR`) and consistent; counter eases out.
- [ ] Legible over the background (scrim/shadow); colors from props.
- [ ] A real hold sized to reading; longest Persian string doesn't overflow, shortest doesn't look empty.
- [ ] Re-render twice → identical pixels (deterministic).
Related: `../remotion-template-composition/SKILL.md`, `../persian-fonts/SKILL.md`, `../remotion-aspect-ratios/SKILL.md`, `../remotion-design-styles/SKILL.md`, `../remotion-svg-colors/SKILL.md`, `../remotion-sound-effects/SKILL.md`, `../remotion-music-picker/SKILL.md`, `../flatrender-template-seo/SKILL.md`.
@@ -0,0 +1,167 @@
---
name: motion-design-principles
description: The foundation motion-craft reference for FlatRender Remotion templates — easing curves and when to reach for each, timing & spacing, the 12 animation principles applied to Remotion, anticipation/overshoot/follow-through/settle, staggering & choreography, secondary motion, spring() vs interpolate(), and the blocking→timing→polish workflow. Use whenever animating ANY element in a template, reviewing motion quality, or deciding how something should enter, move, or leave. Read this BEFORE writing animation code.
---
# Motion design principles (the FlatRender craft floor)
Project: `services/remotion/` (Remotion 4 + `@remotion/three`, R3F v9, `gl="angle"`). Three aspects (16:9 / 1:1 / 9:16), Persian-first (Vazirmatn, RTL). Helpers: `src/lib/anim.ts` (`hexToRgba`, `mixHex`, `rand`), `src/lib/aspect.ts` (`useLayout``isWide/isSquare/isTall`, `vmin`, `unit`, `pick`), `src/lib/branding.ts` (`colorSchema`, `BRAND`), `src/lib/fonts.ts` (`FONT` = Vazirmatn), `src/lib/three-kit.tsx` (`StudioEnv/Lights/Floor/Effects`, `Confetti3D`).
**Linear motion is the sound of an amateur. Almost nothing in a FlatRender template should move at a constant rate.** This skill is the floor every template stands on.
## The one rule everything hangs on
A Remotion frame is **pure**: `frame → pixels`, sampled at an arbitrary `t` (the After Effects mental model — a keyframe graph read at time `t`). The renderer samples frames **out of order and in parallel**.
- Derive every value from `useCurrentFrame()`. If a value can't be, it doesn't belong in the render.
- **Never** `useFrame` (R3F), `Math.random()`, `Date.now()`, `setState`, or `useEffect`-driven motion. For "randomness" use `rand(seed)` from `anim.ts`.
- **Never hardcode 30fps.** `const { fps } = useVideoConfig(); const sec = (s: number) => Math.round(s * fps);`
## `spring()` vs `interpolate()` — pick deliberately
| | `interpolate()` | `spring()` |
|---|---|---|
| Who authors the curve | **you** (explicit easing) | **physics** (mass/damping/stiffness) |
| Reach for it when | a value must hit an exact mark on an exact frame — storyboard reveals, crossfades, value remaps, color/blur sweeps | organic entrances, pops, bounces, anything that should "feel" alive |
| The trap | forgetting `extrapolate*: "clamp"` → elements drift off-screen / opacity goes negative | trying to land a value on an exact frame |
**Always combine them** — spring drives the *feel* (0→1), interpolate *remaps* it to real px/units in the layout's own scale:
```tsx
const L = useLayout();
const p = spring({ frame: frame - start, fps, config: { mass: 0.6, damping: 12, stiffness: 180 } });
const y = interpolate(p, [0, 1], [L.vmin(80), 0]); // remap into layout units
const opacity = interpolate(p, [0, 1], [0, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp" });
```
### Spring config cheat-sheet
Lower `damping` = more overshoot · higher `mass` = heavier/slower · higher `stiffness` = faster snap.
| Feel | mass | damping | stiffness | Use for |
|---|--:|--:|--:|---|
| Snappy, no overshoot | 0.5 | 200 | 200 | Clean UI / logo reveals |
| **Natural pop (default)** | 0.6 | 12 | 180 | Cards, badges, icons |
| Bouncy / playful | 1 | 8 | 120 | Kids, birthday, mascots |
| Heavy / weighty | 2.5 | 26 | 90 | Big titles, 3D objects landing |
| Loose wobble (follow-through) | 1 | 6 | 80 | Secondary / trailing parts |
## Easing cheat-sheet (`import { Easing } from "remotion"`)
| Situation | Curve | Why |
|---|---|---|
| **Entrances (default)** | `Easing.out(Easing.cubic)` | things arrive and decelerate |
| Hero title entrance | `Easing.out(Easing.quint)` or `Easing.bezier(0.16, 1, 0.3, 1)` | dramatic deceleration |
| **Exits** | `Easing.in(Easing.cubic)`**always sharper than the entrance** | things leave faster than they arrive |
| A→B on-screen move / camera | `Easing.inOut(Easing.cubic)` | smooth both ends |
| "Ta-da" overshoot | `Easing.bezier(0.34, 1.56, 0.64, 1)` | snappy pop past target |
| Wind-up / anticipation | `Easing.bezier(0.36, 0, 0.66, -0.56)` | dips below before launch |
| **Linear ONLY** | `Easing.linear` | rotation, scroll, conveyor, marquee — mechanical continuous motion |
```tsx
const t = interpolate(frame, [start, start + 24], [0, 1],
{ extrapolateLeft: "clamp", extrapolateRight: "clamp", easing: Easing.out(Easing.cubic) });
```
## Timing & spacing (30fps baseline — but always derive with `sec()`)
Spacing (the easing) sets *feel*; timing (frame count) sets *weight & mood*. **Cut frames before you add them — amateurs over-animate.**
| Beat | Frames @30fps |
|---|---|
| Micro pop (icon, badge) | 814 |
| Standard reveal | 1828 |
| Hero entrance | 2840 |
| Scene transition | 1220 |
| Hold | a comfortable read of the text (size to the longest Persian string) |
Symptoms: robotic = linear spacing · floaty/late = timing too long · jittery = no hold between moves.
## The 12 principles → Remotion (the four in **bold** you reach for every shot)
| Principle | Remotion expression |
|---|---|
| Squash & stretch | `scaleX`/`scaleY` inversely around an impact frame, conserve volume (`sx = 1/sy`) |
| **Anticipation** | dip the value below its start before the main move |
| Staging | stagger reveals; dim/blur everything but the hero — one idea per beat |
| Straight-ahead vs pose-to-pose | `interpolate` between keyed frames vs per-frame formula (sim, e.g. `Confetti3D`) |
| **Follow-through & overlapping** | same trigger, **delayed per child** + a *looser* spring so parts settle later |
| **Slow in & slow out** | `Easing.bezier` / `spring()` — the single biggest quality lever |
| Arcs | drive `y` with `sin`/parabola while `x` moves linearly |
| Secondary action | a small `sin` bob/shimmer alongside the primary reveal |
| Timing | frame count + spring `mass`/`damping` = weight & mood |
| **Exaggeration / overshoot** | overshoot > 1.0, then settle to 1.0 |
| Solid drawing | `StudioLights` + reflective material + floor shadows (3D) |
| Appeal | choreography + `StudioEffects` (bloom/DOF/vignette) + good type |
## The four quality multipliers (concrete, reusable)
**Anticipation** — a small negative dip before launch:
```tsx
const scale = interpolate(frame, [start, start + 6, start + 30], [0, -0.12, 1],
{ extrapolateRight: "clamp", easing: Easing.bezier(0.36, 0, 0.66, -0.56) });
```
**Overshoot + settle** — reach past, then land. Ensure the curve *holds* the target (clamp) or it micro-drifts forever:
```tsx
const pop = interpolate(frame, [start, start + 18], [0, 1],
{ extrapolateRight: "clamp", easing: Easing.bezier(0.34, 1.56, 0.64, 1) });
// or: spring with low damping (config { mass: 0.6, damping: 10, stiffness: 170 })
```
**Follow-through** — drive children from the *same* trigger, delay each, looser spring so they settle after the parent. The biggest "feels professional" upgrade for grouped elements:
```tsx
function Child({ i, start }: { i: number; start: number }) {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const p = spring({ frame: frame - start - i * 4, fps, config: { mass: 1, damping: 6, stiffness: 80 } });
return <g style={{ transform: `translateY(${interpolate(p, [0, 1], [24, 0])}px)`, opacity: p }} />;
}
```
**Secondary motion** — never let a held element go dead. Add a tiny `sin` breathe/shimmer:
```tsx
const bob = Math.sin(frame / fps * Math.PI) * L.vmin(4); // gentle float during the hold
```
## Staggering & choreography
Default to a **cascade**, and **tune the stagger per aspect** — wider frames read faster (tighter stagger), tall frames read slower (looser):
```tsx
const L = useLayout();
const stagger = L.pick(/*wide*/ 3, /*square*/ 4, /*tall*/ 5); // pick(wide, square, tall)
const start = i * stagger;
```
Patterns: **cascade** (lists/features) · **center-out** (logo/hero rows: `delay = Math.abs(i - mid) * stagger`) · **deterministic random** (particles: `rand(i)` for delay/offset) · **beat-synced** (snap `start` to music beat frames — see `../remotion-music-picker/SKILL.md`). **One thing enters the eye at a time.**
> `pick` is the standard per-aspect selector on `useLayout()`. If it isn't on `Layout` yet, add it in `aspect.ts`: `pick: <T,>(wide: T, square: T, tall: T): T => kind === "wide" ? wide : kind === "tall" ? tall : square,`
## 3D motion (`@remotion/three`)
Drive every transform off `useCurrentFrame()` (deterministic under ANGLE) — **never `useFrame`**. Rotation/orbit = `linear` (mechanical); entrances/landings = `spring` with **high mass** for weight. Keep crisp Persian text as a 2D `<AbsoluteFill>` overlay above `<ThreeCanvas>`. Let `StudioEffects` (bloom + DOF + vignette) carry the cinematic polish in one component; tune `camera.fov`/`position.z` per aspect so the subject fills the frame.
## The pro workflow — 5 passes, IN ORDER
Polishing before timing is locked wastes the most time.
1. **Reference** — decide the feel before code; pick style (`../remotion-design-styles/SKILL.md`), type (`../persian-fonts/SKILL.md`), composition (`../remotion-template-composition/SKILL.md`), per-aspect rules (`../remotion-aspect-ratios/SKILL.md`). Write the beat list ("logo in → tagline → 3 features cascade → CTA → out").
2. **Blocking** — every element at its final position with crude `interpolate` fades, no easing. Fix off-screen/cropping in all three aspects NOW.
3. **Timing** — lock frame counts, stagger, beats, holds, transitions. Watch at full speed repeatedly. Mood lives here.
4. **Polish** — swap linear for easing/springs; add anticipation + overshoot/settle, follow-through, secondary motion, arcs, squash/stretch; `StudioEffects` for 3D; wire SFX (`../remotion-sound-effects/SKILL.md`) + music sync (`../remotion-music-picker/SKILL.md`) to the locked frames.
5. **Review** — scrub frame-by-frame + full speed against the checklist below.
## Top amateur mistakes → fixes (review gate)
- Linear motion → ease/spring · no anticipation/overshoot → dip-then-launch / back bezier
- Everything on one frame → stagger · forgot `clamp` → clamp both ends
- Hardcoded 30fps → `useVideoConfig().fps` + `sec()`
- `useFrame`/`random`/`Date.now()``useCurrentFrame` + `rand`
- Pixel-hardcoded sizes → `vmin`/`unit` + `pick`/`isWide/isSquare/isTall`
- Over-animating → one idea per beat · no hold → real hold sized to reading
- Exit speed = entrance speed → exits sharper · dead holds → `sin` bob/breathe/shimmer
- Color hardcoded → read from `colorSchema` props
## Pre-ship motion checklist
- [ ] No linear easing anywhere except mechanical continuous motion (rotation/marquee).
- [ ] Entrances ease-out; exits ease-in **and sharper** than entrances.
- [ ] Every `interpolate` that could overshoot has `extrapolateLeft/Right: "clamp"`.
- [ ] At least one anticipation (dip) and one overshoot-and-settle in the piece.
- [ ] Grouped elements stagger; trailing parts follow through (looser spring).
- [ ] No dead holds — held heroes have a subtle `sin` breathe/shimmer.
- [ ] Stagger/scale tuned per aspect via `pick`; verified in 16:9 / 1:1 / 9:16.
- [ ] All timing from `sec()`/`fps`; no hardcoded 30; no `useFrame`/`random`/`Date.now`.
- [ ] One clear hero moment with the biggest motion; the eye always knows where to look.
- [ ] Re-render twice → pixel-identical (deterministic).
Related: `../remotion-design-styles/SKILL.md`, `../remotion-aspect-ratios/SKILL.md`, `../remotion-template-composition/SKILL.md`, `../remotion-character-design/SKILL.md`, `../remotion-sound-effects/SKILL.md`, `../remotion-music-picker/SKILL.md`, `../persian-fonts/SKILL.md`, `../flatrender-template-seo/SKILL.md`.
@@ -0,0 +1,120 @@
---
name: particles-and-effects
description: How to add production-value FX — confetti, sparkles, bokeh, light leaks, dust, smoke, glow, lens flare, film grain, chromatic aberration, vignette, camera shake — to FlatRender Remotion templates, in both 2D (SVG/CSS) and 3D (@remotion/three). Use when a template needs atmosphere, finishing texture, particle systems, or a celebratory/cinematic hit. Every effect is a deterministic function of useCurrentFrame() — never Math.random.
---
# Particles & effects for Remotion
Project: `services/remotion/` (Remotion 4 + `@remotion/three`, R3F v9, `gl="angle"`). Effects are the **8th finishing layer** — the thing that separates "made in a tool" from "made by a studio." A flat, ungrainy, perfectly-locked frame reads as AI/template. Imperfect-by-design wins.
## The one non-negotiable rule
Render is headless Chrome sampling frames out of order, in parallel. **Every particle position, every grain offset, every flicker MUST derive from `useCurrentFrame()`.** Never `Math.random()`, `Date.now()`, `useFrame` (R3F), `useState`, or `useEffect` motion. Use `rand(seed)` from `src/lib/anim.ts` for stable per-index pseudo-randomness, and `rand(i + frame)`-style offsets when you want it to *move*. Re-render twice → identical bytes, or it's wrong.
Helpers you build on:
- `anim.ts``rand(i)` (deterministic 0..1), `hexToRgba(hex,a)`, `mixHex(a,b,t)`.
- `aspect.ts``useLayout()``isWide/isSquare/isTall`, `vmin(n)`, `unit`, and `pick(wide,square,tall)`. **Scale particle COUNT and SIZE per aspect** — a tall 9:16 needs fewer, bigger sparkles than a wide 16:9.
- `branding.ts``colorSchema` props are `accentColor / secondaryColor / backgroundColor / textColor`. FX color comes from these so the studio recolors them.
- `three-kit.tsx``StudioEnv`, `StudioLights`, `StudioFloor`, `StudioEffects` (bloom+DOF+vignette), `Confetti3D`.
## 2D vs 3D — pick per effect
- **2D (SVG/CSS)** is the default: cheap, crisp, no WebGL. Confetti, sparkles, grain, light leaks, vignette, aberration, camera shake — all better/cheaper in 2D as an `<AbsoluteFill>` overlay, even on top of a 3D scene.
- **3D (@remotion/three)** when the effect must respond to scene lighting/depth: volumetric bloom, real bokeh/DOF, `emissive` glow that bloom picks up, 3D confetti with perspective. Let `StudioEffects` do bloom/DOF/vignette in ONE component — don't re-roll them.
- Persian text NEVER goes in 3D — keep it as a 2D overlay above `<ThreeCanvas>`.
## Effect → recipe table
| Effect | Layer | Core technique | Determinism |
|---|---|---|---|
| **Confetti (2D)** | overlay | N `<rect>`/`<path>`, `rand(i)` for x/rot/color; `y` = `(frame*speed + rand(i)*span) % span` | `rand(i)` seed |
| **Confetti (3D)** | scene | reuse `Confetti3D` from three-kit | built-in |
| **Sparkles / shine** | overlay | 4-point star SVG, twinkle `opacity = abs(sin((frame+rand(i)*60)/12))`, scale pulse | `rand(i)` |
| **Bokeh** | bg | big blurred radial-gradient circles drifting on `sin(frame/period)`, low opacity, `mix-blend:screen` | per-circle seed |
| **Light leaks** | overlay | warm radial/linear gradient sweeping across via `interpolate(frame,...)` translate, `mix-blend:screen` | frame |
| **Dust motes** | overlay | tiny dim dots, slow upward drift + lateral `sin`, `rand` size/speed | `rand(i)` |
| **Smoke / fog** | bg/3D | 2D: layered blurred blobs drifting+scaling; 3D: stacked transparent planes | frame |
| **Glow** | any | 2D `filter:drop-shadow(0 0 Npx accent)` / `textShadow`; 3D `emissive`+`emissiveIntensity`, `toneMapped={false}`, let bloom bloom it | static |
| **Lens flare** | overlay | bright core + chromatic ring sprites along a line from a light point, opacity by angle/frame | frame |
| **Film grain** | top | SVG `feTurbulence` with per-frame `seed`, `mix-blend:overlay`, low opacity — MUST animate or it looks frozen | frame |
| **Chromatic aberration** | top | duplicate layer, offset R/B channels ±13px, strongest at impact frames | frame |
| **Vignette** | top | `boxShadow: inset 0 0 vmin(600) rgba(0,0,0,.6)` or `StudioEffects` in 3D | static |
| **Camera shake** | root | translate whole frame by `rand(frame)`-driven jitter, decaying after an impact | `rand(frame)` |
## Deterministic particle field (the pattern to memorize)
```tsx
const frame = useCurrentFrame();
const { vmin, pick } = useLayout();
const count = pick(60, 48, 36); // fewer on tall
{Array.from({ length: count }).map((_, i) => {
const x = rand(i) * 100; // % of width
const drift = Math.sin((frame + rand(i + 9) * 200) / 40) * 3;
const fall = (frame * (0.3 + rand(i + 1) * 0.5) + rand(i + 5) * 120) % 120;
const twinkle = Math.abs(Math.sin((frame + rand(i + 2) * 60) / 12));
return <div key={i} style={{
position: "absolute", left: `${x + drift}%`, top: `${fall - 10}%`,
width: vmin(6), height: vmin(6), opacity: twinkle,
background: i % 2 ? accentColor : secondaryColor,
transform: `rotate(${frame * 2 + rand(i) * 360}deg)`,
}} />;
})}
```
Notice: `rand(i)` = stable identity per particle; `frame` = motion; `% span` = seamless wrap; aspect drives count via `pick`.
## Animated film grain (SVG — the cheapest authenticity layer)
```tsx
<svg style={{ position: "absolute", inset: 0, mixBlendMode: "overlay", opacity: 0.08 }}>
<filter id="grain">
<feTurbulence type="fractalNoise" baseFrequency="0.9"
numOctaves="2" seed={frame % 100} stitchTiles="stitch" />
</filter>
<rect width="100%" height="100%" filter="url(#grain)" />
</svg>
```
`seed={frame % 100}` is what makes it crawl. Keep opacity 0.050.12. For paper/vignette use `mix-blend:multiply` instead.
## Chromatic aberration & impact-driven FX
Aberration should be **strongest at impacts** (a hard cut, the hero reveal, a confetti burst) and near-zero otherwise:
```tsx
const ab = interpolate(frame, [hit - 2, hit, hit + 8], [0, vmin(4), 0], { extrapolateLeft: "clamp", extrapolateRight: "clamp" });
```
Render the content twice, offset the red copy `translateX(-ab)` `mix-blend:screen` and the blue copy `translateX(+ab)`. Same `interpolate` curve also drives a one-shot camera-shake amplitude — things calm down fast.
## Camera shake (subtle continuous + impact)
```tsx
// continuous "frame alive" drift — tiny, always on
const driftX = Math.sin(frame / 50) * vmin(3) + (rand(frame) - 0.5) * vmin(1);
// impact shake — decays
const amp = interpolate(frame, [hit, hit + 12], [vmin(14), 0], { extrapolateLeft: "clamp", extrapolateRight: "clamp" });
const shake = (rand(frame * 7) - 0.5) * amp;
// apply to a root <AbsoluteFill style={{ transform: `translate(${driftX+shake}px, ${...}px)` }}>
```
A locked, perfectly-still frame reads amateur. A *tiny* always-on drift makes it feel hand-held and alive — keep it under ~`vmin(4)` or it's distracting.
## 3D glow & bloom
Make a material glow into bloom: `<meshStandardMaterial emissive={accentColor} emissiveIntensity={2} toneMapped={false} />`, then mount `<StudioEffects bloom={0.9} />`. For sparkly metal confetti raise `metalness`. Drive every transform off `useCurrentFrame()` (deterministic under ANGLE), rotation = `linear` (mechanical), entrances = `spring` with high mass.
## Reusable components — make these, don't inline
Put shared FX in `src/lib/fx.tsx` so every template gets the same texture:
- `<GrainOverlay opacity? blend? />` — animated `feTurbulence`.
- `<Vignette strength? />` — inset boxShadow.
- `<Confetti2D colors count? burstFrame? />` — burst (spring spread) vs rain (continuous fall) modes.
- `<Sparkles colors count? area? />` — twinkling 4-point stars.
- `<Bokeh colors count? />` + `<LightLeak color from to />` — bg/overlay atmosphere.
- `<Aberration amount /> <CameraShake amount />` — finishing pair, wrap the whole comp.
Each takes `colorSchema` colors so the studio picker recolors the FX, and reads `useLayout()` for per-aspect count/size.
## Restraint — FX amplify a hero, they are not the show
- One celebratory burst on the **hero moment**, not raining the whole video. Often **silence before** + confetti + sparkle SFX on the same frame (see `../remotion-sound-effects/SKILL.md`).
- Finishing texture (grain, vignette, drift) is *subtle and always-on*; spectacle (confetti, flare, big aberration) is *brief and on a beat*.
- Don't stack 6 effects at full strength — that reads as a tool preset. Grain at 0.08, vignette at 0.5, aberration only at impacts.
- All FX color from `colorSchema`; pass a user's garish hex through `mixHex(hex, background, 0.2)` so it doesn't blow out.
## Pre-ship checklist
- [ ] Zero `Math.random` / `Date.now` / `useFrame` — only `rand()` + `frame`. Re-render twice → identical.
- [ ] Grain is *animated* (per-frame seed), not frozen.
- [ ] Particle count & size scale per aspect via `pick`/`vmin` — verified in 16:9, 1:1, 9:16; particles stay in the safe zone, never crop Persian text.
- [ ] Every `interpolate` has `extrapolateLeft/Right: "clamp"` — no drift, no negative opacity.
- [ ] Spectacle FX land on a beat / the hero; texture FX are subtle & continuous.
- [ ] FX colors read from `colorSchema`; a continuous camera drift keeps the frame alive.
- [ ] 3D glow uses `emissive`+`toneMapped={false}` + `StudioEffects` (not hand-rolled bloom).
Related: `../remotion-design-styles/SKILL.md`, `../remotion-character-design/SKILL.md`, `../remotion-aspect-ratios/SKILL.md`, `../remotion-template-composition/SKILL.md`, `../remotion-sound-effects/SKILL.md`, `../remotion-music-picker/SKILL.md`, `../remotion-svg-colors/SKILL.md`, `../persian-fonts/SKILL.md`, `../remotion-template-catalog/SKILL.md`, `../flatrender-template-seo/SKILL.md`.
@@ -0,0 +1,55 @@
---
name: persian-fonts
description: Persian (Farsi) and Latin font selection for FlatRender — which font to use where, with a Persian-first priority. Use when choosing typefaces for a template or UI, pairing display vs body fonts, handling RTL/Persian numerals, or adding a new font to the Remotion project. Persian fonts are the priority.
---
# Persian-first typography
This is an Iran-facing product: **Persian (Farsi) is the default and the priority**. Latin is secondary. Get the Persian type right first.
## What the project already uses
- **Vazirmatn** — the default Persian face everywhere. Remotion bundles `services/remotion/public/fonts/vazirmatn-{400,600,700,800,900}.woff2` and exposes `FONT` from `src/lib/fonts.ts` (use `fontFamily: FONT` + `direction: "rtl"` in every composition).
- Web app: `globals.css` has a `[dir="rtl"]` block that FORCES Vazirmatn on all elements — don't fight it with utility classes; work with it.
- Latin pairing in the web app: **Plus Jakarta Sans** + **Inter**.
## Persian font palette (pick by role)
| Font | Character | Use for |
|---|---|---|
| **Vazirmatn** | clean, neutral, many weights | DEFAULT — body, UI, most template text |
| **Estedad** | modern, geometric, friendly | headings, modern brand templates |
| **Yekan Bakh** | contemporary sans, professional | corporate/business templates |
| **Shabnam / Sahel** | soft, readable | body text, calm/elegant designs |
| **IRANSans / IRANYekan** | familiar Iranian UI standard | UI-style promos, app mockups |
| **Morabba** | bold display, rounded | big punchy headlines, posters |
| **Gandom** | strong display | impactful titles, sale/sport |
| **Lalezar** | playful, heavy, fun | kids, party, birthday, casual |
| **Shekari / Vahid** | decorative/calligraphic | festive, traditional (Nowruz, wedding, Yalda) |
| **Nastaliq (e.g. IranNastaliq)** | classical calligraphy | very formal/traditional, invitations — use sparingly, hard to read small |
Pairing rule: one display face for the headline + Vazirmatn (or Shabnam) for everything else. Don't mix two display faces.
## Match font to template mood
- Corporate / SaaS → Estedad / Yekan Bakh + Vazirmatn.
- Festive (birthday, party) → Lalezar / Morabba + Vazirmatn.
- Traditional / occasion (Nowruz, wedding, Yalda, Eid) → a decorative/Nastaliq display for the greeting + Vazirmatn for details.
- Sale / bold promo → Gandom / Morabba (heavy) + Vazirmatn.
- Minimal / elegant → Shabnam / Sahel, lighter weights.
## Persian typesetting rules
- **RTL:** always `direction: "rtl"`; align right or center. Mixed Persian+Latin/numbers needs care (bidi) — test the actual string.
- **Numerals:** decide Persian (۱۲۳) vs Latin (123) and be consistent. For Persian digits, format with `toLocaleString('fa-IR')` or use a font that supports Persian numerals. Years/prices in templates are usually Persian digits (۱۴۰۶, ۲۹۹٬۰۰۰).
- **Weights:** Persian script needs a bit more weight to feel solid — headings 700-900, body 400-600. Avoid ultra-thin for small text.
- **Line-height:** Persian needs slightly more (`lineHeight` 1.4-1.6 for body) — descenders/diacritics need room.
- **ZWNJ (نیم‌فاصله, ``):** preserve it in words like «می‌شود», «نیم‌فاصله» — don't strip it.
- **No fake bold/italic:** use real weights; Persian has no italic — don't slant it.
## Adding a new font to a Remotion template
1. Get the woff2 (license-checked — Vazirmatn/Estedad/Shabnam are SIL OFL, free for commercial; verify others). Place in `services/remotion/public/fonts/`.
2. Register it (a `@font-face` injected via `<style>` in the composition, or `@remotion/fonts` `loadFont`) and expose a `FONT_X` const next to `FONT` in `lib/fonts.ts`.
3. Use `fontFamily: FONT_X` only for that template's display text; keep body on Vazirmatn.
4. Embed the actual weights you use (don't ship 9 weights if you use 2) to keep bundles small.
## Licensing
Prefer SIL OFL / free-for-commercial Persian fonts (Vazirmatn, Estedad, Shabnam, Sahel, Samim, Gandom, Morabba — most from the `font-store`/Google Persian sets are OFL). Verify each before shipping in an exported-video product; keep a license record.
Related: `../remotion-template-composition/SKILL.md`, `../remotion-design-styles/SKILL.md`.
@@ -0,0 +1,244 @@
# FlatRender Design & Motion R&D — Trends + Professional Craft
> Single-source R&D brief for the FlatRender Remotion engine (`services/remotion`). Stack: **Remotion 4 + `@remotion/three`** (R3F v9, `gl="angle"`), Persian-first (Vazirmatn, RTL), three mandatory aspects (16:9 / 1:1 / 9:16), color-customizable templates driven by `colorSchema` props. Operating context: **Iran** — geo-blocked CDNs, sanctioned SaaS dashboards, reachable Nexus mirror (`mirror.soroushasadi.com`). Render is headless Chrome in Docker, so **every value must be a pure function of `useCurrentFrame()`** and **every asset must be vendored** into `public/`.
>
> Existing grounding files: `src/lib/anim.ts`, `aspect.ts`, `branding.ts`, `fonts.ts`, `three-kit.tsx`, `kit.tsx`; templates in `src/templates.tsx`; `public/` currently holds only `fonts/`.
---
## The two meta-truths to keep over everything
1. **Imperfect-by-design beats glossy.** As feeds fill with AI-perfect imagery, deliberate imperfection — grain, texture, hand-rendered type, natural-feeling motion — now signals "a real human made this." Even fully-rendered templates win by *adding back* texture and human-feeling motion. Apply this lens to every trend below.
2. **A masterpiece is ~8 finishing layers, not one big thing.** Sound design, micro-easing, a design system, depth/lighting, color grade, pacing, a clear hero moment, and subtle texture. Amateurs stop at "the text animates in." We must finish all eight.
---
## 1) Design trends to adopt (each with a concrete how-to in our stack)
Every trend below survives all three aspects only if you anchor to safe-zone **percentages / `layout.vmin()`**, never absolute pixels. Read `../remotion-aspect-ratios/SKILL.md` before positioning anything.
### Typography (type is a first-class motion element, not a label)
| Trend | When to use | How in our stack |
|---|---|---|
| **Bold / oversized hero type** (fills 6090% of frame, clipped by edges) | Logo reveals, promo hooks; strongest on 9:16 | `fitText` from `@remotion/layout-utils` to auto-scale a word to frame width; animate `scale`/`translateY` with `spring()`; parent `overflow:hidden` to clip. |
| **Variable-font animation** (`wght`/`wdth`/`slnt` over time) | Premium beat-synced intros | `style={{ fontVariationSettings: \`'wght' ${interpolate(frame,[0,30],[100,900])}\` }}`. **Vazirmatn ships a variable build** — animate its weight axis for Persian hero type. Load via `@font-face` (vendored in `public/fonts/`, never Google CDN at render). |
| **Kinetic typography** (word-by-word / line-by-line) | Quotes, captions, fast hooks | Split into spans; per-word `delay = i * staggerFrames`; drive each with `spring({frame: frame - delay, fps})`; combine `translateY` + `opacity` + slight `rotate`. `<Sequence>` per line for timeline clarity. |
| **Anti-AI / hand-rendered / scribbled** | Grunge/street/youth/music/events | Pre-make rough-edged SVG/PNG lettering; "draw" it on with a `clipPath`/mask wipe; add `filter:url(#displace)` (SVG `feTurbulence` + `feDisplacementMap`) with a per-frame jitter for photocopy wobble. |
| **Chrome / Y2K metallic type** | Hype, music, fashion, "premium" reveals | CSS: layered `linear-gradient` text fill via `background-clip:text` cycling silver→steel→highlight by shifting `background-position` per frame. For real reflections, use Three.js (see liquid-chrome below). |
### 3D / Blender-look (`@remotion/three`)
- **Real-time 3D logo reveals** — the default "premium intro." Render `<ThreeCanvas>`, drive camera/object rotation from `useCurrentFrame()` (**never `useFrame`**). Extrude logo via `TextGeometry`/extruded SVG shape, `meshStandardMaterial`, an HDRI `Environment`, `spring()`-driven entrance. Use our `StudioEnv/StudioLights/StudioFloor/StudioEffects` from `three-kit.tsx`.
- **"Plushcore" / soft-3D / claymation** — friendly counter to hard chrome. Pre-render GLTF in Blender (subsurface/soft shaders), import via `useGLTF`; bobbing idle = `Math.sin(frame/fps)` on position. 2D fake-3D = big soft inner-shadows + highlight gradients in CSS.
- **Mixed 2D/3D** — one of the strongest 2026 looks. Layer a `<ThreeCanvas>` behind/within absolutely-positioned 2D Remotion layers (SVG strokes, flat shapes); composite with blend modes.
### Surface & color treatments
- **Grain / texture / noise** — near-universal in 2026; add to almost everything, especially flat/gradient backgrounds. Cheap: tiling noise PNG overlay at low opacity with `mix-blend-mode: overlay`/`soft-light`. **Animated grain must move or it looks frozen** — offset `background-position` per frame, or SVG `feTurbulence` with a per-frame `seed`/`baseFrequency` jitter.
- **Mesh gradients** — soft multi-point blends (not linear), the sophisticated 2026 background. Pre-bake a mesh PNG and slowly drift/scale it, or animate live with a fragment shader in Three.js driven by `frame`. **Always add grain on top.**
- **Glassmorphism (evolved)** — used *selectively* (cards, lower-thirds), multi-layer depth, dynamic blur. `backdrop-filter: blur(16px) saturate(140%)`, semi-transparent `rgba` fill, 1px light top-border, soft shadow; animate blur radius + a moving specular highlight per frame; stack 23 panels at different parallax depths.
- **Retro / Y2K** — chrome + iridescent mesh gradient + occasional pixelate (`image-rendering:pixelated` on a downscaled layer) + sparkle SVG bursts. Music/fashion/Gen-Z/party greetings.
- **Anti-design / tactile brutalism** — hard `1px solid #000` borders, no radius, pure-saturated bg, system/monospace fonts, deliberate overlap. Motion is blunt — **hard cuts and `step`/`linear` snaps, not smooth springs.**
- **Mixed-media / collage** — PNG cutouts with rough edges as layers; "paper-drop" overshoot springs on slight rotation/scale; tape/staple SVGs, paper-grain overlay, occasional 1-frame jump-cut jitter.
- **Isometric** — `transform: rotateX(60deg) rotateZ(-45deg)` on stacked layers with `transform-style:preserve-3d`; stagger layer entrances; move elements along iso axes (matched X/Y deltas). Great for SaaS/feature explainers.
- **Kinetic / liquid / morph** — morphing blobs (animate SVG `path d` with `flubber`; gooey via `feGaussianBlur` + `feColorMatrix`). **Liquid chrome (top-5 2026 animation trend):** Three.js `meshStandardMaterial` `metalness:1, roughness:~0.05` + HDRI `Environment` on a morphing/`MeshDistort`-style geometry — color shifts as the camera moves.
- **AI-aesthetic vs anti-AI** — AI stills/clips as `<Img>`/`<OffthreadVideo>` backgrounds with Ken-Burns for surreal promos; lean into hand-type + grain + collage for trust/authenticity brands.
### Color direction 2026
- **Dopamine / electric accents** (electric blue, neon coral, acid yellow, vivid teal, cobalt) — as high-energy *accents* to guide the eye, never whole palettes.
- **Tech pastels** (lavender haze, powder blue, digital pink) — calm/mature, great for SaaS/UI promos.
- **Warm earth neutrals** (mocha/espresso/caramel/tan) — "quiet luxury," premium and human.
- **Strategy:** bold dopamine accent + neutral/pastel base + grain on top. **Avoid all-flat saturated fields — they read as AI/template.** All color comes from `colorSchema` props so the studio can recolor; pass a user's hex through a grade so a garish value doesn't break the look.
---
## 2) Animating anything (craft + the pro workflow)
### The one rule everything hangs on
A frame in Remotion is **pure**: `frame → pixels`. Motion is *evaluated* at an arbitrary frame, exactly the After Effects mental model (a keyframe graph sampled at time `t`). **Never** use `useFrame`, `Math.random()`, `Date.now()`, `setState`, or `useEffect`-driven motion — the renderer samples frames out of order and in parallel. Use `rand(seed)` from `anim.ts` for deterministic "randomness." If a value can't be derived from `frame`, it doesn't belong in the render.
### The 12 principles → Remotion (the four you reach for every shot in bold)
| Principle | Remotion expression |
|---|---|
| Squash & stretch | `scaleX`/`scaleY` inversely around an impact frame (conserve volume: `sx = 1/sy`) |
| **Anticipation** | Dip the value below its start before the main move |
| Staging | Stagger reveals; dim/blur everything but the hero |
| Straight-ahead vs pose-to-pose | `interpolate` between frames (keyed) vs per-frame formula (sim, e.g. `Confetti3D`) |
| **Follow-through & overlapping** | Same motion, **delayed per child** + a *looser* spring so parts settle later |
| **Slow in & slow out** | `Easing.bezier` / `spring()` — the single biggest quality lever |
| Arcs | Drive `y` with a `sin`/parabola while `x` moves linearly |
| Secondary action | A small `sin` bob/shimmer alongside the primary reveal |
| Timing | Frame count + spring `mass`/`damping` = weight & mood |
| **Exaggeration / overshoot** | Overshoot > 1.0, then settle to 1.0 |
| Solid drawing | `StudioLights` + reflective material + shadows (3D) |
| Appeal | Choreography + `StudioEffects` (bloom/DOF/vignette) + good type |
### `spring()` vs `interpolate()`
- **`interpolate`** — *you* author the curve. Use when a value must hit an exact mark on an exact frame (storyboard reveals, crossfades, value remaps). **Always set `extrapolateLeft/Right: "clamp"`** — forgetting this is the #1 cause of elements drifting off-screen or opacity going negative.
- **`spring`** — *physics* authors the curve. Use for organic entrances, pops, bounces.
- **Combine:** spring drives the *feel*, interpolate *remaps* its 0→1 output to real px/units: `const y = interpolate(spring(...), [0,1], [vmin(80), 0])`.
**Spring config cheat-sheet:**
| Feel | mass | damping | stiffness | Use for |
|---|--:|--:|--:|---|
| Snappy, no overshoot | 0.5 | 200 | 200 | Clean UI/logo reveals |
| Natural pop (slight overshoot) | 0.6 | 12 | 180 | **Default** cards/badges/icons |
| Bouncy / playful | 1 | 8 | 120 | Kids, birthday, mascots |
| Heavy / weighty | 2.5 | 26 | 90 | Big titles, 3D objects landing |
| Loose wobble (follow-through) | 1 | 6 | 80 | Secondary / trailing parts |
Lower damping = more overshoot; higher mass = heavier/slower; higher stiffness = faster snap.
### Easing cheat-sheet (linear is the sound of an amateur)
- **Entrances → ease-out** (`Easing.out(Easing.cubic)`), your default. Hero titles → `Easing.out(Easing.quint)` or `Easing.bezier(0.16,1,0.3,1)`.
- **Exits → ease-in** (`Easing.in(Easing.cubic)`), and **always sharper than the entrance** — things leave faster than they arrive.
- **A→B on-screen moves / camera → ease-in-out.**
- **Snappy "ta-da" → back/overshoot** `Easing.bezier(0.34,1.56,0.64,1)`.
- **Wind-up → anticipate** `Easing.bezier(0.36,0,0.66,-0.56)`.
- **Linear ONLY for mechanical continuous motion** — rotation, scroll, conveyor, marquee.
### Timing & frame budgets (30fps default — but use `sec(s)=Math.round(s*fps)`, never hardcode 30)
Micro pop 814f · standard reveal 1828f · hero entrance 2840f · scene transition 1220f · hold = a comfortable read of the text. **Cut frames before you add them — amateurs over-animate.** Robotic = linear spacing; floaty/late = timing too long.
### The four quality multipliers (concrete patterns)
- **Anticipation:** `interpolate(frame,[0,6,30],[0,-0.12,1])` — small negative dip before launch.
- **Overshoot+settle:** the back bezier, or a low-damping spring. Ensure the curve *reaches and holds* the target (clamp) or it micro-drifts forever.
- **Follow-through:** drive children from the same trigger frame, **delay each** (`frame - start - i*stagger`) with a *looser* spring so they settle after the parent. Biggest "feels professional" upgrade for grouped elements.
- **Staggering / choreography:** default to a **cascade**; tune per aspect via a `pick(wide,square,tall)` helper (wider reads faster → tighter stagger; tall reads slower → looser). Patterns: cascade (lists), center-out (logos/hero rows), random-but-deterministic via `rand(i)` (particles), beat-synced (snap `start` to music beats). **One thing enters the eye at a time** — staging.
### 3D motion
Drive every transform off `useCurrentFrame()` (deterministic under ANGLE). Rotation/orbit = `linear` (mechanical); entrances/landings = `spring` with high mass for weight. Let `StudioEffects` (bloom + DOF + vignette) do the cinematic polish in one component.
### The pro workflow (5 passes, IN ORDER — polishing before timing is locked wastes the most time)
1. **Reference** — decide the feel before code; pull an AE template/Dribbble loop; pick style (`../remotion-design-styles/SKILL.md`), type (`../persian-fonts/SKILL.md`), composition (`../remotion-template-composition/SKILL.md`), per-aspect rules (`../remotion-aspect-ratios/SKILL.md`); write the beat list ("logo in → tagline → 3 features cascade → CTA → out").
2. **Blocking** — every element on screen at final position with crude `interpolate` fades, no easing. Fix off-screen/cropping in all three aspects now.
3. **Timing** — lock frame counts, stagger, beats, holds, transitions. Watch at full speed repeatedly. Mood lives here.
4. **Polish** — swap linear for easing/springs; add anticipation+overshoot+settle, follow-through, secondary motion, arcs, squash/stretch; `StudioEffects` for 3D; wire SFX (`../remotion-sound-effects/SKILL.md`) and music sync (`../remotion-music-picker/SKILL.md`) to the locked frames.
5. **Review** — scrub frame-by-frame + full speed. Nothing pops without an ease; nothing leaves slower than it arrived; the eye always knows where to look; reads in all three aspects; Persian RTL not clipped; colors from `colorSchema`; re-render twice → identical (deterministic). Then run `../flatrender-template-seo/SKILL.md`.
### Top amateur mistakes → fixes (review gate)
Linear motion → ease/spring · no anticipation/overshoot → dip-then-launch / back bezier · everything on one frame → stagger · forgot `clamp` → clamp both ends · hardcoded 30fps → `useVideoConfig().fps` · `useFrame`/`random`/`Date.now` → `useCurrentFrame` + `rand` · pixel-hardcoded sizes → `vmin`/`unit` + branch on `isWide/isSquare/isTall` · over-animating → one idea per beat · no hold → real hold sized to reading · exit = entrance speed → exits sharper · dead holds → `sin` bob/breathe/shimmer · flat 3D lighting → `StudioLights` + floor shadows + `StudioEffects` · color hardcoded → read from props.
---
## 3) Asset pipeline (collecting + designing footage, Iran-aware)
### The Iron Rule
The Iran environment punishes runtime dependencies. **All assets are vendored** — download once (over VPN if needed), commit into `services/remotion/public/`, reference with `staticFile()`. **Never** `<Video src="https://somecdn..." />` in a template — a geo-block or flaky tunnel kills the render mid-frame. Mirror npm/NuGet/Docker via Nexus; asset *binaries* are sourced manually and committed. **Track licenses at acquisition time, not later.**
### License taxonomy (know cold)
- **Ship freely, no attribution:** CC0 / Public Domain, Pixabay License, Pexels License, Unsplash License. **Default target.**
- **Allowed with a credit:** CC-BY (track per-asset; needs attribution UI/end-card).
- **Off-limits:** CC-BY-SA (share-alike can "infect" our proprietary template), CC-BY-NC (we are a paid product = commercial), editorial/rights-managed.
- **Paid stock (Envato/Adobe/Shutterstock):** allowed per license; keep the receipt/license PDF. Note their dashboards/checkout often geo-block Iran at the account/payment layer even with a VPN — use a foreign-established account or a partner.
### Sourcing (CC0 / no-attribution first)
- **Footage:** Pexels Videos (first stop), Pixabay Video, Mixkit, Coverr (check AI badge), Free Nature Stock, Videvo (filter to CC0). Pick exact resolution (don't ship 4K into a 1080p comp), prefer **H.264 MP4** for `<OffthreadVideo>`, commit under `public/footage/`.
- **Images:** Pexels, Pixabay, Unsplash, StockSnap, Burst. For Persian/Iran imagery search English terms ("Tehran", "Iranian food") + self-shot/local stock.
- **Textures & overlays:** Poly Haven Textures (CC0), ambientCG (CC0); grain/light-leak/dust = CC0 video clips you screen-blend.
- **HDRIs:** Poly Haven (1k2k for render speed), ambientCG.
- **3D (GLB):** Poly Haven Models (CC0, cleanest), Kenney.nl (CC0 low-poly), Khronos glTF samples, Sketchfab (**mixed — check each**, filter Downloadable + CC0/CC-BY). **Prefer GLB over glTF+separate-textures** (one file, no broken paths); Draco-compress with `gltf-pipeline -b`; keep low-poly for headless render speed.
- **Icons (bundle via Nexus npm, never CDN):** Lucide, Tabler, Heroicons, Phosphor, Iconoir. **Illustrations (recolorable SVG):** unDraw, Open Peeps, Humaaans — ship as SVG so the studio color picker recolors them (`../remotion-svg-colors/SKILL.md`).
**Iran access:** generally reachable — Pixabay, Mixkit, Coverr, Poly Haven, ambientCG, npm via Nexus. VPN-recommended/intermittent — Pexels, Unsplash, Sketchfab, GitHub raw. Sanction-blocked at account/payment — Adobe Stock, Envato. **Mitigation: batch all sourcing in one "asset run" over a stable tunnel, commit binaries, render never touches the open internet again.**
### AI-generated assets — when it's right
- **Use when:** the asset doesn't exist as stock (specific Persian cultural scene, branded mascot), you need consistency across a template set (reference-image controls), or it's faster than a 5-site license hunt.
- **Don't use when:** clean CC0 stock already exists, you need photographic authenticity, or the **free-tier commercial license is unclear** (many free tiers forbid commercial use / watermark — a legal landmine for a paid product).
- **Iran-pragmatic recommendation: locally-hosted open models** — **HunyuanVideo 1.5** (self-hosted, ~RTX 4090, no geo-block/payment/watermark, full commercial control) for video; **FLUX/SDXL** locally for image/texture/illustration. Use hosted SaaS (Runway Gen-4, Kling 3.0) only when local quality is insufficient and a VPN+foreign-account path exists. **Always record prompt + tool + plan-tier + date** in the asset's sidecar.
### Designing & preparing footage in Remotion
- **Media primitives:** `<OffthreadVideo>` = **default for all video in a render** (FFmpeg frame extraction, deterministic, no seek drift). `<Video>` = preview only. `<Img>` over raw `<img>` (waits for load → no half-loaded frames). `staticFile()` for every vendored asset; never an external URL in a shipped template.
- **Color grading:** per-layer CSS `filter` + blend modes (`contrast/saturate/brightness/hue-rotate`). Build a shared `lib/grades.ts` preset set (`warm`, `teal-orange`, `mono`, `filmic`) so all templates grade consistently and the palette can drive `hue-rotate`/`saturate`. Heavy grading → pre-grade in DaVinci Resolve (free) before committing.
- **Masking / keying:** no native keyer — pre-key in Resolve/AE and export **alpha** (ProRes 4444 or WebM/VP9 alpha), then `<OffthreadVideo>` it. Shape/gradient masks via CSS `maskImage`/`clipPath` or SVG `<mask>`.
- **Seamless loops:** source loop-designed clips (Coverr/Mixkit) or crossfade-to-self with overlapping `<Sequence>`s; mirror-pingpong for imperfect footage. `<OffthreadVideo loop>` once first/last frames match.
- **Overlays (the cheap "authentic" layer):** stack grayscale-on-black/white clips — **screen** for light-leaks/bokeh/dust, **overlay/soft-light** for grain, **multiply** for vignettes/paper. Keep palette-independent.
- **Ken-Burns:** `interpolate` scale (start ≥ 1 overscan so no edge reveals) + translate; ease with `spring`/bezier; `objectFit:cover` + center-safe framing so all three aspects crop cleanly.
- **Performance (headless Docker):** right-sized media, H.264 + `<OffthreadVideo>`, 1k2k HDRIs, Draco GLB; raise `concurrency` carefully and watch RAM.
### Library structure + attribution firewall
Create under `services/remotion/public/` (today only `fonts/`): `footage/{nature,business,abstract}/`, `overlays/`, `images/`, `textures/`, `hdri/`, `models/`, `icons/`, `illustrations/`, plus **`assets.json` + `ASSETS.md`**. Lowercase-kebab names, no spaces.
**`assets.json` — one row per asset, added at download time** (`file, source, url, author, license, attribution_required, commercial_ok, acquired, notes`). Conventions: every asset gets a row (no row = "unknown license = do not ship"); `attribution_required:true` must surface its credit on a shippable surface; sidecar `.license.txt` for AI prompts / paid receipts. **CI validation script** asserts every file in the media folders has a matching row with `commercial_ok:true`, else fails the build — this is the license firewall. `ASSETS.md` is a generated readable table for humans/legal. If the repo bloats, move large media to Git LFS or MinIO (already in stack) with a `predeploy` sync into `public/` — but present at render time.
---
## 4) Masterpiece production + platform playbook
### The 8 production-value layers (what separates "made in a tool" from "made by a studio")
1. **Sound design + beat-sync** — the fastest "professional" tell. Beat-sync every key reveal (map BPM, keyframe on beat boundaries — hero on a downbeat); layered SFX (whoosh on transitions, thump on hard cuts, sparkle on shine sweeps, riser into the hero, pop on icon entrances); **ducking** (music dips under VO/key sound); **silence before the hero reveal** makes the payoff hit harder.
2. **Micro-detail** — easing never linear; overshoot & settle; staggered 25f entrances; secondary motion (shadow/contents react); anticipation.
3. **Design system** — one type scale (45 sizes), one spacing rhythm, constrained palette (1 primary + 12 accents + neutrals), consistent radii/strokes/elevation; Persian-first type handled deliberately (Vazirmatn + matched Latin pairing, not one font stretched across scripts).
4. **Depth & lighting** — layered parallax (bg/mid/fg different speeds), soft directional shadows with one consistent light direction, atmospheric depth (bg blur/desaturate, fg sharp/saturated), rim light on hero.
5. **Color grade** — one unified grade over the whole comp (not per-element colors fighting); lifted/tinted shadows, controlled highlights, deliberate temperature; user hex still passes through the grade.
6. **Pacing / rhythm** — vary cut length, build to a climax, match cut rhythm to music, trim ruthlessly.
7. **A clear hero moment** — one designated peak (logo/price-drop/product/CTA) with the biggest motion, strongest hit, often silence before, most screen real-estate. Flat = nothing lands.
8. **Finishing texture (subtle!)** — low-opacity film grain, gentle vignette, 12px chromatic aberration strongest at impacts, tiny continuous camera drift (frame alive, not locked), sparing light-leaks/bokeh, motion blur on fast elements (its absence is a classic amateur tell).
### Pre-ship polish checklist (if you can't tick it, it's not done)
- **Motion:** no linear easing anywhere; staggered entrances; motion blur on fast elements; ≥1 overshoot-and-settle; nothing pops on/off without a transition.
- **Audio:** BPM mapped, reveals on beat; whoosh on every scene change; accent SFX on hero; music ducks under VO, no clipping, clean end.
- **Composition/design system:** verified in 16:9 / 1:1 / 9:16 (not a letterboxed 16:9); text in platform safe zones; consistent scale/spacing/radii/shadows; constrained palette; FA + EN both correct (RTL, font, numerals).
- **Depth & grade:** consistent light direction; bg depth treatment; unifying grade over the comp.
- **Pacing & hero:** one unmistakable hero; varied cuts matched to music; engaging first frame (it's the thumbnail).
- **Finishing:** subtle grain OR vignette present; frame alive; aberration/light-leak at transitions if style allows.
- **Technical/QA:** clean render at target res (no flicker/z-fighting/font fallback); all editable fields (text/logo/image/colors) swap without breaking layout; longest text doesn't overflow, shortest doesn't look empty; loops cleanly if meant to.
### Platform playbook (2026)
All vertical = **1080×1920, 9:16**. **First frame = the hook = the cover.** High-contrast captions (white/yellow, black outline) in the lower-middle third are the cross-platform default.
| Platform | Length sweet spot | Hook / retention | Safe zone (1080×1920) | Template implication |
|---|---|---|---|---|
| **IG Reels** | 715s punchy; 3090s for depth (up to 90s) | First 23s decide stay/swipe; cleaner/less-cluttered text than TikTok; rewards 3-sec view rate + completion | ~108 top, ~320 bottom, ~60 L, ~120 R; hook text Y≈200600 | Cleaner kinetic type, mesh-gradient + glass lower-thirds, refined transitions |
| **IG Story** | full-bleed | heavy UI chrome | avoid top ~250 (profile) + bottom ~250 (reply/link) | design poll/quiz/link sticker zones into layout |
| **IG Feed post** | — | first caption line is the hook | — | Portrait **4:5 (1080×1350)** standard; 1:1 for grid consistency |
| **TikTok** | 1530s engagement; 1118s virality; ≤60s educational | **3-second rule**; curiosity-gap / bold-claim hooks; word-by-word captions beat full sentences | keep right ~120, bottom ~320 clear of key content | calm neutral grain+warm-earth variant; **word-by-word captions as a first-class editable layer** |
| **YT Shorts** | 1535s | **no runway** — open on the most compelling moment; intro retention >70%, completion >60% (<30s) | center within middle ~1080×1350; clear bottom UI + right buttons | cinematic 3D logo reveals, graded looks |
| **YT long-form intro** | — | cold-open hook in first 515s; branded sting <3s | — | state payoff first, brand second |
| **YT end screen** | last 520s | — | leave clean plate (lower + right) for subscribe/next/playlist | reserve an end-card-safe zone |
| **All three** | drifting to **6090s** | authenticity > perfection, phone-feel > studio, natural light, cinematic grading | — | hook in first 12s; grain/texture everywhere; support longer durations; safe-zone all text |
**Cross-platform synthesis rules:** (1) design to the *tightest* safe zone (Story/TikTok), then it's safe everywhere; (2) first frame = the hook = the cover; (3) front-load the payoff, no preamble; (4) captions are a first-class editable layer (word-by-word), not an afterthought; (5) one template, **three real aspects** — re-flow, never letterbox.
---
## 5) Prioritized "level up our skills + templates" action list
Ordered by ROI. Each item ties to our stack and the relevant skill file.
**Tier 0 — foundation infra (do first; unblocks everything else)**
1. **Establish the asset library + license firewall.** Create the `public/{footage,overlays,images,textures,hdri,models,illustrations}` tree, `assets.json` + `ASSETS.md`, and the **CI validation script** (`commercial_ok` + matching-row check). Do one batched VPN "asset run" of a CC0 starter pack (grain/light-leak/dust overlays, 34 mesh-gradient PNGs, a few Poly Haven HDRIs + GLBs, business/nature/abstract footage). *(Asset pipeline §3.)*
2. **Promote shared helpers into `lib/`.** Add `pick(wide,square,tall)` onto `Layout` in `aspect.ts` (currently only `isWide/isSquare/isTall/vmin/unit`); create `lib/grades.ts` (warm/teal-orange/mono/filmic + palette-driven `hue-rotate/saturate`); confirm `rand`/`hexToRgba`/`mixHex` in `anim.ts` cover deterministic needs. *(Animation §2, Asset §3.)*
3. **Stand up a local AI-asset box** (HunyuanVideo 1.5 + FLUX/SDXL) so bespoke Persian/branded assets don't depend on geo-blocked SaaS. *(Asset §3.)*
**Tier 1 — highest-ROI template work**
4. **Build a "captions" engine as a reusable first-class layer** — word-by-word kinetic captions, high-contrast white/yellow + black outline, lower-middle-third, safe-zoned for all platforms, beat-syncable. This is currently an afterthought and is the biggest cross-platform win. *(Masterpiece §4, Animation §2.)*
5. **Ship the "kinetic oversized type + grain" template** — every aspect, cheap (CSS), uses variable Vazirmatn weight-animation for Persian hero type. Highest ROI per the trends brief. *(Trends §1, Animation §2.)*
6. **Codify the pre-ship polish checklist + 8 layers into a review gate** (extend `../flatrender-template-seo/SKILL.md`'s publish step, or a new lint pass) so no template ships without easing, beat-synced audio, three-aspect verification, and a hero moment. *(Masterpiece §4.)*
7. **Sound-design pass on the existing pack** — wire `../remotion-music-picker/SKILL.md` BPM mapping + `../remotion-sound-effects/SKILL.md` placement + ducking into our current templates. Fastest "professional" upgrade to what already exists. *(Masterpiece §4.)*
**Tier 2 — premium differentiation**
8. **3D logo-reveal template + a liquid-chrome variant** on `@remotion/three` + HDRI (`three-kit.tsx` `StudioEnv/Lights/Floor/Effects`), all motion off `useCurrentFrame()`. Premium tier. *(Trends §1, Animation §2.)*
9. **Mesh-gradient + glass lower-third promo** — clean modern, IG-friendly, palette-driven, grain on top. *(Trends §1.)*
10. **Grunge / collage / anti-AI pack** — rides the authenticity wave for youth/music; uses hand-type, paper overlays, deterministic jitter. *(Trends §1.)*
**Tier 3 — craft & process maturity**
11. **Adopt the 5-pass workflow (reference → blocking → timing → polish → review) as the team norm**, and seed a reference library (AE templates / Dribbble loops) per template type. *(Animation §2.)*
12. **Per-aspect tuning audit** of existing templates — stagger + scale via `pick`, re-flow not letterbox, confirm Persian RTL never clips. *(Animation §2, `../remotion-aspect-ratios/SKILL.md`.)*
13. **Color system discipline** — enforce dopamine-accent-+-neutral/pastel-base + grain-overlay defaults; run user hex through the grade so no garish value breaks a look. *(Trends §1, Masterpiece §4.)*
---
## Sources
**Trends:** [Envato — Motion Trends 2026](https://elements.envato.com/learn/motion-design-trends) · [MonkyVision](https://monkyvision.com/blog/motion-design-trends/) · [GraphicDesignJunction](https://graphicdesignjunction.com/2026/01/video-and-motion-creative-trends-2026/) · [Krumzi](https://www.krumzi.com/blog/12-graphic-design-trends-shaping-2026-and-how-ai-is-changing-the-game) · [It's Nice That](https://www.itsnicethat.com/features/forward-thinking-graphic-trends-2026-graphic-design-120126) · [Fontfabric](https://www.fontfabric.com/blog/10-design-trends-shaping-the-visual-typographic-landscape-in-2026/) · [Kittl](https://www.kittl.com/blogs/graphic-design-trends-2026/) · [StudioMeyer](https://studiomeyer.io/en/blog/webdesign-trends-2026) · [Envato — 3D Trends](https://elements.envato.com/learn/3d-design-trends) · [Patata School](https://www.patataschool.com/blender-typography-in-motion) · [Lummi — Animation Trends](https://www.lummi.ai/blog/animation-trends-2026) · [Fireart — Tactile Brutalism](https://fireart.studio/blog/the-best-web-design-trends/) · [AND Academy — Color](https://www.andacademy.com/resources/blog/graphic-design/color-trends-for-designers/) · [Gelato — Colors](https://www.gelato.com/blog/trending-colors) · [Adobe Express — Color of Year](https://www.adobe.com/express/learn/blog/color-of-year-trends) · [ALM Corp — Short-form](https://almcorp.com/blog/short-form-video-mastery-tiktok-reels-youtube-shorts-2026/) · [ShortSync](https://www.shortsync.app/resources/short-form-video-trends-2026) · [Sprout Social](https://sproutsocial.com/insights/social-media-trends/) · [FrameFlow](https://frameflowedit.com/article/top-5-video-editing-trends-in-2026)
**Assets:** [Colorlib — Stock Video](https://colorlib.com/wp/best-free-stock-video-sites/) · [FreeConvert](https://www.freeconvert.com/blog/best-stock-video-sites/) · [Moonb](https://www.moonb.io/blog/best-stock-video-sites) · [awesome-cc0](https://github.com/madjin/awesome-cc0) · [Poly Haven](https://polyhaven.com/) · [Poly Haven License](https://polyhaven.com/license) · [Khronos glTF Samples](https://github.com/khronosgroup/gltf-sample-models) · [11 Free 3D Asset Sites](https://dev.to/markyu/11-free-3d-asset-sites-for-games-blender-and-webgl-ah2) · [Iran censorship (Wikipedia)](https://en.wikipedia.org/wiki/Internet_censorship_in_Iran) · [Iran tiered internet (Rest of World)](https://restofworld.org/2026/iran-blackout-tiered-internet/) · [Tech sanctions sheet](https://docs.google.com/spreadsheets/d/1b9tetXkMg4PB_XyWcsC_UWGv45MX3pZmasnDBhyQxlY/edit) · [Blocked in Iran](https://www.irun2iran.com/websites-and-social-media-blocked-in-iran/) · [AI Video 2026](https://aiunpacking.com/guides/ai-video-generation-sora-runway-kling-veo/) · [Best AI Video (PixVerse)](https://pixverse.ai/en/blog/best-ai-video-generators) · [Best AI Video (Pixflow)](https://pixflow.net/blog/best-ai-video-generator/)
**Masterpiece + platform:** [IG Safe Zone (Outfy)](https://www.outfy.com/blog/instagram-safe-zone/) · [Reels Safe Zones (TryMyPost)](https://www.trymypost.com/blog/instagram-reels-safe-zones-text-placement-2026) · [IG Reel Size (InVideo)](https://invideo.io/blog/instagram-reel-size-guide/) · [IG Story Dimensions (AdMake)](https://admakeai.com/blog/instagram-story-dimensions-2026) · [TikTok Length (Go-Viral)](https://www.go-viral.app/blog/tiktok-video-length/) · [TikTok 3-Second Rule (2Point)](https://www.2pointagency.com/glossary/tiktok-creative-best-practices-the-3-second-rule/) · [TikTok Hooks (Selfstorming)](https://www.selfstorming.com/guides/social-media-hooks/tiktok-video-hooks) · [TikTok Captions (Blitzcut)](https://blitzcutai.com/blog/best-caption-style-tiktok) · [Shorts Length (OpusClip)](https://www.opus.pro/blog/ideal-youtube-shorts-length-format-retention) · [Shorts Best Practices (Miraflow)](https://miraflow.ai/blog/youtube-shorts-best-practices-2026-complete-guide) · [Shorts Safe Zone (Kreatli)](https://kreatli.com/guides/youtube-shorts-safe-zone) · [Post Production (Balance Studio)](https://www.balancestudio.tv/blog/color-grading-sound-mixing-motion-graphics-what-youre-really-paying-for-in-post-production) · [Sound Design in Motion (GUVI)](https://www.guvi.in/blog/sound-design-in-motion-graphics/)
**Stack files referenced:** `D:\Projects\Flatrender2\services\remotion\src\lib\{anim,aspect,branding,fonts}.ts`, `three-kit.tsx`, `kit.tsx` · `src\templates.tsx` · asset root to create: `services\remotion\public\` (currently only `fonts\`) · manifest to create: `services\remotion\public\{assets.json,ASSETS.md}` · suggested: `src\lib\grades.ts`.
@@ -0,0 +1,91 @@
---
name: remotion-aspect-ratios
description: How to design ONE Remotion template that genuinely fits all three FlatRender aspects — 16:9, 1:1, 9:16 — without text cropping, off-screen elements, or a layout that is really just the 16:9 version letterboxed. Use whenever building or reviewing a template's layout. Read this BEFORE positioning any text or element.
---
# Designing for 16:9 / 1:1 / 9:16 (do this right)
Every template registers in all three aspects (`ASPECTS` in `src/lib/aspect.ts`). A common mistake (made in early FlatRender templates) is to design for 16:9 and just let the same coordinates render in 9:16 — which **crops text, pushes elements off-screen, and looks broken**. A template must be *re-laid-out* per aspect, not scaled.
## Two strategies — responsive component OR per-aspect components
There are TWO legitimate ways to support the three aspects; pick per template:
1. **One responsive component** (default) — a single composition that adapts via `useLayout()` (`isWide/isSquare/isTall`, `pick()`). Use when the design is fundamentally the same and only positions/sizes change. Less code, stays in sync.
2. **A dedicated component per aspect** — when the design must differ STRUCTURALLY (different layout, different scene, different element set), not just reposition. e.g. a cinematic wide hero vs a stacked vertical story vs a centered square badge can be genuinely different scenes.
The registry supports both. In `services/remotion/src/templates.tsx` a `TemplateDef` has `component` (shared default) plus an optional `componentsByAspect` map keyed by aspect id:
```tsx
{
id: "MyTemplate",
component: MyTemplateWide, // fallback for any aspect not overridden
componentsByAspect: {
"1x1": MyTemplateSquare, // dedicated square design
"9x16": MyTemplateTall, // dedicated vertical design
},
schema, durationSec, defaultProps, // SHARED across aspects — keep the editable
} // fields, props and duration identical
```
`Root.tsx` picks `componentsByAspect[aspectId] ?? component`. **Keep `schema`, `defaultProps`, and `durationSec` shared** so the studio shows the same editable fields and the same composition ids (`${id}-${aspect}`) regardless — only the visual layout differs. Reuse shared sub-components (background, characters, text overlay) across the per-aspect files so they don't drift.
Guideline: start with one responsive component; split into per-aspect components only when responsive branching gets gnarly or the designs truly diverge. Don't duplicate three files when `pick()` would do.
## Why the naive approach breaks
`useLayout().vmin(n)` sizes off the SHORT side (1080 in all three aspects), so a `vmin(92)` font is the same pixel size everywhere. But the WIDTH differs hugely: **1920px (16:9) vs 1080px (9:16)**. A headline that fits 1920 wide overflows/crops at 1080 wide. Likewise positioning at `width*0.34` puts an element in a totally different place relative to its own size when width changes.
## The rules
1. **Design 9:16 (tall) first.** It's the tightest. If it fits there, widening to 1:1 and 16:9 is easy. Building 16:9-first guarantees the tall version breaks.
2. **Branch layout on `L.isWide / L.isSquare / L.isTall`** — don't just scale. Things that sit side-by-side in 16:9 should STACK vertically in 9:16:
```tsx
const L = useLayout();
// hero element position differs per aspect
const heroX = L.isTall ? L.width * 0.5 : L.width * 0.34; // centered in tall, left in wide
// layout direction
flexDirection: L.isTall ? "column" : "row"
```
Add a tiny helper to `aspect.ts` and use it everywhere:
```ts
pick: <T,>(wide: T, square: T, tall: T): T =>
kind === "wide" ? wide : kind === "tall" ? tall : square,
```
Then: `fontSize: L.pick(L.vmin(92), L.vmin(84), L.vmin(72))`.
3. **Cap font size to the WIDTH, not just the short side.** Headlines must wrap, never crop. Always set `maxWidth` and let text wrap:
```tsx
maxWidth: L.width * 0.86, // safe text column
// and scale type DOWN in tall:
fontSize: L.pick(L.vmin(90), L.vmin(80), L.vmin(64)),
wordBreak: "normal", lineHeight: 1.15,
```
Test with the LONGEST realistic Persian string for that field, not the short default.
4. **Respect SAFE ZONES.** Keep all meaningful content inside the central safe area; give tall more vertical margin:
- 16:9: ~5% horizontal / 8% vertical padding.
- 9:16: ~8% horizontal, and keep the hero in the middle 60% vertically (top/bottom of phones get UI chrome).
Anchor text blocks to a zone (top third / bottom third), put the hero visual in the center.
5. **Reposition the hero per aspect.** A character/object that's at `x=34%` and text on the right in 16:9 should become hero-centered with text above/below in 1:1 and 9:16. Use `pick()` for x/y and for `justifyContent`/`alignItems`.
6. **Scale element COUNT/spread, not just size.** A row of 5 floating shapes that spans 1920 looks sparse/clipped at 1080 — reduce spread radius or count in tall (`L.pick(...)`).
7. **3D:** adjust `camera.fov` / `position.z` per aspect so the subject fills the frame (a tall frame needs the camera pulled back or a narrower fov). Keep the 2D text overlay using the same `pick()` rules.
## Mandatory verification (the step that was skipped before)
Render a still in **all three aspects** at a frame where text is visible, with a LONG test string, and LOOK at each:
```
npx remotion still src/index.ts "<Comp>-16x9" out/_a.png --frame=NN
npx remotion still src/index.ts "<Comp>-1x1" out/_b.png --frame=NN
npx remotion still src/index.ts "<Comp>-9x16" out/_c.png --frame=NN
```
Reject if: text is clipped at any edge, an element is off-frame, the hero is tiny/huge, or the tall version is obviously "the wide one squished".
## Checklist
- [ ] Designed tall-first; used `pick()`/`isTall` to branch layout (not just scale).
- [ ] Headlines wrap with `maxWidth`; tested with long Persian text — no cropping.
- [ ] Hero repositioned/centered per aspect; content in safe zones.
- [ ] Spread/count adjusted for narrow frames; 3D fov/camera tuned per aspect.
- [ ] Eyeballed stills in ALL THREE aspects.
Related: `../remotion-template-composition/SKILL.md`, `../remotion-design-styles/SKILL.md`.
@@ -0,0 +1,51 @@
---
name: remotion-character-design
description: How to build and animate 2D (SVG) and 3D (@remotion/three) characters and mascots for FlatRender Remotion templates. Use when a template needs a character — a mascot, Haji Firuz, animals (goldfish, butterflies), people, or any articulated figure. Covers construction from primitives, rigging via grouped transforms, animation cycles (walk/dance/idle), facial expression, and the 2D-vs-3D / GLTF trade-offs.
---
# Character design for Remotion
We have NO rigged 3D model assets and the asset CDNs are geo-blocked, so characters are built from **primitives** — SVG shapes (2D) or Three.js geometries (3D) — and animated off `useCurrentFrame()`. Reference implementations: `NowruzGreeting.tsx` (2D Haji Firuz, goldfish, butterflies) and `Nowruz3D.tsx` (3D scene).
## Core principle: build in parts, rig with groups
A character = a tree of grouped parts, each animated by transforming its group around a PIVOT.
- 2D: nested `<g transform="translate/rotate/scale">`. Put the pivot at the joint (e.g. shoulder) by translating the group there, drawing the limb from origin, and rotating the group.
- 3D: nested `<group position rotation>`. Same idea — position the group at the joint, model the limb from there, rotate the group.
## 2D characters (SVG)
Construction kit:
- Head = `<circle>`; body = `<path>` trapezoid (`M.. Q..`); limbs = `<rect rx>` (rounded) or `<path>`; hands/feet = small ellipses; hat = `<path>` triangle/cone.
- Face (friendly/stylized): two dot eyes, a `Q` curve smile, rosy `circle` cheeks at low opacity. Keep it simple — over-detailing reads worse, not better.
- Skin/clothing: flat fills + one darker shade for a soft AO at edges (`fillOpacity` overlay).
Animating cycles (all from `frame`):
- **Idle/breathe:** body `scale.y = 1 + 0.02*sin(frame/30)`.
- **Dance/bounce:** whole body `translateY = -abs(sin(frame/7))*H`, plus a small `rotate(sin(frame/7)*4)` sway; legs counter-rotate.
- **Limb swing:** `rotate(amp*sin(frame/period))` around the joint pivot.
- **Hop-in entrance:** `spring()` from off-frame X to the target, then switch to the idle/dance loop.
- **Prop shake (tambourine, flag):** `rotate(sin(frame/3.2)*18)` + sparkle accents on peaks.
## 3D characters (primitives)
Build the figure from: `sphereGeometry` (head/joints), `cylinderGeometry`/`capsule` (limbs/torso), `coneGeometry` (hat/skirt), `RoundedBox` (blocky bodies), `torusGeometry` (rings/mouth). Group per limb for articulation. Material: `meshStandardMaterial` (roughness ~0.5 for cloth, lower for shiny). Light with `three-kit` StudioLights; add a soft contact shadow (a dark blurred plane or `shadows` + a floor).
- 3D animation is the SAME math (rotate/translate groups by `frame`), just in 3D space and you can also move the camera/scene for parallax.
- Faces in 3D are hard — keep them simple (sphere eyes, a small torus/curve mouth) or face the character slightly away.
## Animation principles (what makes it not look stiff)
- **Anticipation:** dip before a jump, wind-up before a throw.
- **Squash & stretch:** scale on impacts/landings (subtle: ±8%).
- **Overlap / secondary motion:** hat ball, scarf, string, ears, tambourine lag behind the body — offset their phase.
- **Easing:** `Easing.out(Easing.cubic)` for arrivals, `spring()` for bouncy pops, never linear for organic motion.
- **Hold:** let a pose read for a beat before the next move.
## Cultural / brand-safety notes
- Default to a **modern stylized** look (friendly, non-realistic) unless the user asks otherwise. Specifically for Haji Firuz: red costume + conical hat + tambourine, but a friendly non-blackface face (avoids the blackface connotation).
- Confirm a storyboard with the user BEFORE building complex characters (the project convention) — list the cast, the beats, and the look.
## When NOT to hand-build
- Photoreal humans or complex rigged motion → out of scope without GLTF assets; propose a stylized take or a non-character design instead.
- If a GLTF model IS provided, load with drei `useGLTF` and animate transforms by `frame` (no `useFrame`).
## Workflow
Storyboard → build parts → rig groups → animate cycles → **render stills at 3-4 beats and LOOK** → refine → render all 3 aspects (see `../remotion-aspect-ratios/SKILL.md` — characters must sit in the safe zone in 9:16 too).
Related: `../remotion-design-styles/SKILL.md`, `../remotion-aspect-ratios/SKILL.md`, `../remotion-template-catalog/SKILL.md`.
@@ -0,0 +1,49 @@
---
name: remotion-design-styles
description: Visual art-direction reference for building FlatRender video templates with Remotion (2D) and Three.js/@remotion/three (3D). Use when starting a new template, picking an art style, designing color palettes, or designing objects/scenes. Covers flat, gradient/mesh, glassmorphism, neon, 3D/cinematic, paper-cut, isometric, luxury looks plus color theory, lighting, and material design.
---
# Remotion design styles (2D + 3D)
Project location: `services/remotion/`. Shared helpers: `src/lib/anim.ts` (hexToRgba, mixHex, rand), `src/lib/branding.ts` (colorSchema + BRAND), `src/lib/aspect.ts` (useLayout), `src/lib/three-kit.tsx` (3D studio kit). Animate everything off `useCurrentFrame()` — never `Math.random()`/`Date.now()` (breaks determinism; use `rand(i)`), and in 3D never use R3F's `useFrame` (use `useCurrentFrame`).
## Pick a style first, then build
Each template should commit to ONE art style — mixing reads as "basic". Catalog:
| Style | Look | How (Remotion) | Best for |
|---|---|---|---|
| Flat / minimal | solid fills, generous whitespace, 1-2 accents | SVG shapes, simple springs | corporate, clean promos |
| Gradient / mesh | soft drifting color blobs | blurred radial-gradient divs animated by frame (see GradientPromo) | modern SaaS, backgrounds |
| Glassmorphism | frosted translucent cards | `backdrop-filter: blur`, rgba fills, thin borders | UI/app promos |
| Neon / glow | dark bg + luminous strokes | `drop-shadow`/`textShadow` glows, emissive in 3D | gaming, nightlife, tech |
| 3D / cinematic | real depth, reflections, bokeh | @remotion/three + three-kit (lighting, MeshReflectorMaterial, bloom/DOF) | premium logo/product reveals |
| Paper-cut / layered | stacked shapes with soft shadows | layered SVG + offset box-shadows | storytelling, kids, greetings |
| Isometric | 2.5D objects on a grid | SVG with skew/`rotateX` CSS, or true 3D ortho camera | explainer, product, city scenes |
| Luxury / gold | dark + metallic gold + serif | gold gradients, shine sweeps, slow easing | weddings, premium brands |
## 2D vs 3D — choose deliberately
- **2D (SVG/CSS):** fast to render, crisp text, full control, no WebGL. Use for flat/gradient/glass/neon/character scenes, anything text-heavy.
- **3D (@remotion/three):** depth, real lighting/reflections, bokeh, camera moves. Use for premium logo/product/abstract reveals. Costs render time. Setup is already done: R3F v9 + `Config.setChromiumOpenGlRenderer("angle")`. Reuse `three-kit.tsx` (StudioEnv, StudioLights, StudioFloor, StudioEffects, Confetti3D). Keep crisp Persian text as a 2D `<AbsoluteFill>` overlay ON TOP of `<ThreeCanvas>` — don't render Persian text in 3D.
## Color design
- Drive every colorable element from the `colorSchema` props (accent / secondary / background / text) so the studio can recolor it — see `../remotion-svg-colors/SKILL.md`.
- Build depth with VALUE, not just hue: dark bg → mid elements → bright accents/highlights. Add glow (`hexToRgba(accent, .6)` shadows) and a vignette (`inset 0 0 600px rgba(0,0,0,.6)`).
- Gradients: 2-3 stops max; blend related hues (`mixHex(a,b,.5)`). Mesh look = several large blurred radial-gradient circles drifting on `sin(frame/…)`.
- Contrast: text needs ≥ 4.5:1 over its backdrop — add a scrim/shadow when over busy/3D scenes.
- Default palettes per mood: tech = blue→violet on near-black; festive = warm gold/red/green on cream or turquoise; luxury = gold on charcoal; fresh = teal/green on light.
## Object design
- **2D:** compose from primitives — `<circle>`, `<rect rx>`, `<path>` (quadratic `Q` for organic curves). Reuse `rand(i)` for deterministic scatter (particles, confetti, petals). Add glow via SVG `filter: drop-shadow`.
- **3D:** primitives + good material = premium. `meshStandardMaterial` with `metalness` 0.3-0.7, `roughness` 0.15-0.35, `flatShading` for faceted gems, `emissive`+`emissiveIntensity` for glow that bloom picks up (set `toneMapped={false}` on flames/glows). Light with 3-point + colored rims (StudioLights). Faceted icosahedron/octahedron/dodecahedron read as "gems"; RoundedBox for gifts/cards.
## Motion = polish
Stagger entrances (don't reveal everything at once), use `spring()` for pops and `interpolate(..., Easing.out(Easing.cubic))` for slides, add secondary motion (breathe, drift, twinkle), and a subtle continuous camera/scene sway in 3D. Hold the final frame ~1s for readability.
## Quality checklist
- One coherent style; depth via value + glow + vignette.
- All colors come from props (recolorable).
- Staggered, eased motion with secondary detail.
- Renders deterministically (no random/date).
- Verify visually: render stills at 3-4 key frames and LOOK before shipping.
Related: `../remotion-character-design/SKILL.md`, `../remotion-aspect-ratios/SKILL.md`, `../remotion-template-composition/SKILL.md`, `../remotion-svg-colors/SKILL.md`.
@@ -0,0 +1,59 @@
---
name: remotion-music-picker
description: How to choose royalty-free background music for a FlatRender template and sync the animation to its beat/vibe. Use when picking a music bed for a template, matching mood and BPM to the visuals, syncing reveals to the beat, or sourcing free/royalty-free tracks.
---
# Music picker for templates
> Status: templates currently ship without a music bed. This is the playbook for adding one. The right track + beat-synced motion is what makes a template feel "produced".
## Match music to the template's job
| Template vibe | Genre / mood | Typical BPM |
|---|---|---|
| Corporate / SaaS logo | clean inspirational, soft piano + synth pluck | 90-110 |
| Energetic promo / sale | upbeat pop, four-on-the-floor, claps | 120-130 |
| Social / Insta / trendy | lo-fi or modern pop, punchy | 100-120 |
| Epic / product reveal | cinematic build, big drum, riser | 70-90 build → hit |
| Festive (birthday, Nowruz, party) | happy ukulele/marimba, bells | 110-128 |
| Emotional (wedding, tribute) | warm piano/strings | 60-80 |
| Tech / gaming | electronic, arpeggios, bass | 120-140 |
| Luxury | downtempo, jazzy, smooth | 80-100 |
| Minimal / explainer | light marimba/plucks, unobtrusive | 95-115 |
## Sync the animation to the beat (this is the magic)
1. Know the track's BPM → frames per beat = `fps * 60 / bpm` (e.g. 30fps, 120bpm → 15 frames/beat).
2. Land your hero reveals, pops, and cuts ON beats (multiples of frames-per-beat). Stagger small element pops on 1/2 or 1/4 beats.
3. Put the BIG reveal on a musical downbeat or right after a riser/drop.
4. For a known track, hardcode beat frames; for generic use, expose `bpm` as a prop and compute beat frames so motion stays in sync if the track changes.
```tsx
const FPS = 30, BPM = 120;
const beat = (FPS * 60) / BPM; // frames per beat
const onBeat = (n: number) => Math.round(n * beat);
// reveal hero on beat 4, CTA on beat 8
```
## Remotion wiring
```tsx
import { Audio, staticFile, interpolate, useCurrentFrame, useVideoConfig } from "remotion";
const { durationInFrames } = useVideoConfig();
const f = useCurrentFrame();
<Audio src={staticFile("music/upbeat-120.mp3")}
volume={(ff)=>interpolate(ff,[0,15,durationInFrames-20,durationInFrames],[0,0.7,0.7,0])} />
```
- Fade IN over ~0.5s and OUT over the last ~0.7s — never start/end abruptly.
- Trim/loop the track to the template length; cut on a bar boundary so the end feels intentional.
- If there's a voiceover, DUCK the music (~-12 dB) under it.
- Store beds in `services/remotion/public/music/`, named with BPM (`upbeat-120.mp3`).
## Free / royalty-free sources (verify license per track)
- **Uppbeat** (free tier, gives a clearance ID), **Pixabay Music** (CC0-ish), **Mixkit** (free for commercial), **Bensound** (free w/ attribution or licensed), **Free Music Archive** (per-track CC), **YouTube Audio Library** (downloadable, check terms), **Incompetech / Kevin MacLeod** (CC-BY).
- ALWAYS check: commercial use allowed? attribution required? Keep a per-file license/attribution record. Prefer CC0 / royalty-free-with-commercial. Avoid anything Content-ID-flagged for a product that exports user videos.
- For an Iran-facing product, also consider local royalty-free Persian/instrumental sources where licensing is clearer.
## Workflow
1. Pick vibe + BPM from the table for the template's purpose.
2. Source 2-3 candidate tracks (license-checked), audition against the animation.
3. Re-time the key reveals to the chosen BPM's beats.
4. Add `<Audio>` with fades; render and LISTEN; nudge beats until reveals hit on the downbeat.
Related: `../remotion-sound-effects/SKILL.md`, `../remotion-template-catalog/SKILL.md`.
@@ -0,0 +1,56 @@
---
name: remotion-sound-effects
description: Which sound effects (SFX) to use for FlatRender video templates and exactly where to place them in the timeline. Use when adding audio punch to a template — whooshes, impacts, sparkles, pops, risers, confetti — and syncing them to keyframes with Remotion's Audio component.
---
# Sound-effect design for templates
> Status: current FlatRender templates have NO audio yet. This skill is the playbook for adding it. SFX dramatically raise perceived quality — a logo "thud" + sparkle makes a reveal feel pro.
## How audio works in Remotion
Use `<Audio>` from `remotion`, placed in the composition tree, timed to frames:
```tsx
import { Audio, useVideoConfig } from "remotion";
// play a one-shot SFX starting at frame 55 (the logo-land beat)
<Audio src={staticFile("sfx/impact.mp3")} startFrom={0} volume={0.8}
// mount it only around its moment using a <Sequence from={55}> wrapper
/>
```
Patterns:
- Wrap one-shots in `<Sequence from={FRAME}>` so they trigger at the right beat.
- `volume` can be a function of frame for fades: `volume={(f)=>interpolate(f,[0,10],[0,1])}`.
- Layer SFX over the music bed (see `../remotion-music-picker/SKILL.md`); keep SFX ~ -6 dB under dialogue, on top of music.
- Put shared SFX in `services/remotion/public/sfx/` and load with `staticFile()`.
## SFX → moment mapping (sync to the KEYFRAME, not vaguely)
| Moment in the animation | SFX | Place at |
|---|---|---|
| Element/text flies in | **whoosh** (short, directional) | 2-3 frames BEFORE it lands |
| Logo / hero lands | **impact / thud / boom** | the exact land frame (spring settle) |
| Glitter / magic reveal | **sparkle / shimmer / chime** | over the particle gather (0.3-0.5s) |
| Small element appears | **pop / tick / blip** | each appearance (stagger to match) |
| Countdown ticking | **clock tick** per number, **ding/airhorn** on GO | each number frame |
| Birthday / party | **party horn + confetti rustle**, soft **chime** | greeting reveal + confetti burst |
| Sale / promo | **cash register "cha-ching" / coin**, **stamp** on the badge | badge pop |
| Shine sweep across logo | **soft shimmer swell** | sweep start→end |
| Transition between scenes | **whoosh + light riser** | on the cut |
| Build-up before reveal | **riser / uplifter** (0.5-1.5s) | leading INTO the hero moment |
## Placement principles
- **Anticipation:** risers and whooshes START before the visual peak and resolve ON it. A whoosh that lands with the logo sells the motion.
- **One hero hit:** the reveal gets ONE big impact — don't stack 3 booms; it muddies.
- **Match the motion curve:** fast spring = sharp transient; slow ease = soft swell.
- **Stagger to the visuals:** if 5 elements pop on different frames, 5 pops on those frames (vary pitch slightly so it's not robotic).
- **Less is more:** 3-6 well-placed SFX per template beats a wall of sound. Leave silence for contrast.
- **Loudness:** normalize SFX, peak ~ -3 dB, sit them under the music bed; the final mix shouldn't clip.
## Free / royalty-aware SFX sources
Mixkit, Pixabay (sound), Freesound (check each license — CC0 vs attribution), Uppbeat (free tier), and Remotion-safe CC0 packs. ALWAYS record the license per file; prefer CC0/royalty-free with commercial use. Keep an attributions file if any source requires it.
## Workflow
1. Identify the 3-6 key beats (reveal, pops, transitions, CTA).
2. Pick one SFX per beat from the table.
3. Place via `<Sequence from={frame}>` + `<Audio>`; tune volume + a short fade.
4. Render with audio and LISTEN — adjust timing by a few frames so hits land exactly on the visual.
Related: `../remotion-music-picker/SKILL.md`, `../remotion-template-composition/SKILL.md`.
@@ -0,0 +1,45 @@
---
name: remotion-svg-colors
description: How FlatRender makes template colors user-editable and generates per-scene SVG so the studio can recolor a template and preview it in real time. Use when wiring a template's colors to the studio color picker, choosing which elements are recolorable, or generating an SVG color-preview for a scene.
---
# SVG + color system (real-time recolor)
Goal: a user opens a template, changes its colors, and sees the result update live. This works because every colorable element reads from a NAMED color, those names are stored as editable color elements in the DB, and a lightweight SVG representation lets the studio recolor without a full re-render.
## The color data model
- **`content.shared_colors`** — project-wide colors (key = `element_key`, e.g. `accentColor`). Used by every scene.
- **`content.scene_color_elements`** — per-scene colors (key = `element_key`, e.g. `frl_c1t1` for AE, or a Remotion prop name).
- Studio copies these into `studio.saved_shared_colors` / `saved_scene_colors`; the render binder (`GetRenderBindings` in render-svc) returns them as `{element_key: hex}`.
- For **Remotion**, those keys must equal the composition's `colorSchema` props: `accentColor`, `secondaryColor`, `backgroundColor`, `textColor` (from `src/lib/branding.ts`). The node-agent passes them as `--props`.
- For **AE**, colours bind into the `frshare` comp's text layers (`bind.jsx`).
**Rule:** design every template so EVERY colorable element's color comes from a named prop — never a hardcoded hex for anything the user should be able to change. Seed a `shared_colors` row per color prop (the seed script already does accent/secondary/background/text).
## The SVG color-preview (live recolor without re-rendering)
A full Remotion/AE re-render is too slow for a color picker. So a scene is also represented as an **SVG** whose shapes' `fill`/`stroke` reference the SAME color keys. The studio swaps the SVG's colors instantly as the user drags the picker.
- AE pipeline: `content.projects.shared_colors_svg` + the `template_svg_previews` table + per-scene snapshots.
- For a Remotion template, generate an SVG snapshot of a representative frame where colorable regions are tagged with their key, e.g.:
```svg
<rect ... fill="var(--accentColor)" data-color-key="accentColor"/>
<text ... fill="var(--textColor)" data-color-key="textColor">...</text>
```
The studio sets CSS variables (`--accentColor: #...`) or rewrites `fill` by `data-color-key` to recolor live; on export the real props go to the renderer.
## How to author a recolorable template
1. Use the 4 `colorSchema` props for all themeable color (add more named props only if a template genuinely needs them — and seed a matching `shared_colors` row for each).
2. Keep colorable regions as FLAT fills/strokes that map cleanly to one key (gradients = blend of two named props via `mixHex`, still derived from props).
3. Produce an SVG preview of the key frame with each region tagged `data-color-key` = the prop name, so the studio can map picker → region.
4. Verify: changing a prop changes exactly the intended regions and nothing hardcoded stays the wrong color.
## Generating the SVG
- Simple/flat scenes: hand-author or script an SVG mirroring the composition's shapes, tagging fills with the color keys.
- 3D / complex scenes: an SVG can't represent them faithfully — fall back to a rendered key-frame thumbnail per color theme, or a simplified 2D SVG stand-in for the picker (note this limitation to the user).
- Store alongside the template (the AE path uses `shared_colors_svg` / `template_svg_previews`; mirror that for Remotion).
## Pitfalls
- A hardcoded hex on a "should-be-editable" element = the picker silently does nothing there. Audit for stray hexes.
- Key mismatch (SVG `data-color-key` ≠ schema prop ≠ seeded `element_key`) breaks the binding — keep ONE naming source of truth.
- Contrast: when users can recolor text + background, enforce/encourage a min contrast or the text can vanish.
Related: `../remotion-template-composition/SKILL.md`, `../remotion-design-styles/SKILL.md`.
@@ -0,0 +1,54 @@
---
name: remotion-template-catalog
description: A taxonomy of video template TYPES to build for FlatRender, with the purpose, key editable elements, suggested style/aspect, and 2D-vs-3D recommendation for each. Use when deciding what template to create next, planning a content batch, or scoping a requested template into a known pattern.
---
# Template catalog — what to build
FlatRender already has these (services/remotion `TEMPLATES`): IlluminatedCircles, KineticQuote, GradientPromo, VerticalStory, LogoMotion, Opener, InstaPromo, YouTubeIntro, Slideshow, HappyBirthday, SalePromo, QuoteCard, EventInvite, Countdown, GlitterReveal (editable logo), NowruzGreeting (2D characters), + 3D: Hero3D, Nowruz3D, Birthday3D, Promo3D.
Use this map to pick the NEXT one and to scope a request into a pattern. Each row: key editable elements · suggested style · best aspect(s) · 2D/3D.
## Brand / logo
- **Logo reveal** — logo + tagline · any style · all aspects · 2D or 3D. (have: IlluminatedCircles, LogoMotion, GlitterReveal, Hero3D)
- **Opener / intro sting** — title + subtitle · cinematic/kinetic · 16:9, 9:16 · 2D/3D. (have: Opener)
- **Outro / subscribe / end-card** — CTA + socials + logo · flat/neon · 16:9, 9:16 · 2D. (gap)
- **Lower-third / name tag** — name + role · clean/glass · 16:9 · 2D. (gap)
## Social / marketing
- **Instagram post/story promo** — headline + image + CTA · gradient/bold · 1:1, 9:16. (have: InstaPromo, VerticalStory)
- **YouTube intro/outro** — channel + subscribe · energetic · 16:9. (have: YouTubeIntro)
- **TikTok/Reels hook** — big kinetic text · trendy · 9:16 · 2D. (gap)
- **Sale / discount** — badge + headline + CTA · bold/3D gifts · all. (have: SalePromo, Promo3D)
- **Product showcase / turntable** — product image/3D + specs · 3D cinematic · 16:9, 1:1. (gap — high value)
- **Testimonial / review** — quote + stars + name/photo · clean · 1:1, 9:16. (gap)
- **Explainer / feature list** — icon + text steps · isometric/flat · 16:9. (gap)
- **Real-estate / listing** — photos + price + details · elegant · 16:9, 1:1. (gap)
- **Restaurant / menu / food** — dish image + price · warm/appetizing · 1:1, 9:16. (gap)
## Greetings / occasions (great for characters + 3D)
- **Birthday** — name + message · party/3D cake · all. (have: HappyBirthday, Birthday3D)
- **Nowruz (نوروز)** — greeting + year · spring characters/3D Haft-Sin · all. (have: NowruzGreeting, Nowruz3D)
- **Yalda (یلدا)** — pomegranate/watermelon, candles, warm night · 2D/3D · all. (gap — high value, Persian)
- **Wedding / engagement** — names + date · luxury gold/floral · all. (gap)
- **Eid / Ramadan / Mehregan** — lantern/crescent/autumn motifs · ornate · all. (gap, Persian/region)
- **New Year / holidays** — countdown + fireworks · festive 3D · 16:9, 9:16. (gap)
- **Condolence / tribute** — respectful, minimal, slow · muted palette · all. (gap)
## Content / text
- **Quote card** — quote + author · kinetic/typographic · 1:1, 9:16. (have: QuoteCard, KineticQuote)
- **Countdown** — target + number · energetic riser · all. (have: Countdown)
- **Event invite** — title + date/place + RSVP · elegant · 1:1, 9:16. (have: EventInvite)
- **Slideshow / photo gallery** — N images + captions · clean transitions · all. (have: Slideshow)
- **Music visualizer** — audio-reactive bars + cover · neon/3D · 1:1, 9:16. (gap)
## How to choose next
1. Prefer **gaps** with high value for an Iran-facing product: Yalda, wedding, product showcase, testimonial, lower-thirds, outro/subscribe, music visualizer.
2. Pick a STYLE that differs from neighbors (don't ship five dark-particle reveals) — see `../remotion-design-styles/SKILL.md`.
3. Decide 2D vs 3D by subject (logos/products/abstract → 3D shines; text/character/illustrative → 2D or hybrid).
4. Confirm a storyboard with the user for anything character- or scene-heavy.
## Per-template build steps
Storyboard (confirm) → build composition (lib helpers, design-styles, character-design) → make it fit all aspects (`../remotion-aspect-ratios/SKILL.md`) → wire editable text/logo/colors (`../remotion-template-composition/SKILL.md`, `../remotion-svg-colors/SKILL.md`) → pick fonts (`../persian-fonts/SKILL.md`) → optional music/SFX → render thumbnails + preview → seed (`scripts/seed_remotion_templates.py`) → deploy.
Related: every other remotion-* skill.
@@ -0,0 +1,60 @@
---
name: remotion-template-composition
description: How to compose the editable elements of a FlatRender template — text, logo, image/media, and supporting copy — into a clear, well-paced presentation. Use when laying out what goes where, deciding the visual hierarchy, wiring editable fields, or timing the reveal sequence of a template.
---
# Composing a template (text / logo / image / copy)
A template is not just a nice animation — it's a *fill-in-the-blanks* product. Users edit a few fields and it must look great with THEIR text/logo. Design for editability + clarity.
## The binding model (how editable fields work)
Editable elements live in the DB and bind to Remotion props by KEY (see `../remotion-svg-colors/SKILL.md` for the full pipeline):
- **Text** → `content.scene_content_elements` of type `Text`; the element `key` MUST equal the composition's Zod schema field (e.g. `headline`, `tagline`). Studio shows a text input.
- **Logo / image / media** → a `scene_content_elements` of type `Media`; key = a `z.string()` prop (e.g. `logoUrl`). Studio shows upload/replace. In the composition: `logoUrl ? <Img src={logoUrl}> : <DefaultMark/>`. See `GlitterReveal.tsx`.
- **Colors** → `shared_colors` / `scene_color_elements`, key = a `colorSchema` prop.
- Seed all of these via `scripts/seed_remotion_templates.py` (it has a `MEDIA` dict for image fields).
**Rule:** every visible piece of copy or media a user would want to change MUST be a prop + a seeded element. Don't hardcode the brand name, date, price, etc.
## Visual hierarchy (most templates need 2-4 tiers)
1. **Primary** — the logo OR the headline/hero. Biggest, highest contrast, center of attention.
2. **Secondary** — tagline / subtitle. ~35-45% of primary size.
3. **Tertiary** — CTA, date, price badge, handle. Small but distinct (pill, badge, accent color).
4. **Ambient** — decorative scene (particles, 3D, characters) — supports, never competes.
Size ratios that read well: primary `vmin(80-110)`, secondary `vmin(26-40)`, tertiary `vmin(24-32)`. Weight: primary 800-900, secondary 500-700, tertiary 600-900.
## Text legibility (critical over busy/3D backgrounds)
- Add a scrim or shadow: `textShadow: 0 0 vmin(20) rgba(0,0,0,.7)`, or a semi-transparent panel behind text.
- Persian is RTL: set `direction: "rtl"`, use `FONT` (Vazirmatn) from `lib/fonts.ts`. See `../persian-fonts/SKILL.md`.
- Keep line length comfortable (`maxWidth ~ 86%`); never let text touch frame edges (see `../remotion-aspect-ratios/SKILL.md`).
- For gradient text use `backgroundClip: text` + a `drop-shadow` for separation.
## Logo placement
- Center-stage for logo reveals; corner/lockup for promos.
- Always provide a branded DEFAULT (the FlatRender mark) so the template looks finished before the user uploads.
- Constrain with `maxWidth/maxHeight` + `objectFit: contain` so any uploaded logo fits without distortion.
## Image / media
- `objectFit: cover` for fullscreen backdrops (with a gradient scrim for text), `contain` for product/logo.
- Add motion: slow Ken-Burns (`scale 1→1.08` + slight pan) so stills feel alive.
- Mask into shapes (rounded rect, circle) for polish.
## Timing & pacing (the reveal sequence)
Stagger — never reveal everything at frame 0. A typical 6s (180f @30) beat sheet:
- 0-30: scene/background establishes, ambient starts.
- 20-55: hero/logo springs in (`spring()`), optional flash/impact.
- 55-90: headline rises/fades in.
- 90-120: tagline fades, letter-spacing settles.
- 120-160: CTA/date pops (`spring`, glow pulse).
- last ~30f: hold everything still for readability.
Give each text element ~0.6-0.8s on screen minimum before the next competes.
## Editability checklist
- [ ] Every changeable text/logo/image/color is a prop + seeded element (key == schema field).
- [ ] Branded default for logo + sensible default copy.
- [ ] Clear 2-4 tier hierarchy; text legible over the background.
- [ ] Staggered, eased reveal with a final hold.
- [ ] Looks good with long Persian text and a tall logo (test it).
Related: `../remotion-svg-colors/SKILL.md`, `../remotion-aspect-ratios/SKILL.md`, `../persian-fonts/SKILL.md`, `../remotion-design-styles/SKILL.md`.
@@ -0,0 +1,121 @@
---
name: scene-transitions
description: How to choreograph transitions BETWEEN scenes (and shots within a scene) in FlatRender Remotion templates — cut, dissolve, wipe, clip-path mask, morph, match-cut, shape transition, camera push, zoom/whip-blur — built from primitives. Use whenever a template has more than one scene/beat, when one element must hand off to the next, or when stitching multi-scene sequences so they feel seamless instead of slideshow-y. Read before sequencing scenes.
---
# Scene transitions for Remotion
A multi-scene template lives or dies on its *joins*. A hard slideshow of fades reads as "made in a tool"; a transition that carries motion, color, or a shape across the cut reads as "made by a studio". We have **no `@remotion/transitions` package** (not a dependency) and asset CDNs are geo-blocked — so every transition is built from primitives: `<Sequence>`, `interpolate`/`spring`, CSS `clipPath`/`maskImage`, blend modes, and (for 3D) a camera move driven by `useCurrentFrame()`. Everything is a pure function of `frame`**never** `useFrame`, `Math.random`, `Date.now` (use `rand()` from `lib/anim.ts`).
## The one structural rule: overlap, don't abut
A clean transition needs scenes to **overlap** for the transition window (1220f). Don't place `<Sequence>`s back-to-back — give the outgoing scene a tail and the incoming a head that share the window.
```tsx
import { Sequence, useVideoConfig } from "remotion";
const { fps } = useVideoConfig();
const sec = (s: number) => Math.round(s * fps); // never hardcode 30
const T = sec(0.5); // transition window
// Scene A holds frames 0..120, Scene B starts at 120-T so they cross-fade
<Sequence from={0} durationInFrames={120}><SceneA /></Sequence>
<Sequence from={120 - T} durationInFrames={120}><SceneB /></Sequence>
```
Inside each scene, derive a local progress from `useCurrentFrame()` (already 0-based inside a `Sequence`) for its *in* and *out* phases.
## Transition catalog — what to build, when, and how
| Transition | فارسی | Feel / when | Build (primitive) |
|---|---|---|---|
| **Cut** | برش | Hard, energetic, beat-synced, brutalist/anti-design | No overlap; `<Sequence>` ends, next begins. Snap a 1f flash or shake for punch. |
| **Dissolve / crossfade** | محو | Calm, elegant, photo decks, luxury | Outgoing `opacity 1→0`, incoming `0→1` over the window, `clamp` both. |
| **Wipe** | پاک‌کن | Directional energy, news/promo | `clipPath: inset()` on the incoming layer sweeps a hard edge (see below). |
| **Clip-path mask reveal** | ماسک | Premium reveals, shape brand moment | Animate a `circle()`/`polygon()` `clipPath` open over the new scene. |
| **Morph** | ریخت‌گردانی | Liquid/organic, kinetic trend | Animate SVG `path d` (`flubber`-style) or `feGaussianBlur`+`feColorMatrix` gooey merge. |
| **Match-cut** | برش تطبیقی | Storytelling, "made by a studio" | A shape/element at the SAME position+size in both scenes; cut while it's identical. |
| **Shape transition** | گذار شکلی | Brand mark grows into scene | A circle/blob scales up to fill frame (color = `accent`), then the new scene is revealed inside it. |
| **Camera push / dolly** | حرکت دوربین (۳بعدی) | Cinematic, 3D logo/product | Move the R3F camera `position.z` / target between two staged setups by `frame`. |
| **Zoom / whip blur** | زوم/تار حرکتی | Fast, hype, music, TikTok | Scale up + `filter: blur()` on out, scale-down + blur-out on in; peak blur ON the cut. |
### Wipe (clip-path inset)
```tsx
import { useCurrentFrame, interpolate, Easing } from "remotion";
const f = useCurrentFrame(); // local frame in the incoming Sequence
const p = interpolate(f, [0, sec(0.45)], [0, 100], {
extrapolateLeft: "clamp", extrapolateRight: "clamp",
easing: Easing.bezier(0.16, 1, 0.3, 1),
});
// RTL-aware: wipe in from the right for Persian (mirror the direction in `en`)
<AbsoluteFill style={{ clipPath: `inset(0 0 0 ${100 - p}%)` }}><SceneB/></AbsoluteFill>
```
Soften the edge with a leading gradient strip (a thin `accent` bar riding `p`) for a "luminance wipe".
### Clip-path circle / shape reveal
```tsx
const r = interpolate(spring({ frame: f, fps, config: { mass: 0.6, damping: 14 } }),
[0, 1], [0, 150]); // 150% covers corners
<AbsoluteFill style={{ clipPath: `circle(${r}% at 50% 50%)` }}><SceneB/></AbsoluteFill>
```
For a brand shape transition: render a full-frame circle filled with `colorSchema.accent` scaling up over Scene A, then swap to Scene B *masked by the same circle* — the brand color carries the cut.
### Zoom / whip-blur
```tsx
// outgoing tail
const out = interpolate(f, [0, T], [1, 1.4], { extrapolateRight: "clamp" });
const blurOut = interpolate(f, [0, T], [0, vmin(24)], { extrapolateRight: "clamp" });
<AbsoluteFill style={{ transform: `scale(${out})`, filter: `blur(${blurOut}px)` }}><SceneA/></AbsoluteFill>
// incoming head (local frame): scale 1.25→1, blur 24→0 — peak blur of BOTH meets on the cut
```
`vmin` comes from `useLayout()` (`lib/aspect.ts`) so the blur reads the same in all three aspects.
### Camera push (3D, @remotion/three)
```tsx
// inside <ThreeCanvas> — drive the camera off frame, NOT useFrame
const z = interpolate(spring({ frame: f, fps, config: { mass: 2.5, damping: 26 } }),
[0, 1], [7, 3.2]); // dolly in, heavy = weight
useThree(({ camera }) => { camera.position.z = z; camera.updateProjectionMatrix(); });
```
Use `StudioEnv/StudioLights/StudioFloor/StudioEffects` from `lib/three-kit.tsx`; let DOF + bloom + vignette sell the move. Camera moves use `ease-in-out`/heavy spring; never linear (linear is only for continuous orbit/rotation).
## Match-cut & seamless choreography (the studio-grade joins)
The eye forgives a cut if **something continues across it**. Carry one of:
- **Position+scale** — a circle bottom-left in Scene A is a circle bottom-left, same size, in Scene B. Cut while identical. (Classic match-cut.)
- **Color** — Scene A ends on a full-frame `accent` wash; Scene B opens from that wash. Use `mixHex`/`hexToRgba` (`lib/anim.ts`) so it's palette-driven.
- **Motion vector** — text exits stage-left at speed `v`; the next element enters from stage-right at the same `v`. Momentum reads as continuity.
- **A mask** — the shape that wiped scene A out is the shape scene B wipes in with.
For a full template: write a **beat list first** (logo in → tagline → 3 features cascade → CTA → out), assign one transition per join, and make adjacent joins *differ* (don't dissolve every cut) but **rhyme** (reuse the brand shape/color). Vary cut length and build to the hero moment — pacing is a transition too.
## Timing & easing (the difference between pro and slideshow)
- **Window:** scene transition **1220f**; whip/cut feels best at the short end, dissolve/camera at the long end.
- **Entrances ease-out** (`Easing.out(Easing.quint)` / `Easing.bezier(0.16,1,0.3,1)`); **exits ease-in and always SHARPER than the entrance** — scenes leave faster than they arrive.
- **A→B on-screen / camera = ease-in-out.** **Linear ONLY** for continuous rotation/marquee.
- Snap transition `from` frames to the **music beats** (`../remotion-music-picker/SKILL.md`) so cuts land on downbeats.
- Per-aspect: tighten the window on `isWide` (reads faster), loosen on `isTall`. Use the proposed `pick(wide,square,tall)` helper on `Layout` when it lands; until then branch on `isWide/isSquare/isTall`.
## Reusable transition components
Build these once in `lib/` and reuse across templates — each takes an `enter`/`exit` phase and a window:
```tsx
// CrossFade.tsx — wrap any scene; computes its own in/out from frame + duration
export const Dissolve: React.FC<{ children: React.ReactNode; win: number }> = ({ children, win }) => {
const f = useCurrentFrame();
const { durationInFrames } = useVideoConfig(); // length of THIS Sequence
const o = Math.min(
interpolate(f, [0, win], [0, 1], { extrapolateRight: "clamp" }),
interpolate(f, [durationInFrames - win, durationInFrames], [1, 0], { extrapolateLeft: "clamp" }),
);
return <AbsoluteFill style={{ opacity: o }}>{children}</AbsoluteFill>;
};
```
Make sibling wrappers `Wipe`, `CircleReveal`, `WhipZoom`, `ShapeWipe` with the same `(children, win, dir)` contract so a template can swap transitions by changing one wrapper. Keep the SFX hook in mind: a whoosh 23f before the cut + an impact ON it (`../remotion-sound-effects/SKILL.md`).
## Pre-ship transition checklist
- [ ] No back-to-back `<Sequence>`s where a join should be smooth — scenes **overlap** the window.
- [ ] Every join has a chosen transition with an *intent* (energy/calm/brand), not a default fade everywhere.
- [ ] At least one join **carries** position, color, motion, or a mask across the cut (not all isolated fades).
- [ ] All `interpolate` have `clamp` on both ends (the #1 drift bug).
- [ ] Exits are sharper than entrances; nothing linear except continuous motion.
- [ ] Cuts snapped to beats; whoosh-in + impact-on-cut wired.
- [ ] Verified in 16:9 / 1:1 / 9:16 — wipe direction & blur amount read the same (`vmin`, not px); Persian RTL wipes from the right.
- [ ] Colors via `colorSchema` (`mixHex`/`hexToRgba`), never hardcoded; deterministic (re-render twice → identical).
Related: `../remotion-template-composition/SKILL.md`, `../remotion-aspect-ratios/SKILL.md`, `../remotion-design-styles/SKILL.md`, `../remotion-sound-effects/SKILL.md`, `../remotion-music-picker/SKILL.md`, `../remotion-character-design/SKILL.md`, `../remotion-svg-colors/SKILL.md`, `../persian-fonts/SKILL.md`, `../flatrender-template-seo/SKILL.md`.
@@ -0,0 +1,114 @@
---
name: video-hooks
description: How to design the scroll-stopping first 1-3 seconds of a FlatRender Remotion template — hook archetypes, pattern interrupts, on-screen text hooks, curiosity gaps, and platform-specific (Instagram/TikTok/YouTube) hook norms — and bake them into the template's opening beats. Use whenever building or reviewing a template's first frames, the cover/first frame, the caption hook layer, or retention pacing of the open.
---
# The hook (first 1-3 seconds — where templates are won or lost)
On a 9:16 feed the viewer decides **stay or swipe in 2-3 seconds** (TikTok's "3-second rule"; IG rewards 3-sec view rate). YouTube Shorts has **no runway** — open on the most compelling moment. So a FlatRender template doesn't get a polite logo intro: the **first frame is the cover/thumbnail and the hook**, and the first ~45-90 frames (@30fps) must arrest the eye. Everything here is a *pure function of `useCurrentFrame()`* — no `Math.random`/`Date.now`/`useFrame`; use `rand(seed)` from `lib/anim.ts`. Read `../remotion-aspect-ratios/SKILL.md` before positioning a single hook element.
## The frame budget for the open (30fps; use `sec(s)=Math.round(s*fps)`)
| Beat | Frames | Job |
|---|--:|---|
| **f0 — cover** | 1 | Must already read as a finished, intriguing thumbnail. No black/empty frame 0. |
| **Pattern interrupt** | 0-12 | One bold motion/sound jolt that breaks the scroll rhythm. |
| **Hook text lands** | 6-30 | The promise/question/claim, big, high-contrast, lower-middle third. |
| **Curiosity hold** | 30-75 | Pose an open loop the rest of the video closes. Don't resolve yet. |
| **Hero handoff** | 60-90 | Flow into logo/headline (`../remotion-template-composition/SKILL.md`). |
Front-load the payoff — **no preamble, no slow brand sting first**. Brand comes *after* the hook earns the watch.
## Hook archetypes (Persian-first copy; pick ONE per template)
| Archetype | Persian opener pattern | Best for | Motion signature |
|---|---|---|---|
| **Curiosity gap** | «اینو تا آخر ببین…» / «هیچ‌کس اینو بهت نگفته» | tips, reveals, teasers | text snaps in, then a held pause (open loop) |
| **Bold claim / contrarian** | «این روش رو فراموش کن» / «۹۰٪ اشتباه انجامش می‌دن» | how-to, product | hard cut + overshoot back-bezier |
| **Question** | «دنبال … می‌گردی؟» | services, lead-gen | rise + tilt, then steady |
| **Negativity / warning** | «این اشتباه رو نکن» | finance, health, safety | red accent flash + shake |
| **Number / list** | «۳ دلیل که…» / «۵ نکته…» | listicles, carousels | counter ticks up, items pre-stack off-screen |
| **Result-first** | show the after/price-drop/win immediately | promo, sale, before-after | hero appears f0, *then* explains |
| **Direct address** | «تو که … هستی، اینو لازم داری» | niche/targeted | type fills 70-90% of frame |
Use Persian numerals (`۰-۹`) — never Latin digits — in hook copy and counters; `fa` is source of truth, `en` mirrors 1:1.
## Pattern interrupts (the scroll-breaking jolt in f0-12)
The feed has a rhythm; a hook *breaks* it. Stack 1-2 of these, never all:
- **Motion jolt** — whip-in with overshoot: `Easing.bezier(0.34,1.56,0.64,1)`, or a low-damping `spring({mass:0.6,damping:9,stiffness:200})`. Add motion blur on the fast frames (its absence is an amateur tell).
- **Hard cut + flash** — a 1-2 frame white/accent wash: `opacity = frame < 2 ? 1 : 0` over a `hexToRgba(accentColor, …)` fill. Pair with a thump SFX (`../remotion-sound-effects/SKILL.md`).
- **Scale punch** — start at `scale` 1.6→1.0 (clamp) so the subject "slams" toward camera.
- **Color shock** — open on a dopamine accent (electric blue/coral/acid) on a neutral base; pull it from `accentColor` so the studio recolors it.
- **Silence-then-hit** — a held silent f0-8, then riser+downbeat on the hook (`../remotion-music-picker/SKILL.md` BPM map). The pause *is* the interrupt.
```tsx
// Pattern-interrupt whip-in for the hook line (deterministic, clamped)
const f = useCurrentFrame();
const { fps } = useVideoConfig();
const intro = spring({ frame: f, fps, config: { mass: 0.6, damping: 9, stiffness: 200 } });
const y = interpolate(intro, [0, 1], [L.vmin(60), 0]); // rises into place
const flash = interpolate(f, [0, 2, 5], [1, 0.5, 0], { extrapolateRight: "clamp" });
```
## On-screen text hooks (the highest-ROI layer)
The hook text is a **first-class editable field**, not decoration — it is the captions/cover layer the whole brief calls the biggest cross-platform win.
- **Placement:** lower-middle third, inside the *tightest* safe zone (Story/TikTok) so it's safe everywhere. For 1080×1920 keep hook Y ≈ `height*0.18-0.55`; clear top ~108 and bottom ~320 (UI chrome).
- **Legibility:** high-contrast white or acid-yellow fill + **black outline** (`WebkitTextStroke` or layered `textShadow`), never thin grey on busy bg. Add a scrim if over media.
- **Oversized & clipped:** the hook word can fill 60-90% of frame (`fitText` from `@remotion/layout-utils`); clip with `overflow:hidden`. Strongest on 9:16.
- **Kinetic / word-by-word:** beats full sentences on TikTok. Split to spans, `delay = i*stagger`, drive each with `spring({frame: f - delay, fps})`. Stagger looser on tall, tighter on wide via `pick`.
- **Variable weight pop:** Vazirmatn ships a variable build — animate `fontVariationSettings: "'wght' " + interpolate(f,[0,12],[300,900])` for a Persian hero hook.
```tsx
// Word-by-word Persian hook, RTL, outlined, beat-staggered
const words = hookText.split(" ");
const stagger = L.pick(2, 3, 4); // wide reads faster → tighter
return (
<div style={{ direction: "rtl", fontFamily: FONT, display: "flex",
gap: L.vmin(8), justifyContent: "center", flexWrap: "wrap",
maxWidth: L.width * 0.86 }}>
{words.map((w, i) => {
const s = spring({ frame: f - i * stagger, fps, config: { damping: 12 } });
return (
<span key={i} style={{
fontSize: L.pick(L.vmin(96), L.vmin(84), L.vmin(72)), fontWeight: 900,
color: textColor, WebkitTextStroke: `${L.vmin(6)}px ${BRAND.ink}`,
paintOrder: "stroke", transform: `translateY(${(1 - s) * L.vmin(40)}px)`,
opacity: s,
}}>{w}</span>
);
})}
</div>
);
```
## Curiosity & retention pacing across the open
- **Open a loop, close it later** — the hook *promises*, the hero *pays off*. Never resolve the question in the first 2s or there's no reason to stay.
- **One idea per beat** — staging: dim/blur everything but the hook; let it own the eye before the next element competes.
- **Hold for the read** — a hook line needs ~0.6-0.8s minimum on screen before motion competes. Robotic = linear; floaty = held too long. Cut frames before adding.
- **Tiny life in the hold** — a `sin(f/fps)` breathe/shimmer so the held hook isn't a frozen frame.
- **Grain + texture** from f0 — even the cover frame should have animated grain (offset `background-position` per frame); flat-saturated = reads as AI/template.
## Platform hook norms → template implication
| Platform | Hook window | Norm | Template move |
|---|---|---|---|
| **TikTok** | 3s | curiosity-gap / bold-claim; word-by-word captions | calm neutral grain + warm-earth variant; word-by-word hook as editable layer |
| **IG Reels** | 2-3s | cleaner, less-cluttered than TikTok | refined kinetic type, glass lower-third, mesh-gradient bg, one clean interrupt |
| **YT Shorts** | f0 | no runway — open on the peak | result-first / hero-at-f0; cinematic graded look |
| **YT long-form intro** | 5-15s | cold-open hook, brand sting <3s | state payoff first, brand second |
| **IG Story** | full-bleed | heavy UI chrome | keep hook clear of top ~250 / bottom ~250 |
| **All three** | 1-2s | first frame = hook = cover; authenticity > gloss | hook prop in every aspect, re-flowed not letterboxed |
## Tie the hook into template structure
- Make the hook copy a Zod prop (e.g. `hookText: z.string()`) + a seeded `Text` element whose `key` matches — same binding model as `../remotion-template-composition/SKILL.md`. Ship strong Persian default copy so it reads finished pre-edit.
- Hook color = `accentColor`/`textColor` from `colorSchema`; pass user hex through a grade so a garish value doesn't break the open (`../remotion-svg-colors/SKILL.md`).
- The hook is a `<Sequence from={0} durationInFrames={sec(2.5)}>`; the hero sequence overlaps its tail so the handoff is a flow, not a cut.
- 3D hooks: keep the interrupt object filling the frame per aspect (tune `fov`/`position.z`), drive entrance from `useCurrentFrame()` with high `mass` for weight; let `StudioEffects` (bloom/DOF/vignette) finish it.
## Hook checklist (gate the open)
- [ ] Frame 0 reads as a finished, intriguing cover — no black/empty/half-loaded frame.
- [ ] A single clear pattern interrupt in f0-12 (motion / flash / scale / color / silence-then-hit) with SFX.
- [ ] ONE hook archetype; Persian-first copy with Persian numerals; `en` mirror present.
- [ ] Hook text is an editable prop, high-contrast + outlined, in the tightest safe zone, no clipping with long Persian strings.
- [ ] An open loop is posed and NOT resolved in the first 2s; payoff lands at the hero.
- [ ] Eased/overshoot motion (no linear), held for the read, with a tiny live shimmer; animated grain from f0.
- [ ] Verified the open in all three aspects (`pick`-tuned), recolors cleanly, re-renders identical (deterministic).
Related: `../remotion-template-composition/SKILL.md`, `../remotion-aspect-ratios/SKILL.md`, `../remotion-design-styles/SKILL.md`, `../remotion-sound-effects/SKILL.md`, `../remotion-music-picker/SKILL.md`, `../remotion-svg-colors/SKILL.md`, `../persian-fonts/SKILL.md`, `../remotion-template-catalog/SKILL.md`, `../flatrender-template-seo/SKILL.md`.
+11 -1
View File
@@ -1,7 +1,8 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies # dependencies (root + any nested, e.g. services/remotion/node_modules)
/node_modules /node_modules
node_modules/
/.pnp /.pnp
.pnp.js .pnp.js
.yarn/install-state.gz .yarn/install-state.gz
@@ -55,3 +56,12 @@ node-agent.exe
# node-agent local build + secrets # node-agent local build + secrets
services/node-agent/dist/ services/node-agent/dist/
agent.env agent.env
# remotion render outputs (regenerated; thumbnails/previews live in public/template-media)
services/remotion/out/
# local scratch / agent work
/-w
/.agent-work/
dist/
services/remotion/player-dist/
+9 -7
View File
@@ -1,6 +1,8 @@
# ── Stage 1: install dependencies ──────────────────────────────────────────── # ── Stage 1: install dependencies ────────────────────────────────────────────
FROM mirror.soroushasadi.com/node:20-alpine AS deps FROM mirror.soroushasadi.com/node:20-slim AS deps
RUN apk add --no-cache libc6-compat # Debian (glibc) base on purpose: Alpine (musl) needs `libc6-compat` for next-swc,
# which is only on the geo-blocked Alpine CDN (unreachable from the CI server).
# Debian ships glibc, so next-swc's gnu binary loads natively — no apk, no CDN.
WORKDIR /app WORKDIR /app
COPY package.json package-lock.json* ./ COPY package.json package-lock.json* ./
@@ -18,7 +20,7 @@ RUN for i in 1 2 3 4 5; do \
echo "npm ci failed after 5 attempts" && exit 1 echo "npm ci failed after 5 attempts" && exit 1
# ── Stage 2: build ─────────────────────────────────────────────────────────── # ── Stage 2: build ───────────────────────────────────────────────────────────
FROM mirror.soroushasadi.com/node:20-alpine AS builder FROM mirror.soroushasadi.com/node:20-slim AS builder
WORKDIR /app WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules COPY --from=deps /app/node_modules ./node_modules
@@ -51,15 +53,15 @@ ENV NODE_ENV=production
RUN npm run build RUN npm run build
# ── Stage 3: production runner ──────────────────────────────────────────────── # ── Stage 3: production runner ────────────────────────────────────────────────
FROM mirror.soroushasadi.com/node:20-alpine AS runner FROM mirror.soroushasadi.com/node:20-slim AS runner
WORKDIR /app WORKDIR /app
ENV NODE_ENV=production ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1 ENV NEXT_TELEMETRY_DISABLED=1
# Create a non-root user (security best practice) # Create a non-root user (security best practice). Debian uses groupadd/useradd.
RUN addgroup --system --gid 1001 nodejs \ RUN groupadd --system --gid 1001 nodejs \
&& adduser --system --uid 1001 nextjs && useradd --system --uid 1001 --gid nodejs nextjs
# Copy public assets # Copy public assets
COPY --from=builder /app/public ./public COPY --from=builder /app/public ./public
@@ -0,0 +1,16 @@
-- 32_content_render_engine.sql
-- Two render engines per template: After Effects (.aep, rendered by a node-agent)
-- and Remotion (code-based React composition). render_engine selects which; for
-- Remotion templates render_remotion_comp holds the composition id to render
-- (the .aep / render_aep_comp columns stay null for those).
--
-- Apply manually on the live DB (migrations are not auto-run):
-- docker exec -i <postgres> psql -U postgres -d flatrender < 32_content_render_engine.sql
ALTER TABLE content.projects
ADD COLUMN IF NOT EXISTS render_engine TEXT NOT NULL DEFAULT 'AfterEffects';
ALTER TABLE content.projects
ADD COLUMN IF NOT EXISTS render_remotion_comp TEXT;
-- Existing templates are all After Effects; the default already covers them.
@@ -0,0 +1,35 @@
-- =====================================================================
-- PAYMENT BROKER — global settings (admin-editable ZarinPal config) + is_test
-- Lets the merchant id / sandbox flag / amount unit be set from the admin
-- panel instead of env + redeploy. A client_app may still override per-site.
-- Also adds transactions.is_test so admin smoke-test payments never fire a
-- client's production webhook.
--
-- Apply manually on an existing volume (runs after 31_payment_broker.sql):
-- docker exec -i fr2-postgres psql -U flatrender -d flatrender < 33_payment_settings.sql
-- =====================================================================
CREATE SCHEMA IF NOT EXISTS payment;
SET search_path TO payment, public;
CREATE TABLE IF NOT EXISTS payment.settings (
id SMALLINT PRIMARY KEY DEFAULT 1 CHECK (id = 1), -- singleton row
zarinpal_merchant_id TEXT NOT NULL DEFAULT '',
zarinpal_sandbox BOOLEAN NOT NULL DEFAULT TRUE,
zarinpal_amount_unit TEXT NOT NULL DEFAULT 'rial', -- 'rial' | 'toman'
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- NOTE: the singleton row is intentionally NOT pre-seeded. Until an admin saves
-- settings, GetSettings returns no-row and the broker falls back to ENV
-- (ZARINPAL_MERCHANT_ID / ZARINPAL_SANDBOX / ZARINPAL_AMOUNT_UNIT). Seeding a
-- default row here would force sandbox=TRUE and silently override a production
-- env (ZARINPAL_SANDBOX=false), routing real payments to the sandbox gateway.
DROP TRIGGER IF EXISTS tg_pay_settings_updated ON payment.settings;
CREATE TRIGGER tg_pay_settings_updated BEFORE UPDATE ON payment.settings
FOR EACH ROW EXECUTE FUNCTION public.tg_set_updated_at();
-- Mark admin smoke-test transactions so the webhook dispatcher never notifies a
-- real client (which could otherwise credit coins/activate a plan from a test).
ALTER TABLE payment.transactions ADD COLUMN IF NOT EXISTS is_test BOOLEAN NOT NULL DEFAULT FALSE;
+8 -3
View File
@@ -23,9 +23,14 @@ ZarinPal gateway shared by FlatRender + meezi.ir + bargevasat.ir — ZarinPal on
accepts callbacks on that one verified domain. It does NOT sit behind the API accepts callbacks on that one verified domain. It does NOT sit behind the API
gateway (clients authenticate with an API key + HMAC). See gateway (clients authenticate with an API key + HMAC). See
[`PAYMENTS.md`](./PAYMENTS.md) for the integration contract. The `payment` schema [`PAYMENTS.md`](./PAYMENTS.md) for the integration contract. The `payment` schema
is migration `31_payment_broker.sql` — on an existing DB volume it must be applied is migrations `31_payment_broker.sql` (tables) + `33_payment_settings.sql`
manually (migrations only auto-run on first volume creation): (admin-editable ZarinPal config + `transactions.is_test`) — apply BOTH, in order,
`docker exec -i fr2-postgres psql -U postgres -d flatrender < backend/db/migrations/31_payment_broker.sql`. on an existing DB volume (migrations only auto-run on first volume creation):
```
docker exec -i fr2-postgres psql -U flatrender -d flatrender < backend/db/migrations/31_payment_broker.sql
docker exec -i fr2-postgres psql -U flatrender -d flatrender < backend/db/migrations/33_payment_settings.sql
```
The broker image expects `is_test` (migration 33) — deploy it together with both migrations.
## One-time setup (do these BEFORE the first `git push gitea master`) ## One-time setup (do these BEFORE the first `git push gitea master`)
+11 -4
View File
@@ -32,7 +32,9 @@ services:
ports: ports:
# HOST_BIND=127.0.0.1 in prod keeps these off the public interface (only # HOST_BIND=127.0.0.1 in prod keeps these off the public interface (only
# Caddy's 80/443 face the internet). Unset → 0.0.0.0 for local/LAN dev. # Caddy's 80/443 face the internet). Unset → 0.0.0.0 for local/LAN dev.
- "${HOST_BIND:-0.0.0.0}:5432:5432" # Host port is configurable (PG_HOST_PORT) — 5432 is often taken by another
# local project's postgres; the internal stack always uses postgres:5432.
- "${HOST_BIND:-0.0.0.0}:${PG_HOST_PORT:-5532}:5432"
healthcheck: healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-postgres} -d flatrender"] test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-postgres} -d flatrender"]
interval: 5s interval: 5s
@@ -86,6 +88,9 @@ services:
Jwt__Audience: "flatrender" Jwt__Audience: "flatrender"
Jwt__AccessTokenMinutes: "${JWT_ACCESS_MINUTES:-1440}" Jwt__AccessTokenMinutes: "${JWT_ACCESS_MINUTES:-1440}"
ServiceToken: "${SERVICE_TOKEN:-internal-service-secret}" ServiceToken: "${SERVICE_TOKEN:-internal-service-secret}"
# Payment callbacks land on this service (api.*); the result page is on the
# frontend. Used to make /payment/result redirects absolute to the site.
Frontend__BaseUrl: "${NEXT_PUBLIC_SITE_URL:-http://localhost:3000}"
ZarinPal__MerchantId: "${ZARINPAL_MERCHANT_ID:-}" ZarinPal__MerchantId: "${ZARINPAL_MERCHANT_ID:-}"
ZarinPal__CallbackUrl: "${ZARINPAL_CALLBACK_URL:-http://localhost:8080/v1/payments/callback/zarinpal}" ZarinPal__CallbackUrl: "${ZARINPAL_CALLBACK_URL:-http://localhost:8080/v1/payments/callback/zarinpal}"
ZarinPal__Sandbox: "${ZARINPAL_SANDBOX:-true}" ZarinPal__Sandbox: "${ZARINPAL_SANDBOX:-true}"
@@ -275,13 +280,15 @@ services:
container_name: fr2-payment container_name: fr2-payment
restart: unless-stopped restart: unless-stopped
ports: ports:
- "${EDGE_BIND:-0.0.0.0}:${PAY_PORT:-8090}:8080" # Default to the production port 1607 so the bind works without an ENV_FILE
# edit (8090 collided on the server). Override via PAY_PORT if 1607 is taken.
- "${EDGE_BIND:-0.0.0.0}:${PAY_PORT:-1607}:8080"
environment: environment:
DATABASE_URL: "postgres://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:-postgres}@postgres:5432/flatrender?search_path=payment,public" DATABASE_URL: "postgres://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:-postgres}@postgres:5432/flatrender?search_path=payment,public"
JWT_SECRET: "${JWT_SECRET}" JWT_SECRET: "${JWT_SECRET}"
PORT: "8080" PORT: "8080"
# Externally reachable base — ZarinPal callback + user redirect are built from it. # Externally reachable base — ZarinPal callback + user redirect are built from it.
PUBLIC_BASE_URL: "${PAY_PUBLIC_URL:-http://localhost:8090}" PUBLIC_BASE_URL: "${PAY_PUBLIC_URL:-http://localhost:1607}"
# Shared default ZarinPal merchant (a client_app may override per-site). # Shared default ZarinPal merchant (a client_app may override per-site).
ZARINPAL_MERCHANT_ID: "${ZARINPAL_MERCHANT_ID:-}" ZARINPAL_MERCHANT_ID: "${ZARINPAL_MERCHANT_ID:-}"
ZARINPAL_SANDBOX: "${ZARINPAL_SANDBOX:-true}" ZARINPAL_SANDBOX: "${ZARINPAL_SANDBOX:-true}"
@@ -366,7 +373,7 @@ services:
gateway: gateway:
condition: service_healthy condition: service_healthy
healthcheck: healthcheck:
test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:3000 || exit 1"] test: ["CMD", "node", "-e", "require('http').get('http://127.0.0.1:3000',r=>process.exit(r.statusCode<500?0:1)).on('error',()=>process.exit(1))"]
interval: 30s interval: 30s
timeout: 10s timeout: 10s
retries: 3 retries: 3
+60 -6
View File
@@ -144,14 +144,14 @@
"a7": "Yes. Cancel from your account settings at any time. You keep access through the end of your billing period, and you can downgrade to Free without losing your projects." "a7": "Yes. Cancel from your account settings at any time. You keep access through the end of your billing period, and you can downgrade to Free without losing your projects."
}, },
"pricing": { "pricing": {
"heading": "Choose your FlatRender plan", "heading": "Pay by the second, not by the video",
"monthly": "Monthly", "monthly": "Monthly",
"annual": "Annual", "annual": "Annual",
"saveBadge": "Save up to {percent}%", "saveBadge": "Save up to {percent}%",
"subscribe": "Subscribe", "subscribe": "Subscribe",
"freeBannerTitle": "Free plan", "freeBannerTitle": "Free plan",
"freeBannerDesc": "Free forever, no credit card required", "freeBannerDesc": "Free forever, no credit card required",
"perMonth": "/ mo", "perMonth": "monthly",
"billedAnnually": "billed annually", "billedAnnually": "billed annually",
"compareTitle": "Compare all plans", "compareTitle": "Compare all plans",
"allFeatures": "All features", "allFeatures": "All features",
@@ -161,7 +161,45 @@
"proName": "Pro", "proName": "Pro",
"proDesc": "Become a pro and unlock more powerful video, design and website editing tools for commercial use.", "proDesc": "Become a pro and unlock more powerful video, design and website editing tools for commercial use.",
"businessName": "Business", "businessName": "Business",
"businessDesc": "Advanced level solution for teams and businesses. Includes reseller license." "businessDesc": "Advanced level solution for teams and businesses. Includes reseller license.",
"subheading": "Each render costs render-seconds equal to the video length × a quality multiplier. Every plan gives you a monthly bucket of render-seconds.",
"toman": "Toman",
"free": "Free",
"mostPopular": "Most popular",
"currentPlan": "Current plan",
"choosePlan": "Choose plan",
"startFree": "Start free",
"processing": "Redirecting…",
"signInToBuy": "Sign in to buy",
"emptyState": "No plans are available right now.",
"perMonthSuffix": "/ mo",
"featSeconds": "{seconds} render-seconds / month",
"featResolution": "Up to {res} quality",
"featParallelOne": "1 render at a time",
"featParallel": "{n} parallel renders",
"featStorage": "{gb} GB cloud storage",
"featSpeed": "{factor}× render speed",
"featWatermarkOn": "FlatRender watermark",
"featWatermarkOff": "No watermark",
"calcTitle": "How many seconds do I need?",
"calcDesc": "Pick a video length and quality to see the per-render cost in seconds.",
"calcLength": "Video length",
"calcResolution": "Output quality",
"calcCost": "Cost per render",
"calcSecondsUnit": "seconds",
"calcRendersWith": "With each plan:",
"calcVideosFmt": "≈ {count} videos",
"multiplierTitle": "Quality multiplier",
"multiplierDesc": "Render-seconds per render = video length × the multiplier below.",
"multiplierColRes": "Quality",
"multiplierColMul": "Multiplier",
"faqTitle": "Frequently asked",
"faqQ1": "What is a render-second?",
"faqA1": "Instead of a video-count limit, you buy render-seconds. A 15-second video at 720p uses exactly 15 seconds of your balance.",
"faqQ2": "Why does higher quality cost more seconds?",
"faqA2": "4K rendering is much heavier, so each second of video counts as 4 render-seconds; 1080p counts as 2×.",
"faqQ3": "What if I run out of seconds?",
"faqA3": "Upgrade your plan or wait for the next period. Your max resolution and parallel renders also follow your plan."
}, },
"footer": { "footer": {
"brandName": "FlatRender", "brandName": "FlatRender",
@@ -1110,8 +1148,13 @@
"description": "Generate voiceovers from your script directly in the studio." "description": "Generate voiceovers from your script directly in the studio."
}, },
"componentsStudioSidebarColorsCustomTab": { "componentsStudioSidebarColorsCustomTab": {
"mainColor": "Main Color", "mainColor": "Background",
"additionalColor": "Additional Color", "additionalColor": "Accent",
"secondaryColor": "Secondary",
"textColor": "Text",
"themePresets": "Themes",
"applyThemePreset": "Apply {name} theme",
"applyTheme": "Apply theme",
"applyToAllScenes": "Apply to all scenes" "applyToAllScenes": "Apply to all scenes"
}, },
"componentsStudioSidebarColorsPalettesTab": { "componentsStudioSidebarColorsPalettesTab": {
@@ -1146,6 +1189,14 @@
"replaceImage": "Replace image", "replaceImage": "Replace image",
"uploadImage": "Upload image" "uploadImage": "Upload image"
}, },
"componentsStudioSidebarBlockFieldForm": {
"panelTitle": "Edit Scene",
"emptyState": "This scene has no editable fields.",
"fieldFallback": "Field {index}",
"textPlaceholder": "Type here…",
"replaceImage": "Replace image",
"uploadImage": "Upload image"
},
"componentsStudioSidebarTransitionsSidebarContent": { "componentsStudioSidebarTransitionsSidebarContent": {
"heading": "Transitions", "heading": "Transitions",
"randomTransition": "Random Transition", "randomTransition": "Random Transition",
@@ -1186,7 +1237,10 @@
"deleteScene": "Delete {name}", "deleteScene": "Delete {name}",
"resizeSceneDuration": "Resize {name} duration", "resizeSceneDuration": "Resize {name} duration",
"sceneNameLabel": "Scene name", "sceneNameLabel": "Scene name",
"doubleClickToRename": "Double-click to rename" "doubleClickToRename": "Double-click to rename",
"reorderScene": "Reorder {name}",
"durationLabel": "Scene duration (seconds)",
"secondsUnit": "s"
}, },
"componentsStudioTimelineSceneThumbnailStrip": { "componentsStudioTimelineSceneThumbnailStrip": {
"browseScenes": "Browse scenes", "browseScenes": "Browse scenes",
+60 -6
View File
@@ -144,14 +144,14 @@
"a7": "بله. هر زمان از تنظیمات حساب لغو کنید. دسترسی تا پایان دوره صورت‌حساب باقی می‌ماند و می‌توانید به پلن رایگان برگردید بدون اینکه پروژه‌هایتان از دست بروند." "a7": "بله. هر زمان از تنظیمات حساب لغو کنید. دسترسی تا پایان دوره صورت‌حساب باقی می‌ماند و می‌توانید به پلن رایگان برگردید بدون اینکه پروژه‌هایتان از دست بروند."
}, },
"pricing": { "pricing": {
"heading": لن فلت‌رندر خود را انتخاب کنید", "heading": رداخت بر اساس ثانیه، نه تعداد ویدیو",
"monthly": "ماهانه", "monthly": "ماهانه",
"annual": "سالانه", "annual": "سالانه",
"saveBadge": "تا {percent}٪ صرفه‌جویی", "saveBadge": "تا {percent}٪ صرفه‌جویی",
"subscribe": "اشتراک", "subscribe": "اشتراک",
"freeBannerTitle": "پلن رایگان", "freeBannerTitle": "پلن رایگان",
"freeBannerDesc": "برای همیشه رایگان، بدون نیاز به کارت اعتباری", "freeBannerDesc": "برای همیشه رایگان، بدون نیاز به کارت اعتباری",
"perMonth": "/ ماه", "perMonth": "ماهانه",
"billedAnnually": "پرداخت سالانه", "billedAnnually": "پرداخت سالانه",
"compareTitle": "مقایسه همه پلن‌ها", "compareTitle": "مقایسه همه پلن‌ها",
"allFeatures": "همه امکانات", "allFeatures": "همه امکانات",
@@ -161,7 +161,45 @@
"proName": "Pro", "proName": "Pro",
"proDesc": "حرفه‌ای شوید و ابزارهای قدرتمندتر ویدیو، طراحی و وب‌سایت را برای استفاده تجاری باز کنید.", "proDesc": "حرفه‌ای شوید و ابزارهای قدرتمندتر ویدیو، طراحی و وب‌سایت را برای استفاده تجاری باز کنید.",
"businessName": "Business", "businessName": "Business",
"businessDesc": "راه‌حل پیشرفته برای تیم‌ها و کسب‌وکارها. شامل مجوز فروش مجدد." "businessDesc": "راه‌حل پیشرفته برای تیم‌ها و کسب‌وکارها. شامل مجوز فروش مجدد.",
"subheading": "هزینهٔ هر رندر برابر است با طول ویدیو ضربدر ضریب کیفیت. هر پلن ماهانه مقداری «ثانیهٔ رندر» در اختیار شما می‌گذارد.",
"toman": "تومان",
"free": "رایگان",
"mostPopular": "محبوب‌ترین",
"currentPlan": "پلن فعلی",
"choosePlan": "انتخاب پلن",
"startFree": "شروع رایگان",
"processing": "در حال انتقال…",
"signInToBuy": "برای خرید وارد شوید",
"emptyState": "در حال حاضر پلنی برای نمایش وجود ندارد.",
"perMonthSuffix": "/ ماه",
"featSeconds": "{seconds} ثانیهٔ رندر در ماه",
"featResolution": "کیفیت تا {res}",
"featParallelOne": "۱ رندر همزمان",
"featParallel": "{n} رندر همزمان",
"featStorage": "{gb} گیگابایت فضای ابری",
"featSpeed": "سرعت رندر ×{factor}",
"featWatermarkOn": "دارای واترمارک FlatRender",
"featWatermarkOff": "بدون واترمارک",
"calcTitle": "چند ثانیه لازم دارم؟",
"calcDesc": "طول و کیفیت ویدیو را انتخاب کنید تا هزینهٔ ثانیه‌ای هر رندر را ببینید.",
"calcLength": "طول ویدیو",
"calcResolution": "کیفیت خروجی",
"calcCost": "هزینهٔ هر رندر",
"calcSecondsUnit": "ثانیه",
"calcRendersWith": "با هر پلن:",
"calcVideosFmt": "≈ {count} ویدیو",
"multiplierTitle": "ضریب کیفیت",
"multiplierDesc": "ثانیهٔ مصرفی هر رندر = طول ویدیو × ضریب کیفیت زیر.",
"multiplierColRes": "کیفیت",
"multiplierColMul": "ضریب",
"faqTitle": "پرسش‌های پرتکرار",
"faqQ1": "ثانیهٔ رندر یعنی چه؟",
"faqA1": "به‌جای محدودیت تعداد ویدیو، شما مقداری ثانیهٔ رندر می‌خرید. یک ویدیوی ۱۵ ثانیه‌ای با کیفیت ۷۲۰p دقیقاً ۱۵ ثانیه از سهم شما کم می‌کند.",
"faqQ2": "چرا کیفیت بالاتر ثانیهٔ بیشتری می‌برد؟",
"faqA2": "رندر ۴K پردازش سنگین‌تری دارد، بنابراین هر ثانیه ویدیو معادل ۴ ثانیهٔ رندر حساب می‌شود؛ ۱۰۸۰p معادل ۲ برابر.",
"faqQ3": "اگر ثانیه‌هایم تمام شود چه می‌شود؟",
"faqA3": "می‌توانید پلن خود را ارتقا دهید یا تا شروع دورهٔ بعد صبر کنید. سقف کیفیت و رندر همزمان نیز بر اساس پلن شماست."
}, },
"footer": { "footer": {
"brandName": "فلت‌رندر", "brandName": "فلت‌رندر",
@@ -1110,8 +1148,13 @@
"description": "صداگذاری را مستقیماً از روی متن خود در استودیو بسازید." "description": "صداگذاری را مستقیماً از روی متن خود در استودیو بسازید."
}, },
"componentsStudioSidebarColorsCustomTab": { "componentsStudioSidebarColorsCustomTab": {
"mainColor": "رنگ اصلی", "mainColor": "پس‌زمینه",
"additionalColor": "رنگ مکمل", "additionalColor": "رنگ اصلی",
"secondaryColor": "رنگ دوم",
"textColor": "رنگ متن",
"themePresets": "تم‌ها",
"applyThemePreset": "اعمال تم {name}",
"applyTheme": "اعمال تم",
"applyToAllScenes": "اعمال به همه صحنه‌ها" "applyToAllScenes": "اعمال به همه صحنه‌ها"
}, },
"componentsStudioSidebarColorsPalettesTab": { "componentsStudioSidebarColorsPalettesTab": {
@@ -1146,6 +1189,14 @@
"replaceImage": "جایگزینی تصویر", "replaceImage": "جایگزینی تصویر",
"uploadImage": "بارگذاری تصویر" "uploadImage": "بارگذاری تصویر"
}, },
"componentsStudioSidebarBlockFieldForm": {
"panelTitle": "ویرایش صحنه",
"emptyState": "این صحنه فیلد قابل‌ویرایشی ندارد.",
"fieldFallback": "فیلد {index}",
"textPlaceholder": "اینجا بنویسید…",
"replaceImage": "جایگزینی تصویر",
"uploadImage": "بارگذاری تصویر"
},
"componentsStudioSidebarTransitionsSidebarContent": { "componentsStudioSidebarTransitionsSidebarContent": {
"heading": "ترانزیشن‌ها", "heading": "ترانزیشن‌ها",
"randomTransition": "ترانزیشن تصادفی", "randomTransition": "ترانزیشن تصادفی",
@@ -1186,7 +1237,10 @@
"deleteScene": "حذف {name}", "deleteScene": "حذف {name}",
"resizeSceneDuration": "تغییر مدت زمان {name}", "resizeSceneDuration": "تغییر مدت زمان {name}",
"sceneNameLabel": "نام صحنه", "sceneNameLabel": "نام صحنه",
"doubleClickToRename": "برای تغییر نام، دوبار کلیک کنید" "doubleClickToRename": "برای تغییر نام، دوبار کلیک کنید",
"reorderScene": "جابه‌جایی {name}",
"durationLabel": "مدت صحنه (ثانیه)",
"secondsUnit": "ث"
}, },
"componentsStudioTimelineSceneThumbnailStrip": { "componentsStudioTimelineSceneThumbnailStrip": {
"browseScenes": "مرور صحنه‌ها", "browseScenes": "مرور صحنه‌ها",
+92
View File
@@ -36,6 +36,8 @@
"lucide-react": "^1.16.0", "lucide-react": "^1.16.0",
"next": "14.2.35", "next": "14.2.35",
"next-intl": "^4.12.0", "next-intl": "^4.12.0",
"plyr": "^3.7.8",
"plyr-react": "^5.3.0",
"react": "^18", "react": "^18",
"react-dom": "^18", "react-dom": "^18",
"react-hook-form": "^7.76.0", "react-hook-form": "^7.76.0",
@@ -4002,6 +4004,17 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/core-js": {
"version": "3.49.0",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.49.0.tgz",
"integrity": "sha512-es1U2+YTtzpwkxVLwAFdSpaIMyQaq0PBgm3YD1W3Qpsn1NAmO3KSgZfu+oGSWVu6NvLHoHCV/aYcsE5wiB7ALg==",
"hasInstallScript": true,
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/core-js"
}
},
"node_modules/core-util-is": { "node_modules/core-util-is": {
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
@@ -4042,6 +4055,12 @@
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/custom-event-polyfill": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/custom-event-polyfill/-/custom-event-polyfill-1.0.7.tgz",
"integrity": "sha512-TDDkd5DkaZxZFM8p+1I3yAlvM3rSr1wbrOliG4yJiwinMZN8z/iGL7BTlDkrJcYTmgUSb4ywVCc3ZaUtOtC76w==",
"license": "MIT"
},
"node_modules/damerau-levenshtein": { "node_modules/damerau-levenshtein": {
"version": "1.0.8", "version": "1.0.8",
"resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz",
@@ -6270,6 +6289,12 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/loadjs": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/loadjs/-/loadjs-4.3.0.tgz",
"integrity": "sha512-vNX4ZZLJBeDEOBvdr2v/F+0aN5oMuPu7JTqrMwp+DtgK+AryOlpy6Xtm2/HpNr+azEa828oQjOtWsB6iDtSfSQ==",
"license": "MIT"
},
"node_modules/locate-path": { "node_modules/locate-path": {
"version": "6.0.0", "version": "6.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
@@ -7005,6 +7030,44 @@
"node": ">= 6" "node": ">= 6"
} }
}, },
"node_modules/plyr": {
"version": "3.7.8",
"resolved": "https://registry.npmjs.org/plyr/-/plyr-3.7.8.tgz",
"integrity": "sha512-yG/EHDobwbB/uP+4Bm6eUpJ93f8xxHjjk2dYcD1Oqpe1EcuQl5tzzw9Oq+uVAzd2lkM11qZfydSiyIpiB8pgdA==",
"license": "MIT",
"dependencies": {
"core-js": "^3.26.1",
"custom-event-polyfill": "^1.0.7",
"loadjs": "^4.2.0",
"rangetouch": "^2.0.1",
"url-polyfill": "^1.1.12"
}
},
"node_modules/plyr-react": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/plyr-react/-/plyr-react-5.3.0.tgz",
"integrity": "sha512-m36/HrpHwg1N2rq3E31E8/kpAH55vk6qHUg17MG4uu9jbWYxnkN39lLmZQwxW7/qpDPfW5aGUJ6R3u23V0R3zA==",
"license": "MIT",
"dependencies": {
"plyr": "^3.7.7",
"react-aptor": "^2.0.0"
},
"engines": {
"node": ">=16"
},
"peerDependencies": {
"plyr": "^3.7.7",
"react": ">=16.8"
},
"peerDependenciesMeta": {
"plyr": {
"optional": false
},
"react": {
"optional": true
}
}
},
"node_modules/po-parser": { "node_modules/po-parser": {
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/po-parser/-/po-parser-2.1.1.tgz", "resolved": "https://registry.npmjs.org/po-parser/-/po-parser-2.1.1.tgz",
@@ -7264,6 +7327,12 @@
], ],
"license": "MIT" "license": "MIT"
}, },
"node_modules/rangetouch": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/rangetouch/-/rangetouch-2.0.1.tgz",
"integrity": "sha512-sln+pNSc8NGaHoLzwNBssFSf/rSYkqeBXzX1AtJlkJiUaVSJSbRAWJk+4omsXkN+EJalzkZhWQ3th1m0FpR5xA==",
"license": "MIT"
},
"node_modules/re-resizable": { "node_modules/re-resizable": {
"version": "6.11.2", "version": "6.11.2",
"resolved": "https://registry.npmjs.org/re-resizable/-/re-resizable-6.11.2.tgz", "resolved": "https://registry.npmjs.org/re-resizable/-/re-resizable-6.11.2.tgz",
@@ -7286,6 +7355,23 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/react-aptor": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/react-aptor/-/react-aptor-2.0.0.tgz",
"integrity": "sha512-YnCayokuhAwmBBP4Oc0bbT2l6ApfsjbY3DEEVUddIKZEBlGl1npzjHHzWnSqWuboSbMZvRqUM01Io9yiIp1wcg==",
"license": "MIT",
"engines": {
"node": ">=12.7.0"
},
"peerDependencies": {
"react": ">=16.8"
},
"peerDependenciesMeta": {
"react": {
"optional": true
}
}
},
"node_modules/react-date-object": { "node_modules/react-date-object": {
"version": "2.1.9", "version": "2.1.9",
"resolved": "https://registry.npmjs.org/react-date-object/-/react-date-object-2.1.9.tgz", "resolved": "https://registry.npmjs.org/react-date-object/-/react-date-object-2.1.9.tgz",
@@ -8698,6 +8784,12 @@
"punycode": "^2.1.0" "punycode": "^2.1.0"
} }
}, },
"node_modules/url-polyfill": {
"version": "1.1.14",
"resolved": "https://registry.npmjs.org/url-polyfill/-/url-polyfill-1.1.14.tgz",
"integrity": "sha512-p4f3TTAG6ADVF3mwbXw7hGw+QJyw5CnNGvYh5fCuQQZIiuKUswqcznyV3pGDP9j0TSmC4UvRKm8kl1QsX1diiQ==",
"license": "MIT"
},
"node_modules/use-callback-ref": { "node_modules/use-callback-ref": {
"version": "1.3.3", "version": "1.3.3",
"resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz",
+2
View File
@@ -37,6 +37,8 @@
"lucide-react": "^1.16.0", "lucide-react": "^1.16.0",
"next": "14.2.35", "next": "14.2.35",
"next-intl": "^4.12.0", "next-intl": "^4.12.0",
"plyr": "^3.7.8",
"plyr-react": "^5.3.0",
"react": "^18", "react": "^18",
"react-dom": "^18", "react-dom": "^18",
"react-hook-form": "^7.76.0", "react-hook-form": "^7.76.0",
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 384 KiB

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 225 KiB

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 498 KiB

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 816 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 962 KiB

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 660 KiB

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 889 KiB

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Some files were not shown because too many files have changed in this diff Show More