feat(shorts): 4 more scene types (quote, podium, gridlist, scoreboard) — 15 scene types total
This commit is contained in:
@@ -13,6 +13,10 @@ import {
|
|||||||
CompareScene,
|
CompareScene,
|
||||||
BulletScene,
|
BulletScene,
|
||||||
BangScene,
|
BangScene,
|
||||||
|
QuoteScene,
|
||||||
|
PodiumScene,
|
||||||
|
GridListScene,
|
||||||
|
ScoreboardScene,
|
||||||
MiniKaraoke,
|
MiniKaraoke,
|
||||||
} from "./parts";
|
} from "./parts";
|
||||||
import { TweetCard, ThreadCard, NewsCard } from "./cards";
|
import { TweetCard, ThreadCard, NewsCard } from "./cards";
|
||||||
@@ -147,7 +151,7 @@ export type Segment = {
|
|||||||
label?: string;
|
label?: string;
|
||||||
sentences?: Sentence[];
|
sentences?: Sentence[];
|
||||||
/** optional explicit scene override */
|
/** 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 };
|
stat?: { value: string; label: string };
|
||||||
/** source content (tweet/thread/news) — rendered as a visual card scene */
|
/** source content (tweet/thread/news) — rendered as a visual card scene */
|
||||||
source?: {
|
source?: {
|
||||||
@@ -211,6 +215,12 @@ function pickScene(seg: Segment, index: number, seed = 0): Segment["scene"] | "c
|
|||||||
if (seg.stat) return "stat";
|
if (seg.stat) return "stat";
|
||||||
const stat = extractStat(seg.text);
|
const stat = extractStat(seg.text);
|
||||||
if (stat) return "stat";
|
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";
|
if (i % 3 === 2) return "title";
|
||||||
return "caption";
|
return "caption";
|
||||||
}
|
}
|
||||||
@@ -284,6 +294,58 @@ const Scene: React.FC<{
|
|||||||
</AbsoluteFill>
|
</AbsoluteFill>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (scene === "quote") {
|
||||||
|
return (
|
||||||
|
<AbsoluteFill>
|
||||||
|
<SvgBackground variant={index + (seed || 0)} />
|
||||||
|
<SlideMotion>
|
||||||
|
{label ? <BrandTag label={label} /> : null}
|
||||||
|
<QuoteScene text={text} />
|
||||||
|
<MiniKaraoke sentences={sentences} />
|
||||||
|
<ProgressBar total={duration} />
|
||||||
|
</SlideMotion>
|
||||||
|
</AbsoluteFill>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (scene === "podium") {
|
||||||
|
return (
|
||||||
|
<AbsoluteFill>
|
||||||
|
<SvgBackground variant={index + (seed || 0)} />
|
||||||
|
<SlideMotion>
|
||||||
|
{label ? <BrandTag label={label} /> : null}
|
||||||
|
<PodiumScene text={text} />
|
||||||
|
<MiniKaraoke sentences={sentences} />
|
||||||
|
<ProgressBar total={duration} />
|
||||||
|
</SlideMotion>
|
||||||
|
</AbsoluteFill>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (scene === "gridlist") {
|
||||||
|
return (
|
||||||
|
<AbsoluteFill>
|
||||||
|
<SvgBackground variant={index + (seed || 0)} />
|
||||||
|
<SlideMotion>
|
||||||
|
{label ? <BrandTag label={label} /> : null}
|
||||||
|
<GridListScene text={text} />
|
||||||
|
<MiniKaraoke sentences={sentences} />
|
||||||
|
<ProgressBar total={duration} />
|
||||||
|
</SlideMotion>
|
||||||
|
</AbsoluteFill>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (scene === "scoreboard") {
|
||||||
|
return (
|
||||||
|
<AbsoluteFill>
|
||||||
|
<SvgBackground variant={index + (seed || 0)} />
|
||||||
|
<SlideMotion>
|
||||||
|
{label ? <BrandTag label={label} /> : null}
|
||||||
|
<ScoreboardScene text={text} />
|
||||||
|
<MiniKaraoke sentences={sentences} />
|
||||||
|
<ProgressBar total={duration} />
|
||||||
|
</SlideMotion>
|
||||||
|
</AbsoluteFill>
|
||||||
|
);
|
||||||
|
}
|
||||||
if (scene === "caption" || scene === "title" || scene === "cta") {
|
if (scene === "caption" || scene === "title" || scene === "cta") {
|
||||||
// regular scenes get an animated asset above the card (variety!)
|
// regular scenes get an animated asset above the card (variety!)
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -762,4 +762,155 @@ export const MiniKaraoke: React.FC<{
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/* ═══════════════════════════════════════════════════════════════════════
|
||||||
|
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 (
|
||||||
|
<div style={{ position: "absolute", left: "10%", right: "10%", top: "24%", textAlign: "center" }}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontFamily: FONT_DISPLAY, fontWeight: 800, fontSize: 190, lineHeight: 0.6,
|
||||||
|
color: "rgba(255,255,255,0.25)", transform: `translateY(${(1 - p) * 40}px)`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
“
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontFamily: FONT_DISPLAY, fontWeight: 800, fontSize: size, lineHeight: 1.2,
|
||||||
|
letterSpacing: "-0.02em", color: "#ffffff", marginTop: -20,
|
||||||
|
opacity: p,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{quote}
|
||||||
|
</div>
|
||||||
|
{byline && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
marginTop: 26, display: "inline-block", borderTop: "2px solid rgba(255,255,255,0.6)",
|
||||||
|
paddingTop: 14, fontFamily: FONT_BODY, fontWeight: 700, fontSize: 26,
|
||||||
|
color: "rgba(255,255,255,0.75)", letterSpacing: "0.12em", textTransform: "uppercase",
|
||||||
|
opacity: p,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
— {byline}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 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 (
|
||||||
|
<div style={{ position: "absolute", left: 0, right: 0, top: "26%", display: "flex", alignItems: "flex-end", justifyContent: "center", gap: 30 }}>
|
||||||
|
{picks.map((w, i) => (
|
||||||
|
<div key={i} style={{ display: "flex", flexDirection: "column", alignItems: "center", gap: 14 }}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontFamily: FONT_DISPLAY, fontWeight: 800, fontSize: size,
|
||||||
|
color: i === 1 ? "#000" : "#fff", textAlign: "center",
|
||||||
|
transform: `translateY(${(1 - p) * 80}px)`, opacity: p,
|
||||||
|
width: 210, lineHeight: 1.15,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{w}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: 210, height: 70 + (heights[i] - 70) * p,
|
||||||
|
background: i === 1 ? "#fff" : "rgba(255,255,255,0.25)",
|
||||||
|
border: "2px solid #fff", borderRadius: 14,
|
||||||
|
display: "flex", alignItems: "center", justifyContent: "center",
|
||||||
|
fontFamily: FONT_DISPLAY, fontWeight: 800, fontSize: 44, color: i === 1 ? "#000" : "#fff",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{i + 1}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 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 (
|
||||||
|
<div style={{ position: "absolute", left: "14%", right: "14%", top: "30%", display: "grid", gridTemplateColumns: "1fr 1fr", gap: 24 }}>
|
||||||
|
{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 (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
style={{
|
||||||
|
border: "2px solid rgba(255,255,255,0.9)", borderRadius: 18,
|
||||||
|
minHeight: 140, display: "flex", alignItems: "center", justifyContent: "center",
|
||||||
|
fontFamily: FONT_DISPLAY, fontWeight: 800, fontSize: size,
|
||||||
|
color: "#fff", textAlign: "center", padding: "0 14px",
|
||||||
|
transform: `scale(${0.4 + p * 0.6}) rotate(${(r - 14) * (1 - p)}deg)`,
|
||||||
|
opacity: p,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{w}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 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 (
|
||||||
|
<div style={{ position: "absolute", left: "10%", right: "10%", top: "26%" }}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
background: "#fff", color: "#000", borderRadius: 18, padding: "26px 34px",
|
||||||
|
fontFamily: FONT_DISPLAY, fontWeight: 800, fontSize: size, lineHeight: 1.2,
|
||||||
|
transform: `translateY(${(1 - p) * 60}px)`, opacity: p,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span style={{ opacity: 0.5, fontSize: size - 14, marginRight: 14 }}>Q</span>
|
||||||
|
{q}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
marginTop: 22, border: "2px solid #fff", borderRadius: 18, padding: "26px 34px",
|
||||||
|
fontFamily: FONT_DISPLAY, fontWeight: 800, fontSize: size - 6, lineHeight: 1.2,
|
||||||
|
color: "#fff", transform: `translateY(${(1 - p) * 60}px)`, opacity: Math.max(0, p * 2 - 1),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span style={{ opacity: 0.6, fontSize: size - 14, marginRight: 14 }}>A</span>
|
||||||
|
{a}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
Reference in New Issue
Block a user