diff --git a/services/remotion/docs/CATALOG_PLAN.md b/services/remotion/docs/CATALOG_PLAN.md new file mode 100644 index 0000000..b41ae7e --- /dev/null +++ b/services/remotion/docs/CATALOG_PLAN.md @@ -0,0 +1,66 @@ +# FlatRender — Template Catalog Plan + +The categories to cover, grouped, each mapped to **how it's produced** (engine · +style · assets) and **status**. Built on the FlexStory scene engine + block library +([[project_scene_engine]]), the 3D single-comp templates, and the premium toolchain +([[reference_premium_toolchain]]). Status: ✅ have · 🟡 partial (have a block/3D base) · ⬜ new. + +## 1. Marketing / Promo — *engine: scene-engine + 3D · style: 2.5D / 3D cinematic* +| Category | Approach | Status | +|---|---|---| +| Logo motion | 3D metallic + rays (single-comp) | ✅ LogoMotion3D | +| App / website promo | device mockup + 2.5D | 🟡 AppShowcase3D, ProductShowcase | +| Instagram promo | 2.5D + CTA | 🟡 InstaPromo | +| YouTube promo / channel intro | 2.5D + subscribe CTA | 🟡 YouTubeIntro | +| Sale / offer | 3D gift/box + badge | 🟡 Promo3D, SalePromo | +| Real estate | image showcase + map/stats | ⬜ new (ImageCaption + StatCounter) | +| Fashion | kinetic + image reveal | ⬜ new (Stomp + ImageCaption) | + +## 2. Persian / Islamic occasions — ⭐ **THE MOAT** — *self-authored art; Western tools can't do these authentically* +| Category | Notes | Status | +|---|---|---| +| Nowruz (نوروز) | haft-sin, sabzeh, goldfish | ✅ Nowruz3D, NowruzGreeting | +| Yalda (شب یلدا) | pomegranate, watermelon, Hafez | ⬜ new | +| Ramadan (رمضان) | crescent, fanoos/lantern | ⬜ new | +| Eid (عید فطر/قربان) | celebratory, ornamental | ⬜ new | +| Muharram (محرم) | somber, respectful, calligraphy | ⬜ new | +| Condolences (تسلیت) | minimal, dignified | ⬜ new | +| Wedding (عروسی) | elegant, gold, floral | ⬜ new | +| Birthday (تولد) | confetti, cake | ✅ Birthday3D, HappyBirthday | +> This group is the **differentiation** — Persian-first, culturally authentic, low competition. Highest strategic ROI. Self-authored seasonal art (the discipline we already use for Nowruz), no licensing/Iran blockers. + +## 3. Presentation / Content — *engine: FlexStory scene-engine (already built)* +| Category | Approach | Status | +|---|---|---| +| Presentations / openers | TitleCard + scene list | 🟡 Opener, TitleCard | +| Slideshows | Slideshow block | ✅ | +| Infographics | StatCounter + chart blocks | 🟡 (StatCounter; need Chart/Bar/Pie blocks) | +| Explainers | CharacterScene + scene list | ✅ CharacterStory | +| Toolkits | a *set* of reusable blocks/elements | ✅ the scene engine **is** this | + +## 4. Titles / Typography — *style: kinetic type, beat-synced* +| Category | Approach | Status | +|---|---|---| +| Titles / lower-thirds | kinetic text block | 🟡 KineticQuote | +| Stomp | fast beat-synced typography to music | ⬜ new (Stomp block) | + +## 5. Characters & Animation — *vendored CC0 art + 2.5D + craft* +| Category | Approach | Status | +|---|---|---| +| Characters & animation | CC0 Open-Peeps + 2.5D SceneStage | ✅ CharacterScene | +| Stop motion | craft kit: frame-step + jitter + grain | ⬜ new (toolchain P1) · or Wan 2.2 LoRA | +| Paper-cut | layered planes + shadows + paper PBR | ⬜ new (toolchain) | + +--- + +## What this tells us +- **~60% is assembly on the existing scene engine** — promo, slideshow, openers, explainers, infographics, titles are *block compositions*, not new code. Build the missing blocks (Chart, Stomp, LowerThird) and most of §1/§3/§4 falls out. +- **The Persian/Islamic pack (§2) is the moat** — self-authored, no competitor, no licensing/Iran issue. Invest here for differentiation. +- **Stop-motion/paper-cut/premium (§5)** = the toolchain free-craft tier first, AI tier later. + +## Build waves +- **Wave 1 — Assemble from existing:** ship promo / slideshow / opener / explainer / infographic templates by composing current blocks + a few new blocks (Chart, Stomp, LowerThird, device mockup). Fast, leverages what's built. +- **Wave 2 — The Persian moat:** Yalda · Ramadan · Eid · Muharram · Wedding · Condolences seasonal templates (self-authored art, reuse Nowruz3D learnings). The differentiator. +- **Wave 3 — Craft/premium tier:** the free craft kit (frame-step, paper-cut planes, FinishPass, Lottie) → stop-motion + paper-cut + premium templates. Then self-host AI (Wan/FLUX). + +**Each template still goes through the flow:** brief → ★ quality-gate (cheap still, your approval) ★ → build → per-scene previews → seed → deploy. diff --git a/services/remotion/docs/PREMIUM_TOOLCHAIN.md b/services/remotion/docs/PREMIUM_TOOLCHAIN.md new file mode 100644 index 0000000..d7c30e5 --- /dev/null +++ b/services/remotion/docs/PREMIUM_TOOLCHAIN.md @@ -0,0 +1,72 @@ +# FlatRender — Premium Template Toolchain (stop-motion · paper-cut · premium) + +From the `premium-video-toolchain` research sweep (6 finders). Goal: raise template +quality beyond hand-coded art, for a **Persian-first paid SaaS** that renders +**editable** templates to MP4 server-side, on **Remotion + After Effects** engines. + +## The diagnosis +The quality cap isn't the engines — it's the AI **hand-authoring the art in code** at +render time. Clean vector motion is what code does well; stop-motion / paper-cut live +on **texture, handmade imperfection, real shadows, layered materials** — artist/asset +driven. Fix = **change where the artistry comes from**, keep the editable layer in code. + +## The load-bearing constraint: Iran access is the dominant filter (not quality) +Every frontier AI tool is **OFAC-blocked at signup AND payment** — US (Runway, Sora, +Veo, Luma, Pika, Kaiber) *and* Chinese (Kling, Seedance). The aggregators (fal.ai, +Replicate) are US/Stripe-billed → same wall. There is **no direct payable-from-Iran +path to any hosted model.** Two viable routes: +- **Self-host open weights** → zero OFAC exposure (nothing paid to a US co. at render). The ONLY thing that can sit in the **live, on-demand server render**. +- **Acquire-once via a non-Iran intermediary** → batch-generate clips → **vendor the MP4** under the existing `assets.json` licence firewall (the *output* licence is perpetual; the *service* access is not). Exactly the asset/audio-library pattern. + +## The architecture (this is the whole answer) +An AI clip is **never the template** — it's **moving wallpaper**. Three layers: +``` +L1 BACKDROP baked AI clip / textured render / Lottie (no editable text) +L2 EDITABLE text · logo · colours · images → Remotion inputProps / AE bind.jsx +L3 RENDER composite → MP4 (Remotion OffthreadVideo or Three VideoTexture; AE footage layer) +``` +Rules: bake backdrops at the comp's fps/aspect; keep them **dark/low-contrast/abstract** so editable type stays legible; **colour lives in the overlay, not the AI footage**. This is how premium + editability coexist — and it maps onto our two engines directly. + +## Per style — the recommended approach +| Style | Approach | Tools | +|---|---|---| +| **Stop-motion** | Frame-step ("on twos" / posterizeTime / reduced fps) + per-frame jitter + paper/grain overlay. Pika's first/last-frame keying is "stop-motion-native". | **Remotion code (free)** · Blender (grease pencil / sim) · Pika (acquire-once) · Wan 2.2 LoRA | +| **Paper-cut** | Layered Z-planes + **real cast shadows** + depth in @remotion/three; CC0 paper **normal/displacement maps**; SVG feTurbulence for organic edges. | **@remotion/three (free)** · ambientCG / Poly Haven (CC0) · Recraft (paper-cut image gen) | +| **Premium motion** | A shared **finish pass** (film grain + DOF + bloom + **LUT colour grade**) + **HDRI lighting** + artist **Lottie** + custom illustration. | **@react-three/postprocessing (free)** · Poly Haven HDRI · **@remotion/lottie** · FLUX+LoRA | + +## The tool stack (priority order) +**Free + Iran-safe (do first — biggest ROI, zero licensing/OFAC risk, in our engine):** +1. **`@remotion/lottie`** + **`@remotion/paths`** — artist-made Lottie animations + path morphing. (lottie still NOT installed — flagged repeatedly.) +2. **Craft primitives** — a reusable kit: frame-stepping, jitter, paper-cut planes+shadows, grain. +3. **`@react-three/postprocessing` finish pass** — grain/DOF/bloom/vignette + LUT. One shared component lifts *everything*. +4. **CC0 PBR textures + HDRI** — ambientCG, Poly Haven (paper, cardboard, fabric, light leaks, lighting). + +**Self-host (Iran-safe, GPU needed — the live-render upgrade):** +5. **FLUX + LoRA** (image) / **SDXL** — generate style-locked per-scene illustration; LoRA locks a paper-cut/clay/brand style. Permissive licences, runs offline. +6. **Wan 2.2** (Alibaba, **Apache-2.0**, video, ~RTX 4090) — **the only video model that can be in the live pipeline from Iran**; I2V from your own style still; LoRA-trainable. +7. **Blender** (free) — grease pencil 2D, paper-cut rigs, stop-motion sims; render → backdrop. + +**Acquire-once via non-Iran intermediary (premium hero clips only, vendor the MP4):** +8. **Kling** (best stylized I2V from a still) · **Seedance 2.0** (best multi-ref consistency — up to 9 refs) · **Pika** (keyframe stop-motion). Paid plans grant commercial use; vendor outputs + ledger them. (Kling retains a backdoor licence to your generated content — fine for backdrops.) + +**Reference-only / blocked:** Adobe Firefly, OpenAI/Sora (OFAC + payment); Envato/Storyblocks AE packs (redistribution/competing-service clauses + payment). + +## Consistency lever (critical for multi-scene) +**Image-to-video + reference images**, not text-to-video — animate *your* style-locked still so the model never reinvents the look. **LoRA** training locks a character/style across scenes. Seed every scene from the same reference. + +## Trends to ride (2025–2026) +Craft revival (stop-motion/paper-cut **as luxury**) · the **baked-backdrop + editable-overlay** pattern · **I2V + reference** for consistency · **self-hosted open weights** (sovereignty + Iran) · **LoRA + seed** style-lock · a **centralized finish pass**. + +## Phased plan +- **P0** — install `@remotion/lottie` + `@remotion/paths`. (free, immediate) +- **P1** — build the **craft-primitives kit** (frame-step, jitter, paper-cut planes, grain). (free) +- **P2** — a shared **FinishPass** (postprocessing + LUT). (free) +- **P3** — vendor **CC0 PBR textures + HDRI** (ambientCG / Poly Haven). (free, VPN fetch) +- **P4** — curate + vendor **Lottie / asset packs** behind the licence gate. +- **P5** — stand up **FLUX + LoRA** self-host for style-locked illustration. (GPU) +- **P6** — the **backdrop architecture** end-to-end (AI/asset clip → editable AE/Remotion overlay → render cache). +- **P7** — self-host **Wan 2.2** (video) + **Blender** (stop-motion/paper-cut sims). (GPU) + +**Bottom line:** P0–P3 are free, Iran-safe, in our existing Remotion engine, and raise +quality immediately. The AI tier (P5–P7) is a **self-hosted-weights** play because of +OFAC — Wan 2.2 + FLUX are the sovereignty path; hosted models are acquire-once-and-vendor. diff --git a/services/remotion/src/scenes/blocks/BarChart.tsx b/services/remotion/src/scenes/blocks/BarChart.tsx new file mode 100644 index 0000000..6e8ffa7 --- /dev/null +++ b/services/remotion/src/scenes/blocks/BarChart.tsx @@ -0,0 +1,81 @@ +import React from "react"; +import { AbsoluteFill, interpolate, spring, useCurrentFrame, useVideoConfig } from "remotion"; +import { FONT } from "../../lib/fonts"; +import { hexToRgba, mixHex } from "../../lib/anim"; +import { ThreeBackdrop, Grain, Vignette, ProgressDots, Kicker, useSceneTransition } from "../chrome"; +import type { BlockProps, SceneBlock } from "../types"; + +const faNum = (s: string) => s.replace(/[0-9]/g, (d) => "۰۱۲۳۴۵۶۷۸۹"[+d]); + +const BarChart: React.FC = ({ data, colors, L, index, total, durationInFrames }) => { + const frame = useCurrentFrame(); + const { fps } = useVideoConfig(); + const { opacity, slide } = useSceneTransition(durationInFrames, L); + + const bars = [1, 2, 3, 4, 5] + .map((i) => ({ label: data[`bar${i}Label`], value: parseFloat(data[`bar${i}Value`] || "") })) + .filter((b) => b.label && b.label.trim() && !Number.isNaN(b.value)); + const max = Math.max(1, ...bars.map((b) => b.value)); + + const chartH = L.pick(L.vmin(440), L.vmin(420), L.vmin(420)); + const barW = L.pick(L.vmin(110), L.vmin(100), L.vmin(92)); + const gap = L.vmin(36); + + return ( + + + +
+ +
+
+ {data.title} +
+
+ {bars.map((b, i) => { + const sp = spring({ frame: frame - 12 - i * 6, fps, config: { damping: 18, stiffness: 80 } }); + const h = interpolate(sp, [0, 1], [0, (b.value / max) * chartH * 0.86]); + const valOp = interpolate(sp, [0.6, 1], [0, 1], { extrapolateLeft: "clamp" }); + const fill = i % 2 ? colors.secondaryColor : colors.accentColor; + return ( +
+
+ {faNum(String(b.value))} +
+
+
+ {b.label} +
+
+ ); + })} +
+ + + + + + ); +}; + +export const BarChartBlock: SceneBlock = { + id: "BarChart", + label: "نمودار میله‌ای (اینفوگرافیک)", + component: BarChart, + fields: [ + { key: "title", label: "عنوان", type: "text", default: "رشد ما در یک نگاه" }, + { key: "bar1Label", label: "میله ۱ — برچسب", type: "text", default: "۱۴۰۲" }, + { key: "bar1Value", label: "میله ۱ — مقدار", type: "text", default: "45" }, + { key: "bar2Label", label: "میله ۲ — برچسب", type: "text", default: "۱۴۰۳" }, + { key: "bar2Value", label: "میله ۲ — مقدار", type: "text", default: "72" }, + { key: "bar3Label", label: "میله ۳ — برچسب", type: "text", default: "۱۴۰۴" }, + { key: "bar3Value", label: "میله ۳ — مقدار", type: "text", default: "98" }, + { key: "bar4Label", label: "میله ۴ — برچسب", type: "text", default: "" }, + { key: "bar4Value", label: "میله ۴ — مقدار", type: "text", default: "" }, + { key: "bar5Label", label: "میله ۵ — برچسب", type: "text", default: "" }, + { key: "bar5Value", label: "میله ۵ — مقدار", type: "text", default: "" }, + ], + defaultDurationSec: 5, + minDurationSec: 3, + maxDurationSec: 9, +}; diff --git a/services/remotion/src/scenes/blocks/DeviceMockup.tsx b/services/remotion/src/scenes/blocks/DeviceMockup.tsx new file mode 100644 index 0000000..8ea5f9b --- /dev/null +++ b/services/remotion/src/scenes/blocks/DeviceMockup.tsx @@ -0,0 +1,89 @@ +import React from "react"; +import { AbsoluteFill, Img, interpolate, spring, staticFile, useCurrentFrame, useVideoConfig } from "remotion"; +import { FONT } from "../../lib/fonts"; +import { hexToRgba, mixHex } from "../../lib/anim"; +import { ThreeBackdrop, Grain, Vignette, ProgressDots, Kicker, useSceneTransition } from "../chrome"; +import type { BlockProps, SceneBlock } from "../types"; + +const resolveSrc = (u: string) => (/^https?:\/\//.test(u) ? u : staticFile(u)); + +const DeviceMockup: React.FC = ({ data, colors, L, index, total, durationInFrames }) => { + const frame = useCurrentFrame(); + const { fps } = useVideoConfig(); + const { opacity, slide } = useSceneTransition(durationInFrames, L); + const sp = spring({ frame: frame - 2, fps, config: { damping: 18, stiffness: 90 } }); + const deviceScale = interpolate(sp, [0, 1], [0.86, 1]); + const float = Math.sin(frame / 26) * L.vmin(8); + const textSp = spring({ frame: frame - 10, fps, config: { damping: 18, stiffness: 110 } }); + const textX = interpolate(textSp, [0, 1], [L.vmin(40), 0]); + const isPhone = (data.device || "phone").toLowerCase() !== "browser"; + const src = data.imageUrl ? resolveSrc(data.imageUrl) : null; + const bezel = "#1b1f2e"; + + const Placeholder = ( +
+ {isPhone ? "📱" : "🖥"} +
+ ); + + const phoneW = L.pick(L.vmin(330), L.vmin(300), L.vmin(360)); + const Phone = ( +
+
+ {src ? : Placeholder} +
+
+
+ ); + + const browserW = L.pick(L.vmin(820), L.vmin(720), L.vmin(760)); + const Browser = ( +
+
+ {["#ff5f57", "#febc2e", "#28c840"].map((c) => ( + + ))} +
+
+
+ {src ? : Placeholder} +
+
+ ); + + const Text = ( +
+ +
{data.title}
+
{data.caption}
+
+ ); + + return ( + + + + {isPhone ? Phone : Browser} + {Text} + + + + + + ); +}; + +export const DeviceMockupBlock: SceneBlock = { + id: "DeviceMockup", + label: "نمایش روی دستگاه (اپ/وب)", + component: DeviceMockup, + fields: [ + { key: "imageUrl", label: "تصویر صفحه (اسکرین‌شات)", type: "image", default: "" }, + { key: "device", label: "دستگاه (phone/browser)", type: "text", default: "phone" }, + { key: "title", label: "عنوان", type: "text", default: "اپلیکیشن شما" }, + { key: "caption", label: "توضیح", type: "text", default: "تجربه‌ای روان، سریع و زیبا روی هر دستگاه", multiline: true }, + ], + defaultDurationSec: 5, + minDurationSec: 3, + maxDurationSec: 9, +}; diff --git a/services/remotion/src/scenes/blocks/Stomp.tsx b/services/remotion/src/scenes/blocks/Stomp.tsx new file mode 100644 index 0000000..3c312b5 --- /dev/null +++ b/services/remotion/src/scenes/blocks/Stomp.tsx @@ -0,0 +1,73 @@ +import React from "react"; +import { AbsoluteFill, interpolate, spring, useCurrentFrame, useVideoConfig } from "remotion"; +import { FONT } from "../../lib/fonts"; +import { mixHex, rand } from "../../lib/anim"; +import { Grain, Vignette, useSceneTransition } from "../chrome"; +import type { BlockProps, SceneBlock } from "../types"; + +// Stomp = punchy beat-synced typography: each word slams in (scale overshoot + +// shake), filling the frame, alternating accent/bg flashes. High-energy opener. +const Stomp: React.FC = ({ data, colors, L, durationInFrames }) => { + const frame = useCurrentFrame(); + const { fps } = useVideoConfig(); + const { opacity } = useSceneTransition(durationInFrames, L); + + const words = [data.line1, data.line2, data.line3, data.line4].filter((w) => w && w.trim()); + const n = Math.max(1, words.length); + const slot = durationInFrames / n; + const idx = Math.min(n - 1, Math.floor(frame / slot)); + const local = frame - idx * slot; + + // stomp impact at each slot start + const sp = spring({ frame: local, fps, config: { damping: 9, stiffness: 220 } }); + const scale = interpolate(sp, [0, 1], [1.5, 1]); + const shake = local < 8 ? (rand(idx * 7 + local) - 0.5) * L.vmin(14) * (1 - local / 8) : 0; + const flash = interpolate(local, [0, 5], [1, 0], { extrapolateRight: "clamp" }); + const tilt = idx % 2 ? -3 : 3; + const bg = mixHex(colors.backgroundColor, "#000000", 0.25); + + return ( + + {/* impact flash */} + + {/* impact bars */} + +
+ + +
+ {words[idx]} +
+
+ + + + ); +}; + +export const StompBlock: SceneBlock = { + id: "Stomp", + label: "استامپ (تایپوگرافی ضربه‌ای)", + component: Stomp, + fields: [ + { key: "line1", label: "کلمه ۱", type: "text", default: "جسور" }, + { key: "line2", label: "کلمه ۲", type: "text", default: "سریع" }, + { key: "line3", label: "کلمه ۳", type: "text", default: "حرفه‌ای" }, + { key: "line4", label: "کلمه ۴", type: "text", default: "" }, + ], + defaultDurationSec: 4, + minDurationSec: 2, + maxDurationSec: 7, +}; diff --git a/services/remotion/src/scenes/registry.ts b/services/remotion/src/scenes/registry.ts index a0c9779..37b18f4 100644 --- a/services/remotion/src/scenes/registry.ts +++ b/services/remotion/src/scenes/registry.ts @@ -7,6 +7,9 @@ import { SlideshowBlock } from "./blocks/Slideshow"; import { OutroCTABlock } from "./blocks/OutroCTA"; import { LogoRevealBlock } from "./blocks/LogoReveal"; import { StatCounterBlock } from "./blocks/StatCounter"; +import { BarChartBlock } from "./blocks/BarChart"; +import { StompBlock } from "./blocks/Stomp"; +import { DeviceMockupBlock } from "./blocks/DeviceMockup"; import { ProductShowcaseBlock } from "./blocks/ProductShowcase"; /** @@ -24,6 +27,9 @@ export const SCENE_BLOCKS: Record = { [LogoRevealBlock.id]: LogoRevealBlock, [StatCounterBlock.id]: StatCounterBlock, [ProductShowcaseBlock.id]: ProductShowcaseBlock, + [BarChartBlock.id]: BarChartBlock, + [StompBlock.id]: StompBlock, + [DeviceMockupBlock.id]: DeviceMockupBlock, }; export const BLOCK_LIST = Object.values(SCENE_BLOCKS);