reelme 0.2.2 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -3
- package/assets/audio/PROVENANCE.md +16 -0
- package/assets/audio/bright-sparks.mp3 +0 -0
- package/assets/audio/calm-keys.mp3 +0 -0
- package/assets/audio/circuit-pulse.mp3 +0 -0
- package/assets/audio/clean-horizon.mp3 +0 -0
- package/assets/audio/generate-tracks.mjs +218 -0
- package/assets/audio/manifest.json +83 -0
- package/assets/audio/midnight-protocol.mp3 +0 -0
- package/assets/audio/pixel-bounce.mp3 +0 -0
- package/assets/audio/steady-launch.mp3 +0 -0
- package/assets/audio/sunny-loop.mp3 +0 -0
- package/assets/audio/vector-grid.mp3 +0 -0
- package/package.json +9 -2
- package/src/cache.mjs +155 -14
- package/src/cli.mjs +20 -11
- package/src/render.mjs +172 -9
- package/template/package.json +6 -5
- package/template/pnpm-lock.yaml +186 -116
- package/template/pnpm-workspace.yaml +2 -0
- package/template/src/Root.tsx +73 -51
- package/template/src/audio.ts +24 -0
- package/template/src/benchmark.ts +18 -0
- package/template/src/brief.json +7 -6
- package/template/src/brief.ts +73 -5
- package/template/src/cinematic/Atmosphere.tsx +139 -0
- package/template/src/cinematic/Camera.tsx +54 -0
- package/template/src/cinematic/look.ts +106 -0
- package/template/src/cinematic/transitions.tsx +110 -0
- package/template/src/components/primitives/Bar.tsx +86 -0
- package/template/src/components/primitives/Caption.tsx +5 -4
- package/template/src/components/primitives/Icon.tsx +7 -0
- package/template/src/components/primitives/KeyPill.tsx +1 -1
- package/template/src/components/primitives/Kicker.tsx +46 -0
- package/template/src/components/primitives/Label.tsx +22 -25
- package/template/src/components/primitives/Loop.tsx +65 -0
- package/template/src/components/primitives/RevealText.tsx +123 -0
- package/template/src/components/primitives/Stage.tsx +62 -0
- package/template/src/components/primitives/Terminal.tsx +11 -0
- package/template/src/components/scenes/Benchmark.tsx +83 -0
- package/template/src/components/scenes/BrowserFrame.tsx +5 -4
- package/template/src/components/scenes/CTA.tsx +67 -58
- package/template/src/components/scenes/Clip.tsx +131 -0
- package/template/src/components/scenes/CodeReveal.tsx +10 -8
- package/template/src/components/scenes/DataFlow.tsx +5 -4
- package/template/src/components/scenes/FeatureList.tsx +80 -78
- package/template/src/components/scenes/FileTree.tsx +5 -4
- package/template/src/components/scenes/Hook.tsx +55 -0
- package/template/src/components/scenes/HotkeyScene.tsx +8 -6
- package/template/src/components/scenes/MobileScreen.tsx +308 -155
- package/template/src/components/scenes/OSWindowScene.tsx +8 -7
- package/template/src/components/scenes/Problem.tsx +29 -30
- package/template/src/components/scenes/SplitComparison.tsx +65 -92
- package/template/src/components/scenes/StatCallout.tsx +162 -18
- package/template/src/components/scenes/TerminalScene.tsx +16 -16
- package/template/src/duration.ts +23 -2
- package/template/src/index.ts +7 -5
- package/template/src/platforms.ts +4 -0
- package/template/src/timing.ts +49 -0
|
@@ -2,23 +2,30 @@ import React from "react";
|
|
|
2
2
|
import { AbsoluteFill, useCurrentFrame, useVideoConfig, spring, interpolate } from "remotion";
|
|
3
3
|
import { ProblemScene as ProblemBrief, ProjectMeta } from "../../brief";
|
|
4
4
|
import { Theme } from "../../theme";
|
|
5
|
+
import { PlatformPreset, typeScale } from "../../platforms";
|
|
5
6
|
import { Label } from "../primitives/Label";
|
|
6
7
|
import { Caption } from "../primitives/Caption";
|
|
8
|
+
import { RevealText } from "../primitives/RevealText";
|
|
9
|
+
import { Kicker } from "../primitives/Kicker";
|
|
7
10
|
|
|
8
11
|
interface Props {
|
|
9
12
|
scene: ProblemBrief;
|
|
10
13
|
theme: Theme;
|
|
11
14
|
project: ProjectMeta;
|
|
15
|
+
platform?: PlatformPreset;
|
|
16
|
+
bottomInset?: number;
|
|
12
17
|
}
|
|
13
18
|
|
|
14
19
|
const SNAP_OFFSET = 8;
|
|
15
20
|
|
|
16
|
-
export const Problem: React.FC<Props> = ({ scene, theme, project }) => {
|
|
21
|
+
export const Problem: React.FC<Props> = ({ scene, theme, project, platform, bottomInset = 0 }) => {
|
|
17
22
|
const frame = useCurrentFrame();
|
|
18
23
|
const { fps } = useVideoConfig();
|
|
24
|
+
const scale = platform ? typeScale(platform) : 1.0;
|
|
19
25
|
|
|
20
26
|
const isAnnouncement = project.mode === "announcement" && project.version;
|
|
21
27
|
const isHero = !!scene.hero;
|
|
28
|
+
const left = scene.align === "left";
|
|
22
29
|
|
|
23
30
|
const accentBarProgress = spring({ frame: frame + SNAP_OFFSET, fps, config: theme.motion });
|
|
24
31
|
const accentBarWidth = interpolate(accentBarProgress, [0, 1], [0, isHero ? 120 : 80]);
|
|
@@ -30,20 +37,22 @@ export const Problem: React.FC<Props> = ({ scene, theme, project }) => {
|
|
|
30
37
|
return (
|
|
31
38
|
<AbsoluteFill
|
|
32
39
|
style={{
|
|
33
|
-
background:
|
|
40
|
+
background: "transparent",
|
|
34
41
|
display: "flex",
|
|
35
42
|
flexDirection: "column",
|
|
36
|
-
alignItems: "center",
|
|
43
|
+
alignItems: left ? "flex-start" : "center",
|
|
37
44
|
justifyContent: "center",
|
|
38
|
-
padding: isHero ? "0 80px" : "0 120px",
|
|
45
|
+
padding: left ? "0 0 0 140px" : isHero ? "0 80px" : "0 120px",
|
|
39
46
|
gap: isHero ? 40 : 32,
|
|
40
47
|
}}
|
|
41
48
|
>
|
|
42
|
-
{
|
|
49
|
+
{scene.kicker ? (
|
|
50
|
+
<Kicker text={scene.kicker} theme={theme} startFrame={0} align={left ? "left" : "center"} />
|
|
51
|
+
) : isAnnouncement ? (
|
|
43
52
|
<div
|
|
44
53
|
style={{
|
|
45
54
|
opacity: badgeOpacity,
|
|
46
|
-
|
|
55
|
+
translate: `0 ${badgeY}px`,
|
|
47
56
|
background: theme.accentMuted,
|
|
48
57
|
border: `1.5px solid ${theme.accent}`,
|
|
49
58
|
borderRadius: 999,
|
|
@@ -69,37 +78,27 @@ export const Problem: React.FC<Props> = ({ scene, theme, project }) => {
|
|
|
69
78
|
)}
|
|
70
79
|
|
|
71
80
|
{isHero ? (
|
|
72
|
-
<
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
[0, 1], [0, 1]
|
|
84
|
-
),
|
|
85
|
-
transform: `translateY(${interpolate(
|
|
86
|
-
spring({ frame: frame + SNAP_OFFSET - 4, fps, config: theme.motion }),
|
|
87
|
-
[0, 1], [24, 0]
|
|
88
|
-
)}px)`,
|
|
89
|
-
}}
|
|
90
|
-
>
|
|
91
|
-
{scene.headline}
|
|
92
|
-
</div>
|
|
81
|
+
<RevealText
|
|
82
|
+
text={scene.headline}
|
|
83
|
+
theme={theme}
|
|
84
|
+
fontSize={104 * scale}
|
|
85
|
+
fontWeight={800}
|
|
86
|
+
align={left ? "left" : "center"}
|
|
87
|
+
stagger={3.5}
|
|
88
|
+
letterSpacing="-0.03em"
|
|
89
|
+
lineHeight={1.04}
|
|
90
|
+
maxWidth={left ? 1300 : 1500}
|
|
91
|
+
/>
|
|
93
92
|
) : (
|
|
94
93
|
<>
|
|
95
|
-
<Label text={scene.headline} theme={theme} size="xl" startFrame={4} />
|
|
94
|
+
<Label text={scene.headline} theme={theme} size="xl" align={left ? "left" : "center"} startFrame={4} />
|
|
96
95
|
{scene.subtext && (
|
|
97
|
-
<Label text={scene.subtext} theme={theme} size="md" muted startFrame={16} />
|
|
96
|
+
<Label text={scene.subtext} theme={theme} size="md" muted align={left ? "left" : "center"} startFrame={16} />
|
|
98
97
|
)}
|
|
99
98
|
</>
|
|
100
99
|
)}
|
|
101
100
|
|
|
102
|
-
{scene.caption && <Caption text={scene.caption} theme={theme} startFrame={40} />}
|
|
101
|
+
{scene.caption && <Caption text={scene.caption} theme={theme} startFrame={40} bottomInset={bottomInset} />}
|
|
103
102
|
</AbsoluteFill>
|
|
104
103
|
);
|
|
105
104
|
};
|
|
@@ -7,9 +7,10 @@ import { Caption } from "../primitives/Caption";
|
|
|
7
7
|
interface Props {
|
|
8
8
|
scene: SplitBrief;
|
|
9
9
|
theme: Theme;
|
|
10
|
+
bottomInset?: number;
|
|
10
11
|
}
|
|
11
12
|
|
|
12
|
-
export const SplitComparison: React.FC<Props> = ({ scene, theme }) => {
|
|
13
|
+
export const SplitComparison: React.FC<Props> = ({ scene, theme, bottomInset = 0 }) => {
|
|
13
14
|
const frame = useCurrentFrame();
|
|
14
15
|
const { fps } = useVideoConfig();
|
|
15
16
|
|
|
@@ -23,114 +24,86 @@ export const SplitComparison: React.FC<Props> = ({ scene, theme }) => {
|
|
|
23
24
|
const rightX = interpolate(Math.max(0, rightProgress), [0, 1], [60, 0]);
|
|
24
25
|
const dividerOpacity = interpolate(Math.max(0, dividerProgress), [0, 1], [0, 1]);
|
|
25
26
|
|
|
27
|
+
const Panel: React.FC<{
|
|
28
|
+
label: string;
|
|
29
|
+
content: string;
|
|
30
|
+
accent: boolean;
|
|
31
|
+
opacity: number;
|
|
32
|
+
x: number;
|
|
33
|
+
}> = ({ label, content, accent, opacity, x }) => (
|
|
34
|
+
<div
|
|
35
|
+
style={{
|
|
36
|
+
flex: 1,
|
|
37
|
+
opacity,
|
|
38
|
+
translate: `${x}px 0`,
|
|
39
|
+
display: "flex",
|
|
40
|
+
flexDirection: "column",
|
|
41
|
+
gap: 18,
|
|
42
|
+
}}
|
|
43
|
+
>
|
|
44
|
+
<div
|
|
45
|
+
style={{
|
|
46
|
+
fontFamily: theme.fontSans,
|
|
47
|
+
fontSize: 15,
|
|
48
|
+
fontWeight: 700,
|
|
49
|
+
letterSpacing: "0.12em",
|
|
50
|
+
textTransform: "uppercase",
|
|
51
|
+
color: accent ? theme.accent : theme.textMuted,
|
|
52
|
+
}}
|
|
53
|
+
>
|
|
54
|
+
{label}
|
|
55
|
+
</div>
|
|
56
|
+
<div
|
|
57
|
+
style={{
|
|
58
|
+
minHeight: 240,
|
|
59
|
+
background: theme.surface,
|
|
60
|
+
border: `1.5px solid ${accent ? `${theme.accent}66` : theme.border}`,
|
|
61
|
+
borderRadius: 14,
|
|
62
|
+
padding: "44px 40px",
|
|
63
|
+
display: "flex",
|
|
64
|
+
alignItems: "center",
|
|
65
|
+
justifyContent: "center",
|
|
66
|
+
textAlign: "center",
|
|
67
|
+
fontFamily: theme.fontSans,
|
|
68
|
+
fontSize: 32,
|
|
69
|
+
fontWeight: 600,
|
|
70
|
+
lineHeight: 1.3,
|
|
71
|
+
letterSpacing: "-0.01em",
|
|
72
|
+
color: accent ? theme.text : theme.textMuted,
|
|
73
|
+
boxShadow: accent ? `0 18px 50px ${theme.accent}22` : "0 14px 40px rgba(0,0,0,0.3)",
|
|
74
|
+
}}
|
|
75
|
+
>
|
|
76
|
+
{content}
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
);
|
|
80
|
+
|
|
26
81
|
return (
|
|
27
82
|
<AbsoluteFill
|
|
28
83
|
style={{
|
|
29
|
-
background:
|
|
84
|
+
background: "transparent",
|
|
30
85
|
display: "flex",
|
|
31
86
|
alignItems: "center",
|
|
32
87
|
justifyContent: "center",
|
|
33
|
-
padding: "
|
|
88
|
+
padding: "0 100px",
|
|
34
89
|
}}
|
|
35
90
|
>
|
|
36
|
-
<div style={{ display: "flex", width: "100%",
|
|
37
|
-
{
|
|
38
|
-
<div
|
|
39
|
-
style={{
|
|
40
|
-
flex: 1,
|
|
41
|
-
opacity: leftOpacity,
|
|
42
|
-
transform: `translateX(${leftX}px)`,
|
|
43
|
-
display: "flex",
|
|
44
|
-
flexDirection: "column",
|
|
45
|
-
gap: 16,
|
|
46
|
-
}}
|
|
47
|
-
>
|
|
48
|
-
<div
|
|
49
|
-
style={{
|
|
50
|
-
fontFamily: theme.fontSans,
|
|
51
|
-
fontSize: 13,
|
|
52
|
-
fontWeight: 700,
|
|
53
|
-
letterSpacing: "0.1em",
|
|
54
|
-
textTransform: "uppercase",
|
|
55
|
-
color: theme.textMuted,
|
|
56
|
-
}}
|
|
57
|
-
>
|
|
58
|
-
{scene.before.label}
|
|
59
|
-
</div>
|
|
60
|
-
<div
|
|
61
|
-
style={{
|
|
62
|
-
flex: 1,
|
|
63
|
-
background: theme.surface,
|
|
64
|
-
border: `1.5px solid ${theme.border}`,
|
|
65
|
-
borderRadius: 10,
|
|
66
|
-
padding: "24px 28px",
|
|
67
|
-
fontFamily: theme.fontMono,
|
|
68
|
-
fontSize: 18,
|
|
69
|
-
color: theme.textMuted,
|
|
70
|
-
whiteSpace: "pre-wrap",
|
|
71
|
-
lineHeight: 1.7,
|
|
72
|
-
overflow: "hidden",
|
|
73
|
-
}}
|
|
74
|
-
>
|
|
75
|
-
{scene.before.content}
|
|
76
|
-
</div>
|
|
77
|
-
</div>
|
|
78
|
-
|
|
79
|
-
{/* Divider */}
|
|
91
|
+
<div style={{ display: "flex", width: "100%", maxWidth: 1500, alignItems: "stretch", gap: 0 }}>
|
|
92
|
+
<Panel label={scene.before.label} content={scene.before.content} accent={false} opacity={leftOpacity} x={leftX} />
|
|
80
93
|
<div
|
|
81
94
|
style={{
|
|
82
95
|
width: 2,
|
|
83
|
-
alignSelf: "
|
|
96
|
+
alignSelf: "center",
|
|
97
|
+
height: 200,
|
|
84
98
|
background: theme.border,
|
|
85
|
-
margin: "36px
|
|
99
|
+
margin: "44px 36px 0",
|
|
86
100
|
opacity: dividerOpacity,
|
|
87
101
|
}}
|
|
88
102
|
/>
|
|
89
|
-
|
|
90
|
-
{/* After panel */}
|
|
91
|
-
<div
|
|
92
|
-
style={{
|
|
93
|
-
flex: 1,
|
|
94
|
-
opacity: rightOpacity,
|
|
95
|
-
transform: `translateX(${rightX}px)`,
|
|
96
|
-
display: "flex",
|
|
97
|
-
flexDirection: "column",
|
|
98
|
-
gap: 16,
|
|
99
|
-
}}
|
|
100
|
-
>
|
|
101
|
-
<div
|
|
102
|
-
style={{
|
|
103
|
-
fontFamily: theme.fontSans,
|
|
104
|
-
fontSize: 13,
|
|
105
|
-
fontWeight: 700,
|
|
106
|
-
letterSpacing: "0.1em",
|
|
107
|
-
textTransform: "uppercase",
|
|
108
|
-
color: theme.accent,
|
|
109
|
-
}}
|
|
110
|
-
>
|
|
111
|
-
{scene.after.label}
|
|
112
|
-
</div>
|
|
113
|
-
<div
|
|
114
|
-
style={{
|
|
115
|
-
flex: 1,
|
|
116
|
-
background: theme.surface,
|
|
117
|
-
border: `1.5px solid ${theme.accent}55`,
|
|
118
|
-
borderRadius: 10,
|
|
119
|
-
padding: "24px 28px",
|
|
120
|
-
fontFamily: theme.fontMono,
|
|
121
|
-
fontSize: 18,
|
|
122
|
-
color: theme.text,
|
|
123
|
-
whiteSpace: "pre-wrap",
|
|
124
|
-
lineHeight: 1.7,
|
|
125
|
-
overflow: "hidden",
|
|
126
|
-
}}
|
|
127
|
-
>
|
|
128
|
-
{scene.after.content}
|
|
129
|
-
</div>
|
|
130
|
-
</div>
|
|
103
|
+
<Panel label={scene.after.label} content={scene.after.content} accent opacity={rightOpacity} x={rightX} />
|
|
131
104
|
</div>
|
|
132
105
|
|
|
133
|
-
{scene.caption && <Caption text={scene.caption} theme={theme} startFrame={50} />}
|
|
106
|
+
{scene.caption && <Caption text={scene.caption} theme={theme} startFrame={50} bottomInset={bottomInset} />}
|
|
134
107
|
</AbsoluteFill>
|
|
135
108
|
);
|
|
136
109
|
};
|
|
@@ -1,37 +1,32 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
import {
|
|
2
|
+
import { useCurrentFrame, useVideoConfig, spring, interpolate } from "remotion";
|
|
3
3
|
import { StatCalloutScene as StatCalloutBrief } from "../../brief";
|
|
4
4
|
import { Theme } from "../../theme";
|
|
5
5
|
import { Label } from "../primitives/Label";
|
|
6
|
-
import {
|
|
6
|
+
import { Stage } from "../primitives/Stage";
|
|
7
7
|
|
|
8
8
|
interface Props {
|
|
9
9
|
scene: StatCalloutBrief;
|
|
10
10
|
theme: Theme;
|
|
11
|
+
bottomInset?: number;
|
|
11
12
|
}
|
|
12
13
|
|
|
13
14
|
const HEADLINE_FRAMES = 20;
|
|
14
15
|
const FRAMES_PER_STAT = 35;
|
|
15
16
|
const SNAP_OFFSET = 8;
|
|
16
17
|
|
|
17
|
-
export const StatCallout: React.FC<Props> = ({ scene, theme }) => {
|
|
18
|
+
export const StatCallout: React.FC<Props> = ({ scene, theme, bottomInset = 0 }) => {
|
|
18
19
|
const frame = useCurrentFrame();
|
|
19
|
-
const { fps } = useVideoConfig();
|
|
20
|
+
const { fps, width } = useVideoConfig();
|
|
21
|
+
|
|
22
|
+
if (scene.layout === "hero") {
|
|
23
|
+
return <HeroStat scene={scene} theme={theme} bottomInset={bottomInset} width={width} frame={frame} fps={fps} />;
|
|
24
|
+
}
|
|
20
25
|
|
|
21
26
|
const captionStart = HEADLINE_FRAMES + scene.stats.length * FRAMES_PER_STAT + 20;
|
|
22
27
|
|
|
23
28
|
return (
|
|
24
|
-
<
|
|
25
|
-
style={{
|
|
26
|
-
background: theme.bg,
|
|
27
|
-
display: "flex",
|
|
28
|
-
flexDirection: "column",
|
|
29
|
-
alignItems: "center",
|
|
30
|
-
justifyContent: "center",
|
|
31
|
-
padding: "0 120px",
|
|
32
|
-
gap: 64,
|
|
33
|
-
}}
|
|
34
|
-
>
|
|
29
|
+
<Stage theme={theme} gap={64} caption={scene.caption} captionStart={captionStart} bottomInset={bottomInset}>
|
|
35
30
|
{scene.headline && (
|
|
36
31
|
<Label text={scene.headline} theme={theme} size="lg" startFrame={0} />
|
|
37
32
|
)}
|
|
@@ -71,7 +66,7 @@ export const StatCallout: React.FC<Props> = ({ scene, theme }) => {
|
|
|
71
66
|
alignItems: "center",
|
|
72
67
|
gap: 16,
|
|
73
68
|
opacity,
|
|
74
|
-
|
|
69
|
+
scale: String(scale),
|
|
75
70
|
}}
|
|
76
71
|
>
|
|
77
72
|
<div
|
|
@@ -104,7 +99,156 @@ export const StatCallout: React.FC<Props> = ({ scene, theme }) => {
|
|
|
104
99
|
})}
|
|
105
100
|
</div>
|
|
106
101
|
|
|
107
|
-
|
|
108
|
-
|
|
102
|
+
</Stage>
|
|
103
|
+
);
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* One dominant number, kept in balance. With supporting stats they fill the
|
|
108
|
+
* opposite side as a substantial column (divided by a thin rule) so the frame
|
|
109
|
+
* carries weight on both sides instead of stranding the number against an empty
|
|
110
|
+
* void. Alone, the number is centered and composed so it never sits in a corner.
|
|
111
|
+
*/
|
|
112
|
+
const HeroStat: React.FC<{
|
|
113
|
+
scene: StatCalloutBrief;
|
|
114
|
+
theme: Theme;
|
|
115
|
+
bottomInset: number;
|
|
116
|
+
width: number;
|
|
117
|
+
frame: number;
|
|
118
|
+
fps: number;
|
|
119
|
+
}> = ({ scene, theme, bottomInset, width, frame, fps }) => {
|
|
120
|
+
const [hero, ...rest] = scene.stats;
|
|
121
|
+
const hasSecondary = rest.length > 0;
|
|
122
|
+
const p = spring({ frame: frame + SNAP_OFFSET, fps, config: theme.motion });
|
|
123
|
+
const valueScale = interpolate(p, [0, 1], [0.6, 1]);
|
|
124
|
+
const valueOpacity = interpolate(p, [0, 1], [0, 1]);
|
|
125
|
+
const labelP = spring({ frame: frame - 10 + SNAP_OFFSET, fps, config: theme.motion });
|
|
126
|
+
const dividerP = spring({ frame: frame - 18 + SNAP_OFFSET, fps, config: theme.motion });
|
|
127
|
+
|
|
128
|
+
return (
|
|
129
|
+
<Stage
|
|
130
|
+
theme={theme}
|
|
131
|
+
direction="row"
|
|
132
|
+
justify={hasSecondary ? "space-between" : "center"}
|
|
133
|
+
gap={90}
|
|
134
|
+
padding="0 130px"
|
|
135
|
+
caption={scene.caption}
|
|
136
|
+
captionStart={50}
|
|
137
|
+
bottomInset={bottomInset}
|
|
138
|
+
>
|
|
139
|
+
{/* Primary: the dominant number and its label. */}
|
|
140
|
+
<div
|
|
141
|
+
style={{
|
|
142
|
+
display: "flex",
|
|
143
|
+
flexDirection: "column",
|
|
144
|
+
alignItems: hasSecondary ? "flex-start" : "center",
|
|
145
|
+
maxWidth: hasSecondary ? width * 0.5 : width * 0.78,
|
|
146
|
+
}}
|
|
147
|
+
>
|
|
148
|
+
{scene.headline && (
|
|
149
|
+
<div style={{ marginBottom: 10 }}>
|
|
150
|
+
<Label
|
|
151
|
+
text={scene.headline}
|
|
152
|
+
theme={theme}
|
|
153
|
+
size="md"
|
|
154
|
+
align={hasSecondary ? "left" : "center"}
|
|
155
|
+
startFrame={0}
|
|
156
|
+
muted
|
|
157
|
+
/>
|
|
158
|
+
</div>
|
|
159
|
+
)}
|
|
160
|
+
<div
|
|
161
|
+
style={{
|
|
162
|
+
fontFamily: theme.fontSans,
|
|
163
|
+
fontSize: hasSecondary ? width * 0.16 : width * 0.2,
|
|
164
|
+
fontWeight: 800,
|
|
165
|
+
color: theme.accent,
|
|
166
|
+
lineHeight: 0.9,
|
|
167
|
+
letterSpacing: "-0.05em",
|
|
168
|
+
opacity: valueOpacity,
|
|
169
|
+
scale: String(valueScale),
|
|
170
|
+
transformOrigin: hasSecondary ? "left center" : "center",
|
|
171
|
+
}}
|
|
172
|
+
>
|
|
173
|
+
{hero?.value}
|
|
174
|
+
</div>
|
|
175
|
+
{hero?.label && (
|
|
176
|
+
<div
|
|
177
|
+
style={{
|
|
178
|
+
marginTop: 20,
|
|
179
|
+
fontFamily: theme.fontSans,
|
|
180
|
+
fontSize: 40,
|
|
181
|
+
fontWeight: 600,
|
|
182
|
+
lineHeight: 1.2,
|
|
183
|
+
color: theme.text,
|
|
184
|
+
textAlign: hasSecondary ? "left" : "center",
|
|
185
|
+
maxWidth: hasSecondary ? width * 0.46 : width * 0.66,
|
|
186
|
+
opacity: interpolate(labelP, [0, 1], [0, 1]),
|
|
187
|
+
translate: `0 ${interpolate(labelP, [0, 1], [16, 0])}px`,
|
|
188
|
+
}}
|
|
189
|
+
>
|
|
190
|
+
{hero?.label}
|
|
191
|
+
</div>
|
|
192
|
+
)}
|
|
193
|
+
</div>
|
|
194
|
+
|
|
195
|
+
{/* Secondary mass: supporting stats fill the opposite side as substantial
|
|
196
|
+
blocks, balancing the hero number instead of leaving a void. */}
|
|
197
|
+
{hasSecondary && (
|
|
198
|
+
<div style={{ display: "flex", alignItems: "stretch", gap: 44 }}>
|
|
199
|
+
<div
|
|
200
|
+
style={{
|
|
201
|
+
width: 2,
|
|
202
|
+
alignSelf: "stretch",
|
|
203
|
+
background: theme.border,
|
|
204
|
+
opacity: interpolate(dividerP, [0, 1], [0, 0.7]),
|
|
205
|
+
scale: `1 ${interpolate(dividerP, [0, 1], [0, 1])}`,
|
|
206
|
+
transformOrigin: "top",
|
|
207
|
+
}}
|
|
208
|
+
/>
|
|
209
|
+
<div style={{ display: "flex", flexDirection: "column", gap: 40 }}>
|
|
210
|
+
{rest.map((s, i) => {
|
|
211
|
+
const rp = spring({ frame: frame - 22 - i * 8 + SNAP_OFFSET, fps, config: theme.motion });
|
|
212
|
+
return (
|
|
213
|
+
<div
|
|
214
|
+
key={i}
|
|
215
|
+
style={{
|
|
216
|
+
opacity: interpolate(rp, [0, 1], [0, 1]),
|
|
217
|
+
translate: `${interpolate(rp, [0, 1], [24, 0])}px 0`,
|
|
218
|
+
}}
|
|
219
|
+
>
|
|
220
|
+
<div
|
|
221
|
+
style={{
|
|
222
|
+
fontFamily: theme.fontSans,
|
|
223
|
+
fontSize: 68,
|
|
224
|
+
fontWeight: 800,
|
|
225
|
+
color: theme.text,
|
|
226
|
+
lineHeight: 1,
|
|
227
|
+
letterSpacing: "-0.03em",
|
|
228
|
+
}}
|
|
229
|
+
>
|
|
230
|
+
{s.value}
|
|
231
|
+
</div>
|
|
232
|
+
<div
|
|
233
|
+
style={{
|
|
234
|
+
marginTop: 8,
|
|
235
|
+
fontFamily: theme.fontSans,
|
|
236
|
+
fontSize: 30,
|
|
237
|
+
fontWeight: 500,
|
|
238
|
+
color: theme.textMuted,
|
|
239
|
+
maxWidth: width * 0.28,
|
|
240
|
+
lineHeight: 1.25,
|
|
241
|
+
}}
|
|
242
|
+
>
|
|
243
|
+
{s.label}
|
|
244
|
+
</div>
|
|
245
|
+
</div>
|
|
246
|
+
);
|
|
247
|
+
})}
|
|
248
|
+
</div>
|
|
249
|
+
</div>
|
|
250
|
+
)}
|
|
251
|
+
|
|
252
|
+
</Stage>
|
|
109
253
|
);
|
|
110
254
|
};
|
|
@@ -4,38 +4,38 @@ import { TerminalScene as TerminalBrief } from "../../brief";
|
|
|
4
4
|
import { Theme } from "../../theme";
|
|
5
5
|
import { Terminal } from "../primitives/Terminal";
|
|
6
6
|
import { Caption } from "../primitives/Caption";
|
|
7
|
+
import { TERMINAL_FRAMES_PER_CHAR, TERMINAL_FRAMES_PER_LINE, TERMINAL_START, terminalCaptionStart, terminalLines } from "../../timing";
|
|
7
8
|
|
|
8
9
|
interface Props {
|
|
9
10
|
scene: TerminalBrief;
|
|
10
11
|
theme: Theme;
|
|
12
|
+
bottomInset?: number;
|
|
11
13
|
}
|
|
12
14
|
|
|
13
|
-
export const TerminalScene: React.FC<Props> = ({ scene, theme }) => {
|
|
14
|
-
const lines = scene
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
// Mirror Terminal's timing: startFrame=8, framesPerLine=23, framesPerChar=2.0
|
|
20
|
-
const terminalDuration = lines.reduce((acc, line) => {
|
|
21
|
-
return acc + (line.isOutput ? 23 : line.text.length * 2.0 + 23);
|
|
22
|
-
}, 0);
|
|
23
|
-
const captionStart = 8 + terminalDuration + 20;
|
|
15
|
+
export const TerminalScene: React.FC<Props> = ({ scene, theme, bottomInset = 0 }) => {
|
|
16
|
+
const lines = terminalLines(scene);
|
|
17
|
+
// Timing is shared with the duration oracle so the scene is always long
|
|
18
|
+
// enough for the caption to appear (see timing.ts).
|
|
19
|
+
const captionStart = terminalCaptionStart(scene);
|
|
24
20
|
|
|
25
21
|
return (
|
|
26
22
|
<AbsoluteFill
|
|
27
23
|
style={{
|
|
28
|
-
background:
|
|
24
|
+
background: "transparent",
|
|
29
25
|
display: "flex",
|
|
30
26
|
alignItems: "center",
|
|
31
27
|
justifyContent: "center",
|
|
32
28
|
padding: "60px 100px",
|
|
33
29
|
}}
|
|
34
30
|
>
|
|
35
|
-
<
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
31
|
+
<Terminal
|
|
32
|
+
lines={lines}
|
|
33
|
+
theme={theme}
|
|
34
|
+
startFrame={TERMINAL_START}
|
|
35
|
+
framesPerLine={TERMINAL_FRAMES_PER_LINE}
|
|
36
|
+
framesPerChar={TERMINAL_FRAMES_PER_CHAR}
|
|
37
|
+
/>
|
|
38
|
+
{scene.caption && <Caption text={scene.caption} theme={theme} startFrame={captionStart} bottomInset={bottomInset} />}
|
|
39
39
|
</AbsoluteFill>
|
|
40
40
|
);
|
|
41
41
|
};
|
package/template/src/duration.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { FeatureListScene, FileTreeScene, HotkeyScene, OSWindowScene, Scene, StatCalloutScene } from "./brief";
|
|
1
|
+
import { BenchmarkScene, ClipScene, CodeRevealScene, FeatureListScene, FileTreeScene, HotkeyScene, OSWindowScene, Scene, StatCalloutScene, TerminalScene } from "./brief";
|
|
2
|
+
import { CAPTION_HOLD, codeRevealCaptionStart, terminalCaptionStart } from "./timing";
|
|
2
3
|
|
|
3
4
|
// Hard ceiling for the teaser cut: 10s at 30fps. Renders over the limit
|
|
4
5
|
// succeed, but the CLI prints a prominent warning.
|
|
@@ -22,11 +23,28 @@ export const SCENE_DURATION_MAP: Record<Scene["type"], number> = {
|
|
|
22
23
|
mobile: 150,
|
|
23
24
|
"os-window": 180,
|
|
24
25
|
hotkey: 120,
|
|
26
|
+
hook: 50,
|
|
27
|
+
clip: 150,
|
|
28
|
+
benchmark: 180,
|
|
25
29
|
};
|
|
26
30
|
|
|
27
31
|
export function sceneDuration(scene: Scene): number {
|
|
32
|
+
if (scene.type === "hook") return 50; // no tail: hook IS the headline, no breathing room needed
|
|
33
|
+
if (scene.type === "clip") {
|
|
34
|
+
const s = scene as ClipScene;
|
|
35
|
+
return (s.durationInFrames ?? SCENE_DURATION_MAP.clip) + SCENE_TAIL;
|
|
36
|
+
}
|
|
28
37
|
let content: number;
|
|
29
|
-
if (scene.type === "
|
|
38
|
+
if (scene.type === "terminal") {
|
|
39
|
+
// Long enough to type every command out, then show the caption (F7).
|
|
40
|
+
const s = scene as TerminalScene;
|
|
41
|
+
const start = terminalCaptionStart(s);
|
|
42
|
+
content = s.caption ? start + CAPTION_HOLD : start;
|
|
43
|
+
} else if (scene.type === "code-reveal") {
|
|
44
|
+
const s = scene as CodeRevealScene;
|
|
45
|
+
const start = codeRevealCaptionStart(s);
|
|
46
|
+
content = s.caption ? start + CAPTION_HOLD : start;
|
|
47
|
+
} else if (scene.type === "feature-list") {
|
|
30
48
|
const s = scene as FeatureListScene;
|
|
31
49
|
content = 20 + s.items.length * 25 + 60;
|
|
32
50
|
} else if (scene.type === "stat-callout") {
|
|
@@ -42,6 +60,9 @@ export function sceneDuration(scene: Scene): number {
|
|
|
42
60
|
} else if (scene.type === "hotkey") {
|
|
43
61
|
const s = scene as HotkeyScene;
|
|
44
62
|
content = 20 + s.keys.length * 20 + 70;
|
|
63
|
+
} else if (scene.type === "benchmark") {
|
|
64
|
+
const s = scene as BenchmarkScene;
|
|
65
|
+
content = 20 + s.bars.length * 30 + 60;
|
|
45
66
|
} else {
|
|
46
67
|
content = SCENE_DURATION_MAP[scene.type];
|
|
47
68
|
}
|
package/template/src/index.ts
CHANGED
|
@@ -31,19 +31,21 @@ if (!brief.cuts || !Array.isArray(brief.cuts.main) || brief.cuts.main.length ===
|
|
|
31
31
|
);
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
// A vertical platform without cuts.vertical falls back to the main
|
|
34
|
+
// A vertical platform without a (non-empty) cuts.vertical falls back to the main
|
|
35
|
+
// cut. An empty array is treated as absent so it never builds a 0-scene, and
|
|
36
|
+
// therefore 0-frame, composition that Remotion rejects (F3).
|
|
35
37
|
function scenesForCut(cut: "main" | "vertical"): Scene[] {
|
|
36
|
-
if (cut === "vertical") return brief.cuts.vertical
|
|
38
|
+
if (cut === "vertical") return brief.cuts.vertical?.length ? brief.cuts.vertical : brief.cuts.main;
|
|
37
39
|
return brief.cuts.main;
|
|
38
40
|
}
|
|
39
41
|
|
|
40
42
|
const verticalFallback = platformIds.filter(
|
|
41
|
-
(id) => cutForPlatform(id) === "vertical" && !brief.cuts.vertical
|
|
43
|
+
(id) => cutForPlatform(id) === "vertical" && !brief.cuts.vertical?.length
|
|
42
44
|
);
|
|
43
45
|
if (verticalFallback.length > 0) {
|
|
44
46
|
console.warn(
|
|
45
|
-
`reelme: no cuts.vertical in the brief — ${verticalFallback.join(", ")} will render the main cut ` +
|
|
46
|
-
`
|
|
47
|
+
`reelme: no cuts.vertical in the brief — ${verticalFallback.join(", ")} will re-render the main cut ` +
|
|
48
|
+
`at 9:16. Dense wide scenes may cramp — author a vertical cut for better results.`
|
|
47
49
|
);
|
|
48
50
|
}
|
|
49
51
|
|
|
@@ -34,3 +34,7 @@ export const PLATFORMS = presets as Record<PlatformId, PlatformPreset>;
|
|
|
34
34
|
export function cutForPlatform(id: PlatformId): "main" | "vertical" {
|
|
35
35
|
return PLATFORMS[id].cut;
|
|
36
36
|
}
|
|
37
|
+
|
|
38
|
+
export function typeScale(platform: PlatformPreset): number {
|
|
39
|
+
return platform.height > platform.width ? 1.25 : 1.0;
|
|
40
|
+
}
|