import React from "react"; import { AbsoluteFill, Audio, Sequence, staticFile, useVideoConfig } from "remotion"; import { z } from "zod"; import { colorSchema } from "../lib/branding"; import { FONT } from "../lib/fonts"; import { useLayout } from "../lib/aspect"; import { getBlock } from "../scenes/registry"; import { withDefaults, clampDuration } from "../scenes/types"; import { FinishPass, GRADE_FILTER } from "../scenes/chrome"; /** * FlexStory — the scene sequencer. A template is `scenes: SceneInstance[]`; this * composition stacks each block in a at its own (clamped) duration and * computes the total length dynamically via calculateMetadata. This is the engine * that turns add/duplicate/delete/reorder + per-scene duration into a real render. */ export const flexStorySchema = z.object({ scenes: z.array( z.object({ blockId: z.string(), durationSec: z.number(), props: z.record(z.string()), }) ), // Audio (optional so the existing render binding doesn't need to send them). music: z.string().optional(), // path/url of the music bed; "" = silent musicVolume: z.number().optional(), sfx: z.boolean().optional(), // transition whoosh + outro chime ...colorSchema, }); type Props = z.infer; const FPS = 30; const resolveAudio = (u: string) => (/^https?:\/\//.test(u) ? u : staticFile(u)); export const flexStoryDefaults: Props = { scenes: [ { blockId: "TitleCard", durationSec: 4, props: { kicker: "فلت‌رندر", title: "موتور صحنه‌ای", subtitle: "هر قالب، فهرستی از صحنه‌های قابل‌ویرایش است" } }, { blockId: "CharacterScene", durationSec: 3, props: { title: "یک ایده", caption: "همه‌چیز با یک جرقهٔ کوچک شروع شد", character: "illustrations/dicebear/openpeeps-04.svg", prop: "cup" } }, { blockId: "ImageCaption", durationSec: 4, props: { title: "نمایش تصویر", caption: "تصویر یا اسکرین‌شات خود را اینجا قرار دهید", imageUrl: "" } }, { blockId: "KineticQuote", durationSec: 5, props: { quote: "ساختن ویدیوی حرفه‌ای دیگر سخت نیست.", author: "فلت‌رندر" } }, { blockId: "Slideshow", durationSec: 6, props: { title: "چرا فلت‌رندر؟", slide1: "سریع", slide2: "ارزان", slide3: "حرفه‌ای", slide4: "" } }, { blockId: "OutroCTA", durationSec: 4, props: { brandText: "فلت‌رندر", tagline: "همین حالا داستان خود را بسازید", cta: "شروع کنید" } }, ], accentColor: "#cf8a76", secondaryColor: "#6f9d96", backgroundColor: "#ece4d6", textColor: "#2b3a55", music: "audio/music-ambient.mp3", musicVolume: 0.6, sfx: true, }; const activeScenes = (props: Props) => (props.scenes?.length ? props.scenes : flexStoryDefaults.scenes).filter((s) => getBlock(s.blockId)); export const FlexStory: React.FC = (props) => { const { fps } = useVideoConfig(); const L = useLayout(); const colors = { accentColor: props.accentColor, secondaryColor: props.secondaryColor, backgroundColor: props.backgroundColor, textColor: props.textColor, }; const scenes = activeScenes(props); const music = props.music === undefined ? "audio/music-ambient.mp3" : props.music; const musicVolume = props.musicVolume ?? 0.6; const sfx = props.sfx ?? true; // Precompute each scene's start frame + duration (shared by visuals + SFX). const starts: number[] = []; let acc = 0; const durations = scenes.map((sc) => { const dur = Math.round(clampDuration(sc.durationSec, getBlock(sc.blockId)!) * fps); starts.push(acc); acc += dur; return dur; }); return ( {music ? ); }; /** Composition length = Σ per-scene durations (so add/delete/duration all flow). */ export const calcFlexStoryMetadata = ({ props }: { props: Props }) => { const total = activeScenes(props).reduce((acc, s) => { const b = getBlock(s.blockId)!; return acc + Math.round(clampDuration(s.durationSec, b) * FPS); }, 0); return { durationInFrames: Math.max(1, total) }; };