reelme 0.3.0 → 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/generate-tracks.mjs +6 -1
- package/package.json +8 -2
- package/src/cache.mjs +154 -14
- package/src/cli.mjs +20 -11
- package/src/render.mjs +91 -19
- package/template/package.json +6 -6
- package/template/pnpm-lock.yaml +177 -122
- package/template/pnpm-workspace.yaml +2 -0
- package/template/src/Root.tsx +27 -15
- package/template/src/brief.json +6 -6
- package/template/src/brief.ts +13 -3
- package/template/src/cinematic/Camera.tsx +1 -1
- package/template/src/cinematic/look.ts +25 -8
- package/template/src/cinematic/transitions.tsx +27 -9
- package/template/src/components/primitives/Bar.tsx +6 -2
- package/template/src/components/primitives/Caption.tsx +2 -2
- package/template/src/components/primitives/Icon.tsx +7 -0
- package/template/src/components/primitives/KeyPill.tsx +1 -1
- package/template/src/components/primitives/Label.tsx +4 -1
- package/template/src/components/primitives/Loop.tsx +65 -0
- package/template/src/components/primitives/RevealText.tsx +27 -5
- package/template/src/components/primitives/Stage.tsx +62 -0
- package/template/src/components/scenes/BrowserFrame.tsx +1 -1
- package/template/src/components/scenes/CTA.tsx +45 -82
- package/template/src/components/scenes/Clip.tsx +9 -3
- package/template/src/components/scenes/CodeReveal.tsx +6 -5
- package/template/src/components/scenes/DataFlow.tsx +1 -1
- package/template/src/components/scenes/FeatureList.tsx +74 -77
- package/template/src/components/scenes/FileTree.tsx +1 -1
- package/template/src/components/scenes/HotkeyScene.tsx +4 -3
- package/template/src/components/scenes/MobileScreen.tsx +222 -107
- package/template/src/components/scenes/OSWindowScene.tsx +4 -4
- package/template/src/components/scenes/Problem.tsx +1 -1
- package/template/src/components/scenes/SplitComparison.tsx +1 -1
- package/template/src/components/scenes/StatCallout.tsx +123 -68
- package/template/src/components/scenes/TerminalScene.tsx +12 -11
- package/template/src/duration.ts +12 -2
- package/template/src/index.ts +7 -5
- package/template/src/timing.ts +49 -0
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
4
|
-
import { CTAScene as CTABrief, ProjectMeta } from "../../brief";
|
|
2
|
+
import { useCurrentFrame, useVideoConfig, spring, interpolate, Img, staticFile } from "remotion";
|
|
3
|
+
import { CTAScene as CTABrief, ProjectMeta, showWatermark } from "../../brief";
|
|
5
4
|
import { Theme } from "../../theme";
|
|
6
5
|
import { PlatformPreset, typeScale } from "../../platforms";
|
|
7
|
-
import {
|
|
6
|
+
import { Stage } from "../primitives/Stage";
|
|
7
|
+
import { Terminal } from "../primitives/Terminal";
|
|
8
|
+
import { Loop } from "../primitives/Loop";
|
|
8
9
|
|
|
9
10
|
interface Props {
|
|
10
11
|
scene: CTABrief;
|
|
@@ -12,16 +13,15 @@ interface Props {
|
|
|
12
13
|
project: ProjectMeta;
|
|
13
14
|
platform?: PlatformPreset;
|
|
14
15
|
bottomInset?: number;
|
|
15
|
-
/** gif output: use a flat fill instead of gradients that bloat gif size. */
|
|
16
|
-
lite?: boolean;
|
|
17
16
|
}
|
|
18
17
|
|
|
19
18
|
const SNAP_OFFSET = 8;
|
|
20
19
|
|
|
21
|
-
export const CTA: React.FC<Props> = ({ scene, theme, project, platform, bottomInset = 0
|
|
20
|
+
export const CTA: React.FC<Props> = ({ scene, theme, project, platform, bottomInset = 0 }) => {
|
|
22
21
|
const frame = useCurrentFrame();
|
|
23
22
|
const { fps } = useVideoConfig();
|
|
24
23
|
const scale = platform ? typeScale(platform) : 1.0;
|
|
24
|
+
const isGif = platform?.output.codec === "gif";
|
|
25
25
|
|
|
26
26
|
const logoProgress = spring({ frame: frame + SNAP_OFFSET, fps, config: theme.motion });
|
|
27
27
|
const titleProgress = spring({ frame: frame + SNAP_OFFSET - (project.logo ? 16 : 0), fps, config: theme.motion });
|
|
@@ -31,101 +31,66 @@ export const CTA: React.FC<Props> = ({ scene, theme, project, platform, bottomIn
|
|
|
31
31
|
const titleOpacity = interpolate(titleProgress, [0, 1], [0, 1]);
|
|
32
32
|
const titleY = interpolate(titleProgress, [0, 1], [24, 0]);
|
|
33
33
|
const cmdOpacity = interpolate(cmdProgress, [0, 1], [0, 1]);
|
|
34
|
-
const
|
|
35
|
-
const urlFade = interpolate(urlProgress, [0, 1], [0, 0.
|
|
34
|
+
const cmdY = interpolate(cmdProgress, [0, 1], [18, 0]);
|
|
35
|
+
const urlFade = interpolate(urlProgress, [0, 1], [0, 0.65]);
|
|
36
36
|
|
|
37
37
|
const isAnnouncement = project.mode === "announcement";
|
|
38
38
|
const displayName = isAnnouncement && project.version
|
|
39
39
|
? `${project.name} ${project.version}`
|
|
40
40
|
: project.name;
|
|
41
41
|
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
const
|
|
42
|
+
// When the command starts typing inside the terminal: a beat after the title
|
|
43
|
+
// settles, so the eye lands on the headline first.
|
|
44
|
+
const termStart = project.logo ? 26 : 16;
|
|
45
45
|
|
|
46
46
|
return (
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
// gif path uses a flat fill — a full-frame gradient explodes gif size.
|
|
52
|
-
background: lite
|
|
53
|
-
? theme.accent
|
|
54
|
-
: `radial-gradient(125% 95% at 50% 32%, color-mix(in srgb, ${theme.accent}, #fff 12%) 0%, ${theme.accent} 46%, color-mix(in srgb, ${theme.accent}, #000 24%) 100%)`,
|
|
55
|
-
display: "flex",
|
|
56
|
-
flexDirection: "column",
|
|
57
|
-
alignItems: "center",
|
|
58
|
-
justifyContent: "center",
|
|
59
|
-
gap: 32,
|
|
60
|
-
padding: "0 120px",
|
|
61
|
-
}}
|
|
62
|
-
>
|
|
63
|
-
{/* Edge vignette spotlights the lockup so the field doesn't read as empty.
|
|
64
|
-
Skipped on the gif path to keep the frame flat and compressible. */}
|
|
65
|
-
{!lite && (
|
|
66
|
-
<AbsoluteFill
|
|
67
|
-
style={{
|
|
68
|
-
background: "radial-gradient(70% 70% at 50% 50%, transparent 40%, rgba(0,0,0,0.28) 100%)",
|
|
69
|
-
pointerEvents: "none",
|
|
70
|
-
}}
|
|
71
|
-
/>
|
|
72
|
-
)}
|
|
73
|
-
|
|
47
|
+
// Stay on the film's continuous atmosphere stage instead of flooding the
|
|
48
|
+
// frame with a flat brand field — the end-card belongs to the same shot as
|
|
49
|
+
// the rest of the reel. Stage's transparent root lets the Atmosphere show.
|
|
50
|
+
<Stage theme={theme} gap={40} caption={scene.caption} captionStart={60} bottomInset={bottomInset}>
|
|
74
51
|
{project.logo && (
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
52
|
+
// A barely-there breathe keeps the end-card's hero alive while it holds,
|
|
53
|
+
// after the entrance spring has settled. Off for gif (defeats compression).
|
|
54
|
+
<Loop property="scale" amplitude={0.012} period={4} disabled={isGif}>
|
|
55
|
+
<div
|
|
56
|
+
style={{
|
|
57
|
+
opacity: interpolate(logoProgress, [0, 1], [0, 1]),
|
|
58
|
+
scale: String(interpolate(logoProgress, [0, 1], [0.85, 1])),
|
|
59
|
+
background: "#ffffff",
|
|
60
|
+
borderRadius: 20,
|
|
61
|
+
padding: 16,
|
|
62
|
+
boxShadow: "0 12px 40px rgba(0,0,0,0.35)",
|
|
63
|
+
}}
|
|
64
|
+
>
|
|
65
|
+
<Img
|
|
66
|
+
src={staticFile(project.logo)}
|
|
67
|
+
style={{ height: 80, width: "auto", objectFit: "contain", display: "block" }}
|
|
68
|
+
/>
|
|
69
|
+
</div>
|
|
70
|
+
</Loop>
|
|
90
71
|
)}
|
|
91
72
|
|
|
92
73
|
<div
|
|
93
74
|
style={{
|
|
94
75
|
opacity: titleOpacity,
|
|
95
|
-
|
|
76
|
+
translate: `0 ${titleY}px`,
|
|
96
77
|
fontFamily: theme.fontSans,
|
|
97
78
|
fontSize: 60 * scale,
|
|
98
79
|
fontWeight: 700,
|
|
99
|
-
color:
|
|
80
|
+
color: theme.text,
|
|
100
81
|
letterSpacing: "-0.03em",
|
|
101
82
|
textAlign: "center",
|
|
102
|
-
textShadow: "0 4px 30px rgba(0,0,0,0.25)",
|
|
103
83
|
}}
|
|
104
84
|
>
|
|
105
85
|
{isAnnouncement ? "" : "Get started with "}
|
|
106
|
-
<span style={{ color:
|
|
86
|
+
<span style={{ color: theme.accent, fontWeight: 800 }}>{displayName}</span>
|
|
107
87
|
{isAnnouncement ? " is here." : ""}
|
|
108
88
|
</div>
|
|
109
89
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
background: theme.bg,
|
|
115
|
-
border: `1.5px solid rgba(0,0,0,0.12)`,
|
|
116
|
-
borderRadius: 14,
|
|
117
|
-
padding: "20px 44px",
|
|
118
|
-
fontFamily: theme.fontMono,
|
|
119
|
-
fontSize: 28,
|
|
120
|
-
color: theme.text,
|
|
121
|
-
display: "flex",
|
|
122
|
-
alignItems: "center",
|
|
123
|
-
gap: 16,
|
|
124
|
-
boxShadow: "0 22px 60px rgba(0,0,0,0.30)",
|
|
125
|
-
}}
|
|
126
|
-
>
|
|
127
|
-
<span style={{ color: theme.accent }}>$</span>
|
|
128
|
-
{scene.installCommand}
|
|
90
|
+
{/* The install command in the film's own terminal, typed out — consistent
|
|
91
|
+
with the reel's other terminal scenes instead of a web-style pill. */}
|
|
92
|
+
<div style={{ opacity: cmdOpacity, translate: `0 ${cmdY}px` }}>
|
|
93
|
+
<Terminal lines={[{ text: scene.installCommand }]} theme={theme} startFrame={termStart} />
|
|
129
94
|
</div>
|
|
130
95
|
|
|
131
96
|
{scene.repoUrl && (
|
|
@@ -134,16 +99,14 @@ export const CTA: React.FC<Props> = ({ scene, theme, project, platform, bottomIn
|
|
|
134
99
|
opacity: urlFade,
|
|
135
100
|
fontFamily: theme.fontMono,
|
|
136
101
|
fontSize: 20,
|
|
137
|
-
color:
|
|
102
|
+
color: theme.textMuted,
|
|
138
103
|
}}
|
|
139
104
|
>
|
|
140
105
|
{scene.repoUrl}
|
|
141
106
|
</div>
|
|
142
107
|
)}
|
|
143
108
|
|
|
144
|
-
{
|
|
145
|
-
|
|
146
|
-
{project.watermark !== false && (
|
|
109
|
+
{showWatermark(project.watermark) && (
|
|
147
110
|
<div
|
|
148
111
|
style={{
|
|
149
112
|
position: "absolute",
|
|
@@ -159,6 +122,6 @@ export const CTA: React.FC<Props> = ({ scene, theme, project, platform, bottomIn
|
|
|
159
122
|
made with reelme
|
|
160
123
|
</div>
|
|
161
124
|
)}
|
|
162
|
-
</
|
|
125
|
+
</Stage>
|
|
163
126
|
);
|
|
164
127
|
};
|
|
@@ -8,6 +8,7 @@ import { Caption } from "../primitives/Caption";
|
|
|
8
8
|
interface Props {
|
|
9
9
|
scene: ClipScene;
|
|
10
10
|
theme: Theme;
|
|
11
|
+
bottomInset?: number;
|
|
11
12
|
}
|
|
12
13
|
|
|
13
14
|
const isGif = (src: string) => src.endsWith(".gif");
|
|
@@ -22,12 +23,17 @@ const MediaElement: React.FC<{ scene: ClipScene }> = ({ scene }) => {
|
|
|
22
23
|
/>
|
|
23
24
|
);
|
|
24
25
|
}
|
|
26
|
+
// Stop the source at the requested trim point so the scene's tail doesn't keep
|
|
27
|
+
// playing past what the brief asked for (F23).
|
|
28
|
+
const endAt =
|
|
29
|
+
scene.durationInFrames !== undefined ? (scene.startFrom ?? 0) + scene.durationInFrames : undefined;
|
|
25
30
|
return (
|
|
26
31
|
<OffthreadVideo
|
|
27
32
|
src={staticFile(scene.src)}
|
|
28
33
|
style={{ width: "100%", height: "100%", objectFit: "cover" }}
|
|
29
34
|
muted
|
|
30
35
|
startFrom={scene.startFrom}
|
|
36
|
+
endAt={endAt}
|
|
31
37
|
/>
|
|
32
38
|
);
|
|
33
39
|
};
|
|
@@ -79,7 +85,7 @@ const MobileChrome: React.FC<{ children: React.ReactNode; theme: Theme }> = ({ c
|
|
|
79
85
|
</div>
|
|
80
86
|
);
|
|
81
87
|
|
|
82
|
-
export const Clip: React.FC<Props> = ({ scene, theme }) => {
|
|
88
|
+
export const Clip: React.FC<Props> = ({ scene, theme, bottomInset = 0 }) => {
|
|
83
89
|
const renderMedia = () => <MediaElement scene={scene} />;
|
|
84
90
|
|
|
85
91
|
if (scene.frame === "browser") {
|
|
@@ -94,7 +100,7 @@ export const Clip: React.FC<Props> = ({ scene, theme }) => {
|
|
|
94
100
|
}}
|
|
95
101
|
>
|
|
96
102
|
<BrowserChrome theme={theme}>{renderMedia()}</BrowserChrome>
|
|
97
|
-
{scene.caption && <Caption text={scene.caption} theme={theme} startFrame={30} />}
|
|
103
|
+
{scene.caption && <Caption text={scene.caption} theme={theme} startFrame={30} bottomInset={bottomInset} />}
|
|
98
104
|
</AbsoluteFill>
|
|
99
105
|
);
|
|
100
106
|
}
|
|
@@ -111,7 +117,7 @@ export const Clip: React.FC<Props> = ({ scene, theme }) => {
|
|
|
111
117
|
}}
|
|
112
118
|
>
|
|
113
119
|
<MobileChrome theme={theme}>{renderMedia()}</MobileChrome>
|
|
114
|
-
{scene.caption && <Caption text={scene.caption} theme={theme} startFrame={30} />}
|
|
120
|
+
{scene.caption && <Caption text={scene.caption} theme={theme} startFrame={30} bottomInset={bottomInset} />}
|
|
115
121
|
</AbsoluteFill>
|
|
116
122
|
);
|
|
117
123
|
}
|
|
@@ -4,6 +4,7 @@ import { CodeRevealScene as CodeRevealBrief } from "../../brief";
|
|
|
4
4
|
import { Theme } from "../../theme";
|
|
5
5
|
import { CodeBlock } from "../primitives/CodeBlock";
|
|
6
6
|
import { Caption } from "../primitives/Caption";
|
|
7
|
+
import { CODE_FRAMES_PER_LINE, CODE_START, codeRevealCaptionStart } from "../../timing";
|
|
7
8
|
|
|
8
9
|
interface Props {
|
|
9
10
|
scene: CodeRevealBrief;
|
|
@@ -12,9 +13,9 @@ interface Props {
|
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
export const CodeReveal: React.FC<Props> = ({ scene, theme, bottomInset = 0 }) => {
|
|
15
|
-
|
|
16
|
-
//
|
|
17
|
-
const captionStart =
|
|
16
|
+
// Timing is shared with the duration oracle so the scene lasts long enough for
|
|
17
|
+
// every line to reveal and the caption to appear (see timing.ts).
|
|
18
|
+
const captionStart = codeRevealCaptionStart(scene);
|
|
18
19
|
|
|
19
20
|
return (
|
|
20
21
|
<AbsoluteFill
|
|
@@ -32,8 +33,8 @@ export const CodeReveal: React.FC<Props> = ({ scene, theme, bottomInset = 0 }) =
|
|
|
32
33
|
language={scene.language}
|
|
33
34
|
highlightLine={scene.highlightLine}
|
|
34
35
|
theme={theme}
|
|
35
|
-
startFrame={
|
|
36
|
-
framesPerLine={
|
|
36
|
+
startFrame={CODE_START}
|
|
37
|
+
framesPerLine={CODE_FRAMES_PER_LINE}
|
|
37
38
|
/>
|
|
38
39
|
</div>
|
|
39
40
|
{scene.caption && <Caption text={scene.caption} theme={theme} startFrame={captionStart} bottomInset={bottomInset} />}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
import {
|
|
2
|
+
import { useCurrentFrame, useVideoConfig, spring, interpolate } from "remotion";
|
|
3
3
|
import { FeatureListScene as FeatureListBrief } from "../../brief";
|
|
4
4
|
import { Theme } from "../../theme";
|
|
5
5
|
import { PlatformPreset, typeScale } from "../../platforms";
|
|
6
6
|
import { Label } from "../primitives/Label";
|
|
7
|
-
import {
|
|
8
|
-
import { Icon } from "../primitives/Icon";
|
|
7
|
+
import { Stage } from "../primitives/Stage";
|
|
8
|
+
import { Icon, hasIcon } from "../primitives/Icon";
|
|
9
9
|
|
|
10
10
|
interface Props {
|
|
11
11
|
scene: FeatureListBrief;
|
|
@@ -22,94 +22,91 @@ export const FeatureList: React.FC<Props> = ({ scene, theme, platform, bottomIns
|
|
|
22
22
|
const { fps } = useVideoConfig();
|
|
23
23
|
const scale = platform ? typeScale(platform) : 1.0;
|
|
24
24
|
|
|
25
|
-
const
|
|
25
|
+
const items = scene.items ?? [];
|
|
26
|
+
const captionStart = HEADLINE_FRAMES + items.length * FRAMES_PER_ITEM + 20;
|
|
26
27
|
const left = scene.align === "left";
|
|
27
28
|
|
|
28
29
|
return (
|
|
29
|
-
<
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
<div style={{ marginBottom: 48 }}>
|
|
41
|
-
<Label text={scene.headline} theme={theme} size="lg" align={left ? "left" : "center"} startFrame={0} />
|
|
42
|
-
</div>
|
|
43
|
-
)}
|
|
30
|
+
<Stage theme={theme} caption={scene.caption} captionStart={captionStart} bottomInset={bottomInset}>
|
|
31
|
+
{/* The list is a single block, centered in the frame. Even a short
|
|
32
|
+
left-aligned list then sits in symmetric margins (composed negative
|
|
33
|
+
space) instead of being anchored against one edge with a void opposite.
|
|
34
|
+
Items left-align within the block for a clean reading column. */}
|
|
35
|
+
<div style={{ display: "flex", flexDirection: "column", alignItems: "flex-start" }}>
|
|
36
|
+
{scene.headline && (
|
|
37
|
+
<div style={{ marginBottom: 52, alignSelf: left ? "flex-start" : "center" }}>
|
|
38
|
+
<Label text={scene.headline} theme={theme} size="lg" align={left ? "left" : "center"} startFrame={0} />
|
|
39
|
+
</div>
|
|
40
|
+
)}
|
|
44
41
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
42
|
+
<div style={{ display: "flex", flexDirection: "column", gap: 34 }}>
|
|
43
|
+
{items.map((item, i) => {
|
|
44
|
+
const text = typeof item === "string" ? item : item.text;
|
|
45
|
+
const icon = typeof item === "string" ? undefined : item.icon;
|
|
46
|
+
const showIcon = icon !== undefined && hasIcon(icon);
|
|
49
47
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
48
|
+
const itemStart = HEADLINE_FRAMES + i * FRAMES_PER_ITEM;
|
|
49
|
+
const progress = spring({ frame: frame - itemStart, fps, config: theme.motion });
|
|
50
|
+
const opacity = interpolate(Math.max(0, progress), [0, 1], [0, 1]);
|
|
51
|
+
const translateX = interpolate(Math.max(0, progress), [0, 1], [-24, 0]);
|
|
54
52
|
|
|
55
|
-
|
|
56
|
-
<div
|
|
57
|
-
key={i}
|
|
58
|
-
style={{
|
|
59
|
-
display: "flex",
|
|
60
|
-
alignItems: "center",
|
|
61
|
-
gap: 24,
|
|
62
|
-
opacity,
|
|
63
|
-
transform: `translateX(${translateX}px)`,
|
|
64
|
-
}}
|
|
65
|
-
>
|
|
53
|
+
return (
|
|
66
54
|
<div
|
|
55
|
+
key={i}
|
|
67
56
|
style={{
|
|
68
|
-
width: 36,
|
|
69
|
-
height: 36,
|
|
70
|
-
borderRadius: "50%",
|
|
71
|
-
background: theme.accentMuted,
|
|
72
|
-
border: `1.5px solid ${theme.accent}`,
|
|
73
57
|
display: "flex",
|
|
74
58
|
alignItems: "center",
|
|
75
|
-
|
|
76
|
-
|
|
59
|
+
gap: 26,
|
|
60
|
+
opacity,
|
|
61
|
+
translate: `${translateX}px 0`,
|
|
77
62
|
}}
|
|
78
63
|
>
|
|
79
|
-
{
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
{
|
|
92
|
-
|
|
93
|
-
|
|
64
|
+
{/* A bold accent marker carries the weight — no faint bordered
|
|
65
|
+
chip (a web-UI badge pattern that reads as filler on screen). */}
|
|
66
|
+
<div
|
|
67
|
+
style={{
|
|
68
|
+
width: 40,
|
|
69
|
+
display: "flex",
|
|
70
|
+
alignItems: "center",
|
|
71
|
+
justifyContent: "center",
|
|
72
|
+
flexShrink: 0,
|
|
73
|
+
}}
|
|
74
|
+
>
|
|
75
|
+
{showIcon ? (
|
|
76
|
+
<Icon name={icon} size={34} color={theme.accent} />
|
|
77
|
+
) : (
|
|
78
|
+
<span
|
|
79
|
+
style={{
|
|
80
|
+
fontFamily: theme.fontSans,
|
|
81
|
+
fontSize: 30,
|
|
82
|
+
fontWeight: 800,
|
|
83
|
+
color: theme.accent,
|
|
84
|
+
lineHeight: 1,
|
|
85
|
+
letterSpacing: "-0.03em",
|
|
86
|
+
}}
|
|
87
|
+
>
|
|
88
|
+
{i + 1}
|
|
89
|
+
</span>
|
|
90
|
+
)}
|
|
91
|
+
</div>
|
|
92
|
+
<span
|
|
93
|
+
style={{
|
|
94
|
+
fontFamily: theme.fontSans,
|
|
95
|
+
fontSize: 40 * scale,
|
|
96
|
+
fontWeight: 600,
|
|
97
|
+
color: theme.text,
|
|
98
|
+
lineHeight: 1.3,
|
|
99
|
+
letterSpacing: "-0.015em",
|
|
100
|
+
}}
|
|
101
|
+
>
|
|
102
|
+
{text}
|
|
103
|
+
</span>
|
|
94
104
|
</div>
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
fontSize: 28 * scale,
|
|
99
|
-
fontWeight: 500,
|
|
100
|
-
color: theme.text,
|
|
101
|
-
lineHeight: 1.4,
|
|
102
|
-
letterSpacing: "-0.01em",
|
|
103
|
-
}}
|
|
104
|
-
>
|
|
105
|
-
{text}
|
|
106
|
-
</span>
|
|
107
|
-
</div>
|
|
108
|
-
);
|
|
109
|
-
})}
|
|
105
|
+
);
|
|
106
|
+
})}
|
|
107
|
+
</div>
|
|
110
108
|
</div>
|
|
111
109
|
|
|
112
|
-
|
|
113
|
-
</AbsoluteFill>
|
|
110
|
+
</Stage>
|
|
114
111
|
);
|
|
115
112
|
};
|
|
@@ -93,7 +93,7 @@ export const FileTree: React.FC<Props> = ({ scene, theme, bottomInset = 0 }) =>
|
|
|
93
93
|
alignItems: "center",
|
|
94
94
|
gap: 10,
|
|
95
95
|
opacity,
|
|
96
|
-
|
|
96
|
+
translate: `${translateX}px 0`,
|
|
97
97
|
paddingTop: isHighlighted ? 6 : 4,
|
|
98
98
|
paddingBottom: isHighlighted ? 6 : 4,
|
|
99
99
|
paddingLeft: depth * INDENT + 8,
|
|
@@ -18,7 +18,8 @@ export const Hotkey: React.FC<Props> = ({ scene, theme, bottomInset = 0 }) => {
|
|
|
18
18
|
const frame = useCurrentFrame();
|
|
19
19
|
const { fps } = useVideoConfig();
|
|
20
20
|
|
|
21
|
-
const
|
|
21
|
+
const keys = scene.keys ?? [];
|
|
22
|
+
const actionStart = keys.length * FRAMES_PER_KEY + ACTION_DELAY;
|
|
22
23
|
const captionStart = actionStart + 50;
|
|
23
24
|
|
|
24
25
|
const actionProgress = spring({ frame: frame - actionStart, fps, config: theme.motion });
|
|
@@ -37,7 +38,7 @@ export const Hotkey: React.FC<Props> = ({ scene, theme, bottomInset = 0 }) => {
|
|
|
37
38
|
}}
|
|
38
39
|
>
|
|
39
40
|
<div style={{ display: "flex", alignItems: "center", gap: 12 }}>
|
|
40
|
-
{
|
|
41
|
+
{keys.map((key, i) => (
|
|
41
42
|
<React.Fragment key={i}>
|
|
42
43
|
{i > 0 && (
|
|
43
44
|
<span
|
|
@@ -60,7 +61,7 @@ export const Hotkey: React.FC<Props> = ({ scene, theme, bottomInset = 0 }) => {
|
|
|
60
61
|
<div
|
|
61
62
|
style={{
|
|
62
63
|
opacity: actionOpacity,
|
|
63
|
-
|
|
64
|
+
translate: `0 ${actionY}px`,
|
|
64
65
|
fontFamily: theme.fontSans,
|
|
65
66
|
fontSize: 28,
|
|
66
67
|
fontWeight: 400,
|