diff --git a/src/remotion/ShortsVideo.tsx b/src/remotion/ShortsVideo.tsx index 9cd7368..53f4421 100644 --- a/src/remotion/ShortsVideo.tsx +++ b/src/remotion/ShortsVideo.tsx @@ -13,6 +13,10 @@ import { CompareScene, BulletScene, BangScene, + QuoteScene, + PodiumScene, + GridListScene, + ScoreboardScene, MiniKaraoke, } from "./parts"; import { TweetCard, ThreadCard, NewsCard } from "./cards"; @@ -147,7 +151,7 @@ export type Segment = { label?: string; sentences?: Sentence[]; /** optional explicit scene override */ - scene?: "title" | "stat" | "caption" | "cta" | "tweet" | "thread" | "news" | "terminal" | "compare" | "fix" | "bullets" | "bang"; + scene?: "title" | "stat" | "caption" | "cta" | "tweet" | "thread" | "news" | "terminal" | "compare" | "fix" | "bullets" | "bang" | "quote" | "podium" | "gridlist" | "scoreboard"; stat?: { value: string; label: string }; /** source content (tweet/thread/news) — rendered as a visual card scene */ source?: { @@ -211,6 +215,12 @@ function pickScene(seg: Segment, index: number, seed = 0): Segment["scene"] | "c if (seg.stat) return "stat"; const stat = extractStat(seg.text); if (stat) return "stat"; + // creative scene variety (round 2): quote/podium/grid/scoreboard rotation + const j = (index + seed) % 8; + if (j === 3) return "quote"; + if (j === 5) return "podium"; + if (j === 6) return "gridlist"; + if (j === 7) return "scoreboard"; if (i % 3 === 2) return "title"; return "caption"; } @@ -284,6 +294,58 @@ const Scene: React.FC<{ ); } + if (scene === "quote") { + return ( + + + + {label ? : null} + + + + + + ); + } + if (scene === "podium") { + return ( + + + + {label ? : null} + + + + + + ); + } + if (scene === "gridlist") { + return ( + + + + {label ? : null} + + + + + + ); + } + if (scene === "scoreboard") { + return ( + + + + {label ? : null} + + + + + + ); + } if (scene === "caption" || scene === "title" || scene === "cta") { // regular scenes get an animated asset above the card (variety!) return ( diff --git a/src/remotion/parts.tsx b/src/remotion/parts.tsx index 2421188..894929a 100644 --- a/src/remotion/parts.tsx +++ b/src/remotion/parts.tsx @@ -762,4 +762,155 @@ export const MiniKaraoke: React.FC<{ ))} ); +}; + +/* ═══════════════════════════════════════════════════════════════════════ + SCENE ROUND 2 — 4 more creative scene types (mono flat). + ═══════════════════════════════════════════════════════════════════════ */ + +/** Quote scene — oversized quotation mark + emphasized excerpt + attribution. */ +export const QuoteScene: React.FC<{ text: string; size?: number }> = ({ text, size = 50 }) => { + const frame = useCurrentFrame(); + const { fps } = useVideoConfig(); + const p = spring({ frame, fps, config: { damping: 16, stiffness: 70 } }); + const words = text.split(" "); + const quote = words.length > 8 ? words.slice(0, -3).join(" ") : text; + const byline = words.length > 8 ? words.slice(-3).join(" ") : ""; + return ( +
+
+ “ +
+
+ {quote} +
+ {byline && ( +
+ — {byline} +
+ )} +
+ ); +}; + +/** Podium scene — 1-2-3 columns with the middle one elevated (ranking). */ +export const PodiumScene: React.FC<{ text: string; size?: number }> = ({ text, size = 36 }) => { + const frame = useCurrentFrame(); + const { fps } = useVideoConfig(); + const p = spring({ frame, fps, config: { damping: 16, stiffness: 80 } }); + const words = text.split(" "); + const picks = words.length >= 3 ? [words[0], words[1], words[2]] : ["TOP", "3", "PICKS"]; + const heights = [200, 280, 150]; + return ( +
+ {picks.map((w, i) => ( +
+
+ {w} +
+
+ {i + 1} +
+
+ ))} +
+ ); +}; + +/** Grid list — 2×2 keyword tiles popping in with check corners. */ +export const GridListScene: React.FC<{ text: string; size?: number }> = ({ text, size = 34 }) => { + const frame = useCurrentFrame(); + const { fps } = useVideoConfig(); + const words = text.split(" "); + const tiles = words.length >= 4 ? words.slice(0, 4) : ["AGENT", text.slice(0, 12).toUpperCase(), "TOOLS", "MEMORY"]; + return ( +
+ {tiles.map((w, i) => { + const p = spring({ frame: Math.max(0, frame - i * 6), fps, config: { damping: 16, stiffness: 100 } }); + const r = i % 2 === 0 ? 14 : 20; + return ( +
+ {w} +
+ ); + })} +
+ ); +}; + +/** Scoreboard — Q&A panel (question highlighted, answer slides in). */ +export const ScoreboardScene: React.FC<{ text: string; size?: number }> = ({ text, size = 38 }) => { + const frame = useCurrentFrame(); + const { fps } = useVideoConfig(); + const p = spring({ frame, fps, config: { damping: 16, stiffness: 75 } }); + const words = text.split(" "); + const q = words.slice(0, Math.ceil(words.length / 2)).join(" "); + const a = words.slice(Math.ceil(words.length / 2)).join(" "); + return ( +
+
+ Q + {q} +
+
+ A + {a} +
+
+ ); }; \ No newline at end of file