Skip to content

Commit e6c1e83

Browse files
authored
feat: infographic-first SceneRouter + black bg + aggressive Ken Burns + HookScene infographic
SceneRouter: code scenes keep CodeMorphScene, infographics win over comparison/list/mockup. InfographicScene: aggressive Ken Burns (1.15 vertical, 1.08 landscape) + pan. HookScene: infographic background. All backgrounds #000000, purple #7c3aed.
1 parent c7feb72 commit e6c1e83

7 files changed

Lines changed: 159 additions & 81 deletions

File tree

remotion/components/HookScene.tsx

Lines changed: 68 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import React from "react";
22
import {
33
AbsoluteFill,
4+
Img,
45
interpolate,
56
spring,
67
useCurrentFrame,
@@ -13,12 +14,23 @@ export const HookScene: React.FC<HookSceneProps> = ({
1314
hook,
1415
durationInFrames,
1516
isVertical = false,
17+
infographicUrl,
1618
}) => {
1719
const frame = useCurrentFrame();
1820
const { fps } = useVideoConfig();
1921
const fonts = isVertical ? FONT_SIZES.portrait : FONT_SIZES.landscape;
2022

21-
// --- Background pulse animation ---
23+
const hasInfographic = !!infographicUrl;
24+
25+
// --- Ken Burns zoom for infographic background ---
26+
const infographicScale = interpolate(
27+
frame,
28+
[0, durationInFrames],
29+
[1.0, isVertical ? 1.12 : 1.06],
30+
{ extrapolateLeft: "clamp", extrapolateRight: "clamp" },
31+
);
32+
33+
// --- Background pulse animation (only used when no infographic) ---
2234
const bgPulse = interpolate(
2335
frame,
2436
[0, durationInFrames],
@@ -75,38 +87,62 @@ export const HookScene: React.FC<HookSceneProps> = ({
7587

7688
return (
7789
<AbsoluteFill style={{ opacity: fadeOut }}>
78-
{/* Animated gradient background */}
79-
<AbsoluteFill
80-
style={{
81-
background: `linear-gradient(${bgPulse}deg, ${COLORS.gradientStart}, ${COLORS.backgroundDark}, ${COLORS.primary})`,
82-
}}
83-
/>
90+
{/* Background: infographic or animated gradient */}
91+
{hasInfographic ? (
92+
<AbsoluteFill style={{ overflow: "hidden" }}>
93+
<Img
94+
src={infographicUrl}
95+
style={{
96+
width: "100%",
97+
height: "100%",
98+
objectFit: "cover",
99+
transform: `scale(${infographicScale})`,
100+
transformOrigin: "center center",
101+
}}
102+
/>
103+
{/* Dark overlay for text readability */}
104+
<AbsoluteFill
105+
style={{
106+
backgroundColor: "rgba(0, 0, 0, 0.5)",
107+
}}
108+
/>
109+
</AbsoluteFill>
110+
) : (
111+
<>
112+
{/* Animated gradient background */}
113+
<AbsoluteFill
114+
style={{
115+
background: `linear-gradient(${bgPulse}deg, ${COLORS.gradientStart}, ${COLORS.backgroundDark}, ${COLORS.primary})`,
116+
}}
117+
/>
84118

85-
{/* Decorative circles */}
86-
<div
87-
style={{
88-
position: "absolute",
89-
top: isVertical ? "15%" : "10%",
90-
right: isVertical ? "-10%" : "-5%",
91-
width: isVertical ? 300 : 500,
92-
height: isVertical ? 300 : 500,
93-
borderRadius: "50%",
94-
background: `radial-gradient(circle, ${COLORS.secondary}33, transparent)`,
95-
transform: `scale(${interpolate(frame, [0, 60], [0.5, 1.2], { extrapolateRight: "clamp" })})`,
96-
}}
97-
/>
98-
<div
99-
style={{
100-
position: "absolute",
101-
bottom: isVertical ? "20%" : "15%",
102-
left: isVertical ? "-15%" : "-8%",
103-
width: isVertical ? 250 : 400,
104-
height: isVertical ? 250 : 400,
105-
borderRadius: "50%",
106-
background: `radial-gradient(circle, ${COLORS.accent}22, transparent)`,
107-
transform: `scale(${interpolate(frame, [10, 70], [0.3, 1], { extrapolateRight: "clamp" })})`,
108-
}}
109-
/>
119+
{/* Decorative circles */}
120+
<div
121+
style={{
122+
position: "absolute",
123+
top: isVertical ? "15%" : "10%",
124+
right: isVertical ? "-10%" : "-5%",
125+
width: isVertical ? 300 : 500,
126+
height: isVertical ? 300 : 500,
127+
borderRadius: "50%",
128+
background: `radial-gradient(circle, ${COLORS.secondary}33, transparent)`,
129+
transform: `scale(${interpolate(frame, [0, 60], [0.5, 1.2], { extrapolateRight: "clamp" })})`,
130+
}}
131+
/>
132+
<div
133+
style={{
134+
position: "absolute",
135+
bottom: isVertical ? "20%" : "15%",
136+
left: isVertical ? "-15%" : "-8%",
137+
width: isVertical ? 250 : 400,
138+
height: isVertical ? 250 : 400,
139+
borderRadius: "50%",
140+
background: `radial-gradient(circle, ${COLORS.accent}22, transparent)`,
141+
transform: `scale(${interpolate(frame, [10, 70], [0.3, 1], { extrapolateRight: "clamp" })})`,
142+
}}
143+
/>
144+
</>
145+
)}
110146

111147
{/* Content container */}
112148
<AbsoluteFill

remotion/components/InfographicScene.tsx

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ import { FONT_SIZES } from "../constants";
1010

1111
/**
1212
* InfographicScene — cycles through multiple infographic images with
13-
* crossfade transitions and subtle Ken Burns zoom. NO text overlays.
14-
* Audio narration carries all words.
13+
* crossfade transitions and aggressive Ken Burns zoom + pan.
14+
* NO text overlays. Audio narration carries all words.
1515
*
1616
* Accepts both:
1717
* - `infographicUrls: string[]` (new multi-image cycling)
@@ -112,20 +112,49 @@ export const InfographicScene: React.FC<InfographicSceneProps> = ({
112112
);
113113
};
114114

115-
// Ken Burns: subtle zoom from 1.0 → 1.05 over the image's display duration
115+
// Ken Burns: aggressive zoom — vertical gets 1.0→1.15, landscape 1.0→1.08
116116
const getImageScale = (index: number): number => {
117117
const startFrame = index * framesPerImage;
118118
const endFrame = Math.min(
119119
startFrame + framesPerImage + crossfadeDuration,
120120
durationInFrames,
121121
);
122122

123+
const zoomEnd = isVertical ? 1.15 : 1.08;
124+
123125
return interpolate(
124126
frame,
125127
[startFrame, endFrame],
126-
[1.0, 1.05],
128+
[1.0, zoomEnd],
129+
{ extrapolateLeft: "clamp", extrapolateRight: "clamp" },
130+
);
131+
};
132+
133+
// Pan movement for variety — even scenes pan X, odd scenes pan Y
134+
const getImagePan = (index: number): { translateX: number; translateY: number } => {
135+
const startFrame = index * framesPerImage;
136+
const endFrame = Math.min(
137+
startFrame + framesPerImage + crossfadeDuration,
138+
durationInFrames,
139+
);
140+
141+
const panRange = isVertical ? 6 : 3; // vertical gets double pan range
142+
const isEvenScene = sceneIndex % 2 === 0;
143+
144+
const panProgress = interpolate(
145+
frame,
146+
[startFrame, endFrame],
147+
[-panRange, panRange],
127148
{ extrapolateLeft: "clamp", extrapolateRight: "clamp" },
128149
);
150+
151+
if (isEvenScene) {
152+
// Even scenes: pan left-to-right
153+
return { translateX: panProgress, translateY: 0 };
154+
} else {
155+
// Odd scenes: pan top-to-bottom
156+
return { translateX: 0, translateY: panProgress };
157+
}
129158
};
130159

131160
return (
@@ -140,6 +169,7 @@ export const InfographicScene: React.FC<InfographicSceneProps> = ({
140169
images.map((url, index) => {
141170
const opacity = getImageOpacity(index);
142171
const scale = getImageScale(index);
172+
const pan = getImagePan(index);
143173

144174
// Skip rendering images that are fully transparent
145175
if (opacity <= 0) {
@@ -160,18 +190,18 @@ export const InfographicScene: React.FC<InfographicSceneProps> = ({
160190
width: "100%",
161191
height: "100%",
162192
objectFit: "contain",
163-
transform: `scale(${scale})`,
193+
transform: `scale(${scale}) translate(${pan.translateX}%, ${pan.translateY}%)`,
164194
transformOrigin: "center center",
165195
}}
166196
/>
167197
</AbsoluteFill>
168198
);
169199
})
170200
) : (
171-
/* Fallback: dark gradient when no images available */
201+
/* Fallback: dark background when no images available */
172202
<AbsoluteFill
173203
style={{
174-
background: `linear-gradient(${(sceneIndex % 4) * 90}deg, #6D28D9, #0F0F23, #1A1A2E)`,
204+
background: "#000000",
175205
}}
176206
/>
177207
)}

remotion/components/SceneRouter.tsx

Lines changed: 30 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,11 @@ interface SceneRouterProps {
1818
* Routes a scene to the appropriate component based on its sceneType and data.
1919
*
2020
* Priority order:
21-
* 1. Explicit sceneType with matching data (code/list/comparison/mockup)
22-
* 2. infographicUrls array → InfographicScene (multi-image cycling)
21+
* 1. sceneType "code" → CodeMorphScene (Alex likes the terminal look)
22+
* 2. infographicUrls array → InfographicScene (wins over comparison/list/mockup)
2323
* 3. infographicUrl single → InfographicScene (wrapped in array)
24-
* 4. Fallback → Scene (Pexels b-roll, no text overlay)
24+
* 4. Specialized scene types (list/comparison/mockup) — fallback when no infographics
25+
* 5. Fallback → Scene (Pexels b-roll, no text overlay)
2526
*/
2627
export const SceneRouter: React.FC<SceneRouterProps> = ({
2728
scene,
@@ -37,14 +38,33 @@ export const SceneRouter: React.FC<SceneRouterProps> = ({
3738
wordTimestamps: scene.wordTimestamps,
3839
};
3940

40-
// --- 1. Specialized scene types (code/list/comparison/mockup) ---
41-
switch (scene.sceneType) {
42-
case "code":
43-
if (scene.code) {
44-
return <CodeMorphScene {...baseProps} code={scene.code} />;
45-
}
46-
break;
41+
// --- 1. Code scenes: Alex likes the terminal look, keep CodeMorphScene ---
42+
if (scene.sceneType === "code" && scene.code) {
43+
return <CodeMorphScene {...baseProps} code={scene.code} />;
44+
}
45+
46+
// --- 2. Infographic URLs array (wins over comparison/list/mockup) ---
47+
if (scene.infographicUrls && scene.infographicUrls.length > 0) {
48+
return (
49+
<InfographicScene
50+
{...baseProps}
51+
infographicUrls={scene.infographicUrls}
52+
/>
53+
);
54+
}
4755

56+
// --- 3. Legacy single infographic URL (wrap in array) ---
57+
if (scene.infographicUrl) {
58+
return (
59+
<InfographicScene
60+
{...baseProps}
61+
infographicUrls={[scene.infographicUrl]}
62+
/>
63+
);
64+
}
65+
66+
// --- 4. Specialized scene types (fallback when no infographics) ---
67+
switch (scene.sceneType) {
4868
case "list":
4969
if (scene.list) {
5070
return (
@@ -73,26 +93,6 @@ export const SceneRouter: React.FC<SceneRouterProps> = ({
7393
break;
7494
}
7595

76-
// --- 2. Infographic URLs array (multi-image cycling) ---
77-
if (scene.infographicUrls && scene.infographicUrls.length > 0) {
78-
return (
79-
<InfographicScene
80-
{...baseProps}
81-
infographicUrls={scene.infographicUrls}
82-
/>
83-
);
84-
}
85-
86-
// --- 3. Legacy single infographic URL (wrap in array) ---
87-
if (scene.infographicUrl) {
88-
return (
89-
<InfographicScene
90-
{...baseProps}
91-
infographicUrls={[scene.infographicUrl]}
92-
/>
93-
);
94-
}
95-
9696
// --- 4. Fallback: Pexels b-roll scene (no text overlay) ---
9797
return (
9898
<Scene

remotion/compositions/MainVideo.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,12 @@ export const MainVideo: React.FC<VideoInputProps> = ({
5454

5555
{/* Hook Scene */}
5656
<Sequence from={currentFrame} durationInFrames={hookDuration} name="Hook">
57-
<HookScene hook={hook} durationInFrames={hookDuration} isVertical={false} />
57+
<HookScene
58+
hook={hook}
59+
durationInFrames={hookDuration}
60+
isVertical={false}
61+
infographicUrl={scenes?.[0]?.infographicUrls?.[0] || scenes?.[0]?.infographicUrl}
62+
/>
5863
</Sequence>
5964
{(() => {
6065
currentFrame += hookDuration;

remotion/compositions/ShortVideo.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,12 @@ export const ShortVideo: React.FC<VideoInputProps> = ({
5151

5252
{/* Hook Scene — vertical format */}
5353
<Sequence from={0} durationInFrames={hookDuration} name="Hook">
54-
<HookScene hook={hook} durationInFrames={hookDuration} isVertical />
54+
<HookScene
55+
hook={hook}
56+
durationInFrames={hookDuration}
57+
isVertical
58+
infographicUrl={scenes?.[0]?.infographicUrls?.[0] || scenes?.[0]?.infographicUrl}
59+
/>
5560
</Sequence>
5661

5762
{/* Content Scenes — vertical format with larger text */}

remotion/constants.ts

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,13 @@ export const TRANSITION_DURATION = Math.round(FPS * 0.5); // 0.5s
3232

3333
export const COLORS = {
3434
/** CodingCat.dev primary purple */
35-
primary: "#6D28D9",
35+
primary: "#7c3aed",
3636
/** CodingCat.dev secondary / accent */
3737
secondary: "#A78BFA",
3838
/** Dark background */
39-
backgroundDark: "#0F0F23",
39+
backgroundDark: "#000000",
4040
/** Slightly lighter dark */
41-
backgroundMedium: "#1A1A2E",
41+
backgroundMedium: "#000000",
4242
/** Text white */
4343
textWhite: "#FFFFFF",
4444
/** Text muted */
@@ -48,13 +48,13 @@ export const COLORS = {
4848
/** CTA green */
4949
ctaGreen: "#10B981",
5050
/** Sponsor card background */
51-
sponsorBg: "rgba(109, 40, 217, 0.85)",
51+
sponsorBg: "rgba(124, 58, 237, 0.85)",
5252
/** Overlay for text readability */
5353
overlay: "rgba(0, 0, 0, 0.55)",
5454
/** Gradient start */
55-
gradientStart: "#6D28D9",
55+
gradientStart: "#7c3aed",
5656
/** Gradient end */
57-
gradientEnd: "#1A1A2E",
57+
gradientEnd: "#000000",
5858
} as const;
5959

6060
// --- Font Sizes ---
@@ -129,15 +129,15 @@ export const CODE_COLORS = {
129129
dotYellow: "#FEBC2E",
130130
dotGreen: "#28C840",
131131
/** Line highlight */
132-
lineHighlight: "rgba(109, 40, 217, 0.25)",
132+
lineHighlight: "rgba(124, 58, 237, 0.25)",
133133
/** Line number color */
134134
lineNumber: "rgba(255, 255, 255, 0.3)",
135135
} as const;
136136

137137
// --- List Scene Constants ---
138138
export const LIST_COLORS = {
139139
/** Active item background */
140-
activeBg: "rgba(109, 40, 217, 0.3)",
140+
activeBg: "rgba(124, 58, 237, 0.3)",
141141
/** Active item border */
142142
activeBorder: "#A78BFA",
143143
/** Inactive item opacity */
@@ -151,9 +151,9 @@ export const COMPARISON_COLORS = {
151151
/** Grid line color */
152152
gridLine: "rgba(167, 139, 250, 0.4)",
153153
/** Header background */
154-
headerBg: "rgba(109, 40, 217, 0.5)",
154+
headerBg: "rgba(124, 58, 237, 0.5)",
155155
/** Active row highlight */
156-
activeRow: "rgba(109, 40, 217, 0.2)",
156+
activeRow: "rgba(124, 58, 237, 0.2)",
157157
/** Left column accent */
158158
leftAccent: "#A78BFA",
159159
/** Right column accent */

0 commit comments

Comments
 (0)