reelme 0.2.2 → 0.3.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/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 +213 -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 +2 -1
- package/src/cache.mjs +1 -0
- package/src/render.mjs +95 -4
- package/template/package.json +1 -0
- package/template/pnpm-lock.yaml +15 -0
- package/template/src/Root.tsx +55 -45
- package/template/src/audio.ts +24 -0
- package/template/src/benchmark.ts +18 -0
- package/template/src/brief.json +1 -0
- package/template/src/brief.ts +61 -3
- package/template/src/cinematic/Atmosphere.tsx +139 -0
- package/template/src/cinematic/Camera.tsx +54 -0
- package/template/src/cinematic/look.ts +89 -0
- package/template/src/cinematic/transitions.tsx +92 -0
- package/template/src/components/primitives/Bar.tsx +82 -0
- package/template/src/components/primitives/Caption.tsx +3 -2
- package/template/src/components/primitives/Kicker.tsx +46 -0
- package/template/src/components/primitives/Label.tsx +18 -24
- package/template/src/components/primitives/RevealText.tsx +101 -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 +4 -3
- package/template/src/components/scenes/CTA.tsx +56 -10
- package/template/src/components/scenes/Clip.tsx +125 -0
- package/template/src/components/scenes/CodeReveal.tsx +4 -3
- package/template/src/components/scenes/DataFlow.tsx +4 -3
- package/template/src/components/scenes/FeatureList.tsx +13 -8
- package/template/src/components/scenes/FileTree.tsx +4 -3
- package/template/src/components/scenes/Hook.tsx +55 -0
- package/template/src/components/scenes/HotkeyScene.tsx +4 -3
- package/template/src/components/scenes/MobileScreen.tsx +120 -82
- package/template/src/components/scenes/OSWindowScene.tsx +4 -3
- package/template/src/components/scenes/Problem.tsx +28 -29
- package/template/src/components/scenes/SplitComparison.tsx +65 -92
- package/template/src/components/scenes/StatCallout.tsx +93 -4
- package/template/src/components/scenes/TerminalScene.tsx +5 -6
- package/template/src/duration.ts +12 -1
- package/template/src/platforms.ts +4 -0
|
@@ -8,6 +8,7 @@ import { Caption } from "../primitives/Caption";
|
|
|
8
8
|
interface Props {
|
|
9
9
|
scene: OSWindowBrief;
|
|
10
10
|
theme: Theme;
|
|
11
|
+
bottomInset?: number;
|
|
11
12
|
}
|
|
12
13
|
|
|
13
14
|
const WINDOW_WIDTH = 600;
|
|
@@ -18,7 +19,7 @@ const FRAMES_PER_ITEM = 20;
|
|
|
18
19
|
|
|
19
20
|
const TRAFFIC_LIGHTS = ["#ff5f57", "#ffbd2e", "#28c941"] as const;
|
|
20
21
|
|
|
21
|
-
export const OSWindow: React.FC<Props> = ({ scene, theme }) => {
|
|
22
|
+
export const OSWindow: React.FC<Props> = ({ scene, theme, bottomInset = 0 }) => {
|
|
22
23
|
const frame = useCurrentFrame();
|
|
23
24
|
const { fps } = useVideoConfig();
|
|
24
25
|
|
|
@@ -37,7 +38,7 @@ export const OSWindow: React.FC<Props> = ({ scene, theme }) => {
|
|
|
37
38
|
|
|
38
39
|
return (
|
|
39
40
|
<AbsoluteFill
|
|
40
|
-
style={{ background:
|
|
41
|
+
style={{ background: "transparent", display: "flex", alignItems: "center", justifyContent: "center" }}
|
|
41
42
|
>
|
|
42
43
|
<div
|
|
43
44
|
style={{
|
|
@@ -160,7 +161,7 @@ export const OSWindow: React.FC<Props> = ({ scene, theme }) => {
|
|
|
160
161
|
</div>
|
|
161
162
|
</div>
|
|
162
163
|
|
|
163
|
-
{scene.caption && <Caption text={scene.caption} theme={theme} startFrame={captionStart} />}
|
|
164
|
+
{scene.caption && <Caption text={scene.caption} theme={theme} startFrame={captionStart} bottomInset={bottomInset} />}
|
|
164
165
|
</AbsoluteFill>
|
|
165
166
|
);
|
|
166
167
|
};
|
|
@@ -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,16 +37,18 @@ 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,
|
|
@@ -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
|
+
transform: `translateX(${x}px)`,
|
|
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
|
};
|
|
@@ -8,22 +8,27 @@ import { Caption } from "../primitives/Caption";
|
|
|
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
29
|
<AbsoluteFill
|
|
25
30
|
style={{
|
|
26
|
-
background:
|
|
31
|
+
background: "transparent",
|
|
27
32
|
display: "flex",
|
|
28
33
|
flexDirection: "column",
|
|
29
34
|
alignItems: "center",
|
|
@@ -104,7 +109,91 @@ export const StatCallout: React.FC<Props> = ({ scene, theme }) => {
|
|
|
104
109
|
})}
|
|
105
110
|
</div>
|
|
106
111
|
|
|
107
|
-
{scene.caption && <Caption text={scene.caption} theme={theme} startFrame={captionStart} />}
|
|
112
|
+
{scene.caption && <Caption text={scene.caption} theme={theme} startFrame={captionStart} bottomInset={bottomInset} />}
|
|
113
|
+
</AbsoluteFill>
|
|
114
|
+
);
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
/** One dominant number filling the frame; supporting stats sit small below. */
|
|
118
|
+
const HeroStat: React.FC<{
|
|
119
|
+
scene: StatCalloutBrief;
|
|
120
|
+
theme: Theme;
|
|
121
|
+
bottomInset: number;
|
|
122
|
+
width: number;
|
|
123
|
+
frame: number;
|
|
124
|
+
fps: number;
|
|
125
|
+
}> = ({ scene, theme, bottomInset, width, frame, fps }) => {
|
|
126
|
+
const [hero, ...rest] = scene.stats;
|
|
127
|
+
const p = spring({ frame: frame + SNAP_OFFSET, fps, config: theme.motion });
|
|
128
|
+
const valueScale = interpolate(p, [0, 1], [0.6, 1]);
|
|
129
|
+
const valueOpacity = interpolate(p, [0, 1], [0, 1]);
|
|
130
|
+
const labelP = spring({ frame: frame - 10 + SNAP_OFFSET, fps, config: theme.motion });
|
|
131
|
+
|
|
132
|
+
return (
|
|
133
|
+
<AbsoluteFill
|
|
134
|
+
style={{
|
|
135
|
+
background: "transparent",
|
|
136
|
+
display: "flex",
|
|
137
|
+
flexDirection: "column",
|
|
138
|
+
alignItems: "flex-start",
|
|
139
|
+
justifyContent: "center",
|
|
140
|
+
padding: "0 0 0 140px",
|
|
141
|
+
}}
|
|
142
|
+
>
|
|
143
|
+
{scene.headline && (
|
|
144
|
+
<div style={{ marginBottom: 8 }}>
|
|
145
|
+
<Label text={scene.headline} theme={theme} size="md" align="left" startFrame={0} muted />
|
|
146
|
+
</div>
|
|
147
|
+
)}
|
|
148
|
+
<div
|
|
149
|
+
style={{
|
|
150
|
+
fontFamily: theme.fontSans,
|
|
151
|
+
fontSize: width * 0.2,
|
|
152
|
+
fontWeight: 800,
|
|
153
|
+
color: theme.accent,
|
|
154
|
+
lineHeight: 0.92,
|
|
155
|
+
letterSpacing: "-0.05em",
|
|
156
|
+
opacity: valueOpacity,
|
|
157
|
+
transform: `scale(${valueScale})`,
|
|
158
|
+
transformOrigin: "left center",
|
|
159
|
+
}}
|
|
160
|
+
>
|
|
161
|
+
{hero?.value}
|
|
162
|
+
</div>
|
|
163
|
+
{hero?.label && (
|
|
164
|
+
<div
|
|
165
|
+
style={{
|
|
166
|
+
marginTop: 18,
|
|
167
|
+
fontFamily: theme.fontSans,
|
|
168
|
+
fontSize: 34,
|
|
169
|
+
fontWeight: 600,
|
|
170
|
+
color: theme.text,
|
|
171
|
+
maxWidth: width * 0.6,
|
|
172
|
+
opacity: interpolate(labelP, [0, 1], [0, 1]),
|
|
173
|
+
transform: `translateY(${interpolate(labelP, [0, 1], [16, 0])}px)`,
|
|
174
|
+
}}
|
|
175
|
+
>
|
|
176
|
+
{hero?.label}
|
|
177
|
+
</div>
|
|
178
|
+
)}
|
|
179
|
+
{rest.length > 0 && (
|
|
180
|
+
<div style={{ display: "flex", gap: 56, marginTop: 48 }}>
|
|
181
|
+
{rest.map((s, i) => {
|
|
182
|
+
const rp = spring({ frame: frame - 24 - i * 8 + SNAP_OFFSET, fps, config: theme.motion });
|
|
183
|
+
return (
|
|
184
|
+
<div key={i} style={{ opacity: interpolate(rp, [0, 1], [0, 1]) }}>
|
|
185
|
+
<div style={{ fontFamily: theme.fontSans, fontSize: 44, fontWeight: 800, color: theme.text, letterSpacing: "-0.03em" }}>
|
|
186
|
+
{s.value}
|
|
187
|
+
</div>
|
|
188
|
+
<div style={{ fontFamily: theme.fontSans, fontSize: 18, fontWeight: 500, color: theme.textMuted, textTransform: "uppercase", letterSpacing: "0.04em" }}>
|
|
189
|
+
{s.label}
|
|
190
|
+
</div>
|
|
191
|
+
</div>
|
|
192
|
+
);
|
|
193
|
+
})}
|
|
194
|
+
</div>
|
|
195
|
+
)}
|
|
196
|
+
{scene.caption && <Caption text={scene.caption} theme={theme} startFrame={50} bottomInset={bottomInset} />}
|
|
108
197
|
</AbsoluteFill>
|
|
109
198
|
);
|
|
110
199
|
};
|
|
@@ -8,9 +8,10 @@ import { Caption } from "../primitives/Caption";
|
|
|
8
8
|
interface Props {
|
|
9
9
|
scene: TerminalBrief;
|
|
10
10
|
theme: Theme;
|
|
11
|
+
bottomInset?: number;
|
|
11
12
|
}
|
|
12
13
|
|
|
13
|
-
export const TerminalScene: React.FC<Props> = ({ scene, theme }) => {
|
|
14
|
+
export const TerminalScene: React.FC<Props> = ({ scene, theme, bottomInset = 0 }) => {
|
|
14
15
|
const lines = scene.commands.flatMap((cmd) => [
|
|
15
16
|
{ text: cmd.input, isOutput: false },
|
|
16
17
|
{ text: cmd.output, isOutput: true },
|
|
@@ -25,17 +26,15 @@ export const TerminalScene: React.FC<Props> = ({ scene, theme }) => {
|
|
|
25
26
|
return (
|
|
26
27
|
<AbsoluteFill
|
|
27
28
|
style={{
|
|
28
|
-
background:
|
|
29
|
+
background: "transparent",
|
|
29
30
|
display: "flex",
|
|
30
31
|
alignItems: "center",
|
|
31
32
|
justifyContent: "center",
|
|
32
33
|
padding: "60px 100px",
|
|
33
34
|
}}
|
|
34
35
|
>
|
|
35
|
-
<
|
|
36
|
-
|
|
37
|
-
</div>
|
|
38
|
-
{scene.caption && <Caption text={scene.caption} theme={theme} startFrame={captionStart} />}
|
|
36
|
+
<Terminal lines={lines} theme={theme} startFrame={8} />
|
|
37
|
+
{scene.caption && <Caption text={scene.caption} theme={theme} startFrame={captionStart} bottomInset={bottomInset} />}
|
|
39
38
|
</AbsoluteFill>
|
|
40
39
|
);
|
|
41
40
|
};
|
package/template/src/duration.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { FeatureListScene, FileTreeScene, HotkeyScene, OSWindowScene, Scene, StatCalloutScene } from "./brief";
|
|
1
|
+
import { BenchmarkScene, ClipScene, FeatureListScene, FileTreeScene, HotkeyScene, OSWindowScene, Scene, StatCalloutScene } from "./brief";
|
|
2
2
|
|
|
3
3
|
// Hard ceiling for the teaser cut: 10s at 30fps. Renders over the limit
|
|
4
4
|
// succeed, but the CLI prints a prominent warning.
|
|
@@ -22,9 +22,17 @@ export const SCENE_DURATION_MAP: Record<Scene["type"], number> = {
|
|
|
22
22
|
mobile: 150,
|
|
23
23
|
"os-window": 180,
|
|
24
24
|
hotkey: 120,
|
|
25
|
+
hook: 50,
|
|
26
|
+
clip: 150,
|
|
27
|
+
benchmark: 180,
|
|
25
28
|
};
|
|
26
29
|
|
|
27
30
|
export function sceneDuration(scene: Scene): number {
|
|
31
|
+
if (scene.type === "hook") return 50; // no tail: hook IS the headline, no breathing room needed
|
|
32
|
+
if (scene.type === "clip") {
|
|
33
|
+
const s = scene as ClipScene;
|
|
34
|
+
return (s.durationInFrames ?? SCENE_DURATION_MAP.clip) + SCENE_TAIL;
|
|
35
|
+
}
|
|
28
36
|
let content: number;
|
|
29
37
|
if (scene.type === "feature-list") {
|
|
30
38
|
const s = scene as FeatureListScene;
|
|
@@ -42,6 +50,9 @@ export function sceneDuration(scene: Scene): number {
|
|
|
42
50
|
} else if (scene.type === "hotkey") {
|
|
43
51
|
const s = scene as HotkeyScene;
|
|
44
52
|
content = 20 + s.keys.length * 20 + 70;
|
|
53
|
+
} else if (scene.type === "benchmark") {
|
|
54
|
+
const s = scene as BenchmarkScene;
|
|
55
|
+
content = 20 + s.bars.length * 30 + 60;
|
|
45
56
|
} else {
|
|
46
57
|
content = SCENE_DURATION_MAP[scene.type];
|
|
47
58
|
}
|
|
@@ -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
|
+
}
|