Files
flatrender/services/remotion/src/scenes/blocks/Slideshow.tsx
T
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

62 lines
3.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React from "react";
import { AbsoluteFill, interpolate, spring, useCurrentFrame, useVideoConfig } from "remotion";
import { FONT } from "../../lib/fonts";
import { hexToRgba } from "../../lib/anim";
import { ThreeBackdrop, Grain, Vignette, ProgressDots, Kicker, useSceneTransition } from "../chrome";
import type { BlockProps, SceneBlock } from "../types";
const Slideshow: React.FC<BlockProps> = ({ data, colors, L, index, total, durationInFrames }) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const { opacity, slide } = useSceneTransition(durationInFrames, L);
const items = [data.slide1, data.slide2, data.slide3, data.slide4].filter((s) => s && s.trim());
// distribute reveals across the available time (after the title settles)
const start = 14;
const span = Math.max(18, (durationInFrames - start - 14) / Math.max(1, items.length));
return (
<AbsoluteFill style={{ opacity, fontFamily: FONT, backgroundColor: colors.backgroundColor }}>
<ThreeBackdrop colors={colors} />
<AbsoluteFill style={{ display: "flex", alignItems: L.isWide ? "flex-start" : "center", justifyContent: "center", flexDirection: "column", padding: L.pick(L.vmin(120), L.vmin(80), L.vmin(70)) }}>
<Kicker index={index} total={total} colors={colors} L={L} slide={slide} />
<div style={{ direction: "rtl", textAlign: L.isWide ? "right" : "center", transform: `translateX(${slide}px)`, fontWeight: 800, fontSize: L.pick(L.vmin(72), L.vmin(64), L.vmin(60)), color: colors.textColor, lineHeight: 1.15, letterSpacing: -0.5, marginBottom: L.vmin(34) }}>
{data.title}
</div>
<div style={{ display: "flex", flexDirection: "column", gap: L.vmin(16), width: "100%", alignItems: L.isWide ? "flex-start" : "center" }}>
{items.map((s, i) => {
const sp = spring({ frame: frame - (start + i * span), fps, config: { damping: 18, stiffness: 110 } });
const x = interpolate(sp, [0, 1], [L.vmin(50), 0]);
return (
<div key={i} style={{ direction: "rtl", opacity: sp, transform: `translateX(${x}px)`, display: "flex", alignItems: "center", gap: L.vmin(16), borderRadius: L.vmin(18), background: hexToRgba(colors.textColor, 0.04), border: `1px solid ${hexToRgba(colors.textColor, 0.08)}`, padding: `${L.vmin(18)}px ${L.vmin(26)}px`, maxWidth: L.vmin(1000) }}>
<span style={{ flexShrink: 0, width: L.vmin(40), height: L.vmin(40), borderRadius: 999, background: colors.accentColor, color: "#fff", display: "flex", alignItems: "center", justifyContent: "center", fontWeight: 800, fontSize: L.vmin(22) }}>
{String(i + 1).replace(/[0-9]/g, (d) => "۰۱۲۳۴۵۶۷۸۹"[+d])}
</span>
<span style={{ fontWeight: 600, fontSize: L.pick(L.vmin(34), L.vmin(32), L.vmin(30)), color: hexToRgba(colors.textColor, 0.84) }}>{s}</span>
</div>
);
})}
</div>
</AbsoluteFill>
<ProgressDots index={index} total={total} colors={colors} L={L} />
<Vignette />
<Grain />
</AbsoluteFill>
);
};
export const SlideshowBlock: SceneBlock = {
id: "Slideshow",
label: "اسلایدشو (فهرست)",
component: Slideshow,
fields: [
{ key: "title", label: "عنوان", type: "text", default: "چرا فلت‌رندر؟" },
{ key: "slide1", label: "مورد ۱", type: "text", default: "ساخت ویدیو در چند دقیقه" },
{ key: "slide2", label: "مورد ۲", type: "text", default: "بدون نیاز به دانش فنی" },
{ key: "slide3", label: "مورد ۳", type: "text", default: "خروجی با کیفیت حرفه‌ای" },
{ key: "slide4", label: "مورد ۴", type: "text", default: "" },
],
defaultDurationSec: 6,
minDurationSec: 3,
maxDurationSec: 12,
};