diff --git a/src/remotion/ShortsVideo.tsx b/src/remotion/ShortsVideo.tsx index 69d469a..1be7dc8 100644 --- a/src/remotion/ShortsVideo.tsx +++ b/src/remotion/ShortsVideo.tsx @@ -28,6 +28,16 @@ import { Checklist, HudFrame, StatBlock, + BouncyCount, + SplitText, + Banner, + TrackProgress, + CloudTag, + CardFan, + CameraPip, + WorldClock, + PercentageHands, + PadlockPulse, } from "./assets"; /** Asset pool — a visual building block sits above the caption card on @@ -45,6 +55,16 @@ const ASSET_POOL = [ "checklist", "hud", "stat", + "bouncy", + "split", + "banner", + "track", + "cloud", + "fan", + "camera", + "world", + "hands", + "lock", ] as const; type AssetKey = (typeof ASSET_POOL)[number]; @@ -58,15 +78,19 @@ const AssetDecor: React.FC<{ index: number; seed: number; text: string; label: s // seeded pool for anything else, so videos vary regardless. let key: AssetKey; if (L === "THE STORY") { - key = (["chartbars", "chartline", "ticker", "stat"] as AssetKey[])[(index + seed) % 4]; + key = (["chartbars", "chartline", "ticker", "stat", "bouncy"] as AssetKey[])[(index + seed) % 5]; } else if (L === "THE CONFLICT") { - key = (["pulse", "ring", "gauge", "pulse"] as AssetKey[])[(index + seed) % 4]; + key = (["pulse", "ring", "gauge", "pulse", "hands"] as AssetKey[])[(index + seed) % 5]; } else if (L === "THE INSIGHT") { - key = (["checklist", "nodeflow", "stat", "checklist"] as AssetKey[])[(index + seed) % 4]; + key = (["checklist", "nodeflow", "stat", "checklist", "track"] as AssetKey[])[(index + seed) % 5]; } else if (L === "THE MISTAKE" || L === "WHY IT FAILS") { - key = "pulse"; + key = (["pulse", "lock", "ring", "hands"] as AssetKey[])[(index + seed) % 4]; } else if (L === "THE FIX") { - key = "checklist"; + key = (["checklist", "track", "checklist", "fan"] as AssetKey[])[(index + seed) % 4]; + } else if (L === "HOOK") { + key = (["bouncy", "banner", "bouncy", "split"] as AssetKey[])[(index + seed) % 4]; + } else if (L === "CTA") { + key = (["banner", "bouncy", "hands", "banner"] as AssetKey[])[(index + seed) % 4]; } else { key = ASSET_POOL[(index + seed) % ASSET_POOL.length]; } @@ -86,6 +110,16 @@ const AssetDecor: React.FC<{ index: number; seed: number; text: string; label: s case "checklist": return ; case "hud": return 4 ? "SYSTEM" : "DATA"} />; case "stat": return ; + case "bouncy": return 6 ? "PROGRESS" : "SCORE"} suffix="%" delay={delay} />; + case "split": return 5 ? "OLD" : "DON'T"} b={words > 5 ? "NEW" : "DO"} delay={delay} />; + case "banner": return 6 ? "KEY TAKEAWAY" : "REMEMBER"} sub={text.toUpperCase().slice(0, 40)} delay={delay} />; + case "track": return ; + case "cloud": return ; + case "fan": return ; + case "camera": return ; + case "world": return ; + case "hands": return ; + case "lock": return ; default: return null; } }; diff --git a/src/remotion/assets.tsx b/src/remotion/assets.tsx index 97976f6..bf2451d 100644 --- a/src/remotion/assets.tsx +++ b/src/remotion/assets.tsx @@ -305,4 +305,271 @@ export const StatBlock: React.FC<{ value: string; sub: string; delay?: number }> ); +}; + +/* ═══════════════════════════════════════════════════════════════════════ + ROUND 2 — 11 more reusable assets (mono flat, transform-only). + ═══════════════════════════════════════════════════════════════════════ */ + +/** Bouncy counter — digits roll up to a target value (rolling-wrap odometer). */ +export const BouncyCount: React.FC<{ to?: number; label?: string; delay?: number; suffix?: string }> = ({ to = 100, label = "GOAL", delay = 0, suffix = "%" }) => { + const frame = useCurrentFrame(); + const { fps } = useVideoConfig(); + const p = spring({ frame: Math.max(0, frame - delay), fps, config: { damping: 18, stiffness: 55 } }); + const val = Math.round(to * p); + return ( +
+
+ {val} + {suffix} +
+
+ {label} +
+
+ ); +}; + +/** Split reveal — two words slide apart from a single point with a thin divider. */ +export const SplitText: React.FC<{ a?: string; b?: string; delay?: number }> = ({ a = "BEFORE", b = "AFTER", delay = 0 }) => { + const frame = useCurrentFrame(); + const { fps } = useVideoConfig(); + const p = spring({ frame: Math.max(0, frame - delay), fps, config: { damping: 18, stiffness: 70 } }); + const dist = 60 + (1 - p) * 220; + return ( +
+
+ {a} +
+
+
+ {b} +
+
+ ); +}; + +/** Dipping label — a pill badge that slides in with a "value" line underneath. */ +export const Banner: React.FC<{ title?: string; sub?: string; delay?: number }> = ({ title = "KEY TAKEAWAY", sub = "SHIP EARLY, LEARN FAST", delay = 0 }) => { + const frame = useCurrentFrame(); + const { fps } = useVideoConfig(); + const p = spring({ frame: Math.max(0, frame - delay), fps, config: { damping: 16, stiffness: 90 } }); + return ( +
+
+ {title} +
+
+ {sub} +
+
+ ); +}; + +/** Track progress — vertical step ladder filling in order. */ +export const TrackProgress: React.FC<{ steps?: string[]; delay?: number }> = ({ steps = ["01", "02", "03", "04"], delay = 0 }) => { + const frame = useCurrentFrame(); + const { fps } = useVideoConfig(); + const t = Math.max(0, (frame - delay) / fps); + const active = Math.min(steps.length, Math.floor(t * 1.6)); + return ( +
+ {steps.map((s, i) => { + const on = i < active; + return ( +
+
{s}
+ {i < steps.length - 1 &&
} +
+ ); + })} +
+ ); +}; + +/** Cloud tag — a cluster of keyword pills drifting slowly. */ +export const CloudTag: React.FC<{ tags?: string[]; delay?: number }> = ({ tags = ["AGENT", "MEMORY", "TOOLS", "PROMPTS"], delay = 0 }) => { + const frame = useCurrentFrame(); + const t = (frame - delay) / 60; + return ( +
+ {tags.map((tag, i) => { + const ang = (i / tags.length) * Math.PI * 2 + t * 0.22; + const x = 260 + Math.cos(ang) * 170; + const y = 130 + Math.sin(ang) * 75; + return ( +
+ {tag} +
+ ); + })} + {/* center dot */} +
+
+ ); +}; + +/** Card fan — three tapering cards fanning out from a pivot (stacked deck). */ +export const CardFan: React.FC<{ delay?: number }> = ({ delay = 0 }) => { + const frame = useCurrentFrame(); + const { fps } = useVideoConfig(); + const p = spring({ frame: Math.max(0, frame - delay), fps, config: { damping: 16, stiffness: 70 } }); + const cards = [ + { rot: -24, x: -120, txt: "IDEA" }, + { rot: 0, x: 0, txt: "BUILD" }, + { rot: 24, x: 120, txt: "SHIP" }, + ]; + return ( +
+ {cards.map((c, i) => ( +
+ {c.txt} +
+ ))} +
+ ); +}; + +/** Camera pip — viewfinder frame + REC dot pulsing + corner brackets. */ +export const CameraPip: React.FC<{ label?: string; delay?: number }> = ({ label = "REC", delay = 0 }) => { + const frame = useCurrentFrame(); + const t = Math.max(0, (frame - delay) / 60); + const blink = Math.floor(t * 2) % 2 === 0; + const c = 30, gap = 22; + return ( +
+ {[ + `M ${gap} ${gap + c} L ${gap} ${gap} L ${gap + c} ${gap}`, + `M ${560 - gap - c} ${gap} L ${560 - gap} ${gap} L ${560 - gap} ${gap + c}`, + `M ${gap} ${340 - gap - c} L ${gap} ${340 - gap} L ${gap + c} ${340 - gap}`, + `M ${560 - gap - c} ${340 - gap} L ${560 - gap} ${340 - gap} L ${560 - gap} ${340 - gap - c}`, + ].map((d, i) => ( + + + + ))} +
+
+
{label}
+
+ {/* rule-of-thirds grid */} +
+ {[0, 1].map((i) =>
)} +
+
+ ); +}; + +/** World grid clock — 4 tiles with city:time, one highlighted cycling. */ +export const WorldClock: React.FC<{ delay?: number }> = ({ delay = 0 }) => { + const frame = useCurrentFrame(); + const t = Math.max(0, (frame - delay) / 60); + const active = Math.floor(t) % 4; + const cities = [ + { city: "JKT", off: 7 }, + { city: "NYC", off: -4 }, + { city: "LON", off: 1 }, + { city: "TYO", off: 9 }, + ]; + const hh = (off: number) => ((9 + off + 24) % 24); + return ( +
+ {cities.map((c, i) => ( +
+
{c.city}
+
{String(hh(c.off)).padStart(2, "0")}:00
+
+ ))} +
+ ); +}; + +/** Percentage hands — two pill hands meeting a rising % meter (score vibe). */ +export const PercentageHands: React.FC<{ delay?: number }> = ({ delay = 0 }) => { + const frame = useCurrentFrame(); + const { fps } = useVideoConfig(); + const p = spring({ frame: Math.max(0, frame - delay), fps, config: { damping: 18, stiffness: 60 } }); + const val = Math.round(68 + 27 * p); + return ( +
+
+ {[0, 1].map((i) => ( +
+
+
+
+ ))} +
+
+ {val}% +
+
+
+
+
+ ); +}; + +/** Padlock pulse — lock icon with expanding ripple ring + OPEN/CLOSED state. */ +export const PadlockPulse: React.FC<{ closed?: boolean; delay?: number }> = ({ closed = false, delay = 0 }) => { + const frame = useCurrentFrame(); + const t = Math.max(0, (frame - delay) / 60); + const ringR = 40 + (t % 2) * 46; + const ringO = Math.max(0, 1 - (t % 2)) * 0.8; + return ( +
+
+ {/* ripple rings */} + {[0, 1].map((i) => { + const r = 40 + (((t + i * 1) % 2)) * 46; + const o = Math.max(0, 1 - ((t + i * 1) % 2)) * 0.7; + return ( +
+ ); + })} + {/* lock body */} +
+
+
+ {/* shackle */} +
+
+
+ {closed ? "LOCKED" : "OPEN"} +
+
+ ); }; \ No newline at end of file