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
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { AbsoluteFill } from "remotion";
|
|
3
|
+
import { Theme } from "../../theme";
|
|
4
|
+
import { Caption } from "./Caption";
|
|
5
|
+
|
|
6
|
+
// Standard horizontal safe inset for scene content — keeps text off the frame
|
|
7
|
+
// edges (the video-layout rules want key content ~120px in on a 1080–1920 frame).
|
|
8
|
+
export const STAGE_INSET = 120;
|
|
9
|
+
|
|
10
|
+
interface StageProps {
|
|
11
|
+
theme: Theme;
|
|
12
|
+
/** Stacking direction of the content. */
|
|
13
|
+
direction?: "row" | "column";
|
|
14
|
+
justify?: React.CSSProperties["justifyContent"];
|
|
15
|
+
align?: React.CSSProperties["alignItems"];
|
|
16
|
+
gap?: number;
|
|
17
|
+
/** Horizontal padding; defaults to the standard safe inset on both sides. */
|
|
18
|
+
padding?: React.CSSProperties["padding"];
|
|
19
|
+
/** Optional caption rendered as the film's bottom pill. */
|
|
20
|
+
caption?: string;
|
|
21
|
+
captionStart?: number;
|
|
22
|
+
bottomInset?: number;
|
|
23
|
+
children: React.ReactNode;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The shared scene root. Every scene is shot on the one continuous Atmosphere, so
|
|
28
|
+
* the root is ALWAYS transparent — never paint an opaque full-frame fill here or
|
|
29
|
+
* the stage behind it disappears and the reel stops reading as one film. Content
|
|
30
|
+
* is centered by default so leftover space stays symmetric (composed negative
|
|
31
|
+
* space) instead of being dumped against one edge as a void. For a hero + aside
|
|
32
|
+
* composition use `direction="row"` with `justify="space-between"`.
|
|
33
|
+
*/
|
|
34
|
+
export const Stage: React.FC<StageProps> = ({
|
|
35
|
+
theme,
|
|
36
|
+
direction = "column",
|
|
37
|
+
justify = "center",
|
|
38
|
+
align = "center",
|
|
39
|
+
gap,
|
|
40
|
+
padding,
|
|
41
|
+
caption,
|
|
42
|
+
captionStart = 50,
|
|
43
|
+
bottomInset = 0,
|
|
44
|
+
children,
|
|
45
|
+
}) => {
|
|
46
|
+
return (
|
|
47
|
+
<AbsoluteFill
|
|
48
|
+
style={{
|
|
49
|
+
background: "transparent",
|
|
50
|
+
display: "flex",
|
|
51
|
+
flexDirection: direction,
|
|
52
|
+
alignItems: align,
|
|
53
|
+
justifyContent: justify,
|
|
54
|
+
gap,
|
|
55
|
+
padding: padding ?? `0 ${STAGE_INSET}px`,
|
|
56
|
+
}}
|
|
57
|
+
>
|
|
58
|
+
{children}
|
|
59
|
+
{caption && <Caption text={caption} theme={theme} startFrame={captionStart} bottomInset={bottomInset} />}
|
|
60
|
+
</AbsoluteFill>
|
|
61
|
+
);
|
|
62
|
+
};
|
|
@@ -25,6 +25,16 @@ export const Terminal: React.FC<TerminalProps> = ({
|
|
|
25
25
|
const frame = useCurrentFrame();
|
|
26
26
|
const elapsed = frame - startFrame;
|
|
27
27
|
|
|
28
|
+
// Size the window to its widest physical line so a short command doesn't sit
|
|
29
|
+
// in a huge empty box. Width is fixed from the full text (not the partial
|
|
30
|
+
// reveal) so it never reflows while typing. Inputs reserve room for "$ ".
|
|
31
|
+
const maxChars = lines.reduce((max, line) => {
|
|
32
|
+
const prefix = line.isOutput ? 0 : 2;
|
|
33
|
+
const widest = line.text.split("\n").reduce((m, l) => Math.max(m, l.length), 0);
|
|
34
|
+
return Math.max(max, widest + prefix);
|
|
35
|
+
}, 0);
|
|
36
|
+
const cols = Math.min(Math.max(maxChars, 18), 78);
|
|
37
|
+
|
|
28
38
|
const visibleLines: Array<{ text: string; isOutput: boolean; partialText: string }> = [];
|
|
29
39
|
|
|
30
40
|
let cursor = 0;
|
|
@@ -59,6 +69,7 @@ export const Terminal: React.FC<TerminalProps> = ({
|
|
|
59
69
|
fontFamily: theme.fontMono,
|
|
60
70
|
fontSize: 22,
|
|
61
71
|
boxShadow: "0 8px 48px rgba(0,0,0,0.6)",
|
|
72
|
+
width: `min(calc(${cols}ch + 48px), 100%)`,
|
|
62
73
|
}}
|
|
63
74
|
>
|
|
64
75
|
<div
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { AbsoluteFill } from "remotion";
|
|
3
|
+
import { BenchmarkScene as BenchmarkBrief } from "../../brief";
|
|
4
|
+
import { Theme } from "../../theme";
|
|
5
|
+
import { benchmarkFractions } from "../../benchmark";
|
|
6
|
+
import { Bar } from "../primitives/Bar";
|
|
7
|
+
import { Label } from "../primitives/Label";
|
|
8
|
+
import { Caption } from "../primitives/Caption";
|
|
9
|
+
|
|
10
|
+
interface Props {
|
|
11
|
+
scene: BenchmarkBrief;
|
|
12
|
+
theme: Theme;
|
|
13
|
+
bottomInset?: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const HEADLINE_FRAMES = 20;
|
|
17
|
+
const FRAMES_PER_BAR = 18;
|
|
18
|
+
|
|
19
|
+
export const Benchmark: React.FC<Props> = ({ scene, theme, bottomInset = 0 }) => {
|
|
20
|
+
const fractions = benchmarkFractions(
|
|
21
|
+
scene.bars.map((b) => b.value),
|
|
22
|
+
scene.lowerIsBetter
|
|
23
|
+
);
|
|
24
|
+
const captionStart = HEADLINE_FRAMES + scene.bars.length * FRAMES_PER_BAR + 30;
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<AbsoluteFill
|
|
28
|
+
style={{
|
|
29
|
+
background: "transparent",
|
|
30
|
+
display: "flex",
|
|
31
|
+
flexDirection: "column",
|
|
32
|
+
alignItems: "center",
|
|
33
|
+
justifyContent: "center",
|
|
34
|
+
padding: "0 120px",
|
|
35
|
+
gap: 40,
|
|
36
|
+
}}
|
|
37
|
+
>
|
|
38
|
+
{scene.headline && (
|
|
39
|
+
<Label text={scene.headline} theme={theme} size="lg" startFrame={0} />
|
|
40
|
+
)}
|
|
41
|
+
|
|
42
|
+
{scene.metric && (
|
|
43
|
+
<div
|
|
44
|
+
style={{
|
|
45
|
+
fontFamily: theme.fontSans,
|
|
46
|
+
fontSize: 24,
|
|
47
|
+
color: theme.textMuted,
|
|
48
|
+
marginTop: -16,
|
|
49
|
+
letterSpacing: "0.02em",
|
|
50
|
+
}}
|
|
51
|
+
>
|
|
52
|
+
{scene.metric}
|
|
53
|
+
</div>
|
|
54
|
+
)}
|
|
55
|
+
|
|
56
|
+
<div
|
|
57
|
+
style={{
|
|
58
|
+
display: "flex",
|
|
59
|
+
flexDirection: "column",
|
|
60
|
+
gap: 24,
|
|
61
|
+
width: "100%",
|
|
62
|
+
maxWidth: 1200,
|
|
63
|
+
}}
|
|
64
|
+
>
|
|
65
|
+
{scene.bars.map((bar, i) => (
|
|
66
|
+
<Bar
|
|
67
|
+
key={i}
|
|
68
|
+
label={bar.label}
|
|
69
|
+
valueText={bar.display ?? String(bar.value)}
|
|
70
|
+
fraction={fractions[i]}
|
|
71
|
+
hero={bar.hero}
|
|
72
|
+
theme={theme}
|
|
73
|
+
startFrame={HEADLINE_FRAMES + i * FRAMES_PER_BAR}
|
|
74
|
+
/>
|
|
75
|
+
))}
|
|
76
|
+
</div>
|
|
77
|
+
|
|
78
|
+
{scene.caption && (
|
|
79
|
+
<Caption text={scene.caption} theme={theme} startFrame={captionStart} bottomInset={bottomInset} />
|
|
80
|
+
)}
|
|
81
|
+
</AbsoluteFill>
|
|
82
|
+
);
|
|
83
|
+
};
|
|
@@ -7,9 +7,10 @@ import { Caption } from "../primitives/Caption";
|
|
|
7
7
|
interface Props {
|
|
8
8
|
scene: BrowserBrief;
|
|
9
9
|
theme: Theme;
|
|
10
|
+
bottomInset?: number;
|
|
10
11
|
}
|
|
11
12
|
|
|
12
|
-
export const BrowserFrame: React.FC<Props> = ({ scene, theme }) => {
|
|
13
|
+
export const BrowserFrame: React.FC<Props> = ({ scene, theme, bottomInset = 0 }) => {
|
|
13
14
|
const frame = useCurrentFrame();
|
|
14
15
|
const { fps } = useVideoConfig();
|
|
15
16
|
|
|
@@ -26,7 +27,7 @@ export const BrowserFrame: React.FC<Props> = ({ scene, theme }) => {
|
|
|
26
27
|
return (
|
|
27
28
|
<AbsoluteFill
|
|
28
29
|
style={{
|
|
29
|
-
background:
|
|
30
|
+
background: "transparent",
|
|
30
31
|
display: "flex",
|
|
31
32
|
alignItems: "center",
|
|
32
33
|
justifyContent: "center",
|
|
@@ -37,7 +38,7 @@ export const BrowserFrame: React.FC<Props> = ({ scene, theme }) => {
|
|
|
37
38
|
style={{
|
|
38
39
|
width: "100%",
|
|
39
40
|
opacity: windowOpacity,
|
|
40
|
-
|
|
41
|
+
scale: String(windowScale),
|
|
41
42
|
boxShadow: "0 24px 80px rgba(0,0,0,0.6)",
|
|
42
43
|
borderRadius: 12,
|
|
43
44
|
overflow: "hidden",
|
|
@@ -133,7 +134,7 @@ export const BrowserFrame: React.FC<Props> = ({ scene, theme }) => {
|
|
|
133
134
|
</div>
|
|
134
135
|
</div>
|
|
135
136
|
|
|
136
|
-
{scene.caption && <Caption text={scene.caption} theme={theme} startFrame={60} />}
|
|
137
|
+
{scene.caption && <Caption text={scene.caption} theme={theme} startFrame={60} bottomInset={bottomInset} />}
|
|
137
138
|
</AbsoluteFill>
|
|
138
139
|
);
|
|
139
140
|
};
|
|
@@ -1,20 +1,27 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
import {
|
|
3
|
-
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";
|
|
4
4
|
import { Theme } from "../../theme";
|
|
5
|
-
import {
|
|
5
|
+
import { PlatformPreset, typeScale } from "../../platforms";
|
|
6
|
+
import { Stage } from "../primitives/Stage";
|
|
7
|
+
import { Terminal } from "../primitives/Terminal";
|
|
8
|
+
import { Loop } from "../primitives/Loop";
|
|
6
9
|
|
|
7
10
|
interface Props {
|
|
8
11
|
scene: CTABrief;
|
|
9
12
|
theme: Theme;
|
|
10
13
|
project: ProjectMeta;
|
|
14
|
+
platform?: PlatformPreset;
|
|
15
|
+
bottomInset?: number;
|
|
11
16
|
}
|
|
12
17
|
|
|
13
18
|
const SNAP_OFFSET = 8;
|
|
14
19
|
|
|
15
|
-
export const CTA: React.FC<Props> = ({ scene, theme, project }) => {
|
|
20
|
+
export const CTA: React.FC<Props> = ({ scene, theme, project, platform, bottomInset = 0 }) => {
|
|
16
21
|
const frame = useCurrentFrame();
|
|
17
22
|
const { fps } = useVideoConfig();
|
|
23
|
+
const scale = platform ? typeScale(platform) : 1.0;
|
|
24
|
+
const isGif = platform?.output.codec === "gif";
|
|
18
25
|
|
|
19
26
|
const logoProgress = spring({ frame: frame + SNAP_OFFSET, fps, config: theme.motion });
|
|
20
27
|
const titleProgress = spring({ frame: frame + SNAP_OFFSET - (project.logo ? 16 : 0), fps, config: theme.motion });
|
|
@@ -24,79 +31,66 @@ export const CTA: React.FC<Props> = ({ scene, theme, project }) => {
|
|
|
24
31
|
const titleOpacity = interpolate(titleProgress, [0, 1], [0, 1]);
|
|
25
32
|
const titleY = interpolate(titleProgress, [0, 1], [24, 0]);
|
|
26
33
|
const cmdOpacity = interpolate(cmdProgress, [0, 1], [0, 1]);
|
|
27
|
-
const
|
|
28
|
-
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]);
|
|
29
36
|
|
|
30
37
|
const isAnnouncement = project.mode === "announcement";
|
|
31
38
|
const displayName = isAnnouncement && project.version
|
|
32
39
|
? `${project.name} ${project.version}`
|
|
33
40
|
: project.name;
|
|
34
41
|
|
|
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
|
+
|
|
35
46
|
return (
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
flexDirection: "column",
|
|
41
|
-
alignItems: "center",
|
|
42
|
-
justifyContent: "center",
|
|
43
|
-
gap: 32,
|
|
44
|
-
padding: "0 120px",
|
|
45
|
-
}}
|
|
46
|
-
>
|
|
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}>
|
|
47
51
|
{project.logo && (
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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>
|
|
63
71
|
)}
|
|
64
72
|
|
|
65
73
|
<div
|
|
66
74
|
style={{
|
|
67
75
|
opacity: titleOpacity,
|
|
68
|
-
|
|
76
|
+
translate: `0 ${titleY}px`,
|
|
69
77
|
fontFamily: theme.fontSans,
|
|
70
|
-
fontSize:
|
|
78
|
+
fontSize: 60 * scale,
|
|
71
79
|
fontWeight: 700,
|
|
72
|
-
color: theme.
|
|
73
|
-
letterSpacing: "-0.
|
|
80
|
+
color: theme.text,
|
|
81
|
+
letterSpacing: "-0.03em",
|
|
74
82
|
textAlign: "center",
|
|
75
83
|
}}
|
|
76
84
|
>
|
|
77
85
|
{isAnnouncement ? "" : "Get started with "}
|
|
78
|
-
<span style={{ color: theme.
|
|
86
|
+
<span style={{ color: theme.accent, fontWeight: 800 }}>{displayName}</span>
|
|
79
87
|
{isAnnouncement ? " is here." : ""}
|
|
80
88
|
</div>
|
|
81
89
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
background: theme.bg,
|
|
87
|
-
border: `1.5px solid rgba(0,0,0,0.12)`,
|
|
88
|
-
borderRadius: 12,
|
|
89
|
-
padding: "18px 40px",
|
|
90
|
-
fontFamily: theme.fontMono,
|
|
91
|
-
fontSize: 28,
|
|
92
|
-
color: theme.text,
|
|
93
|
-
display: "flex",
|
|
94
|
-
alignItems: "center",
|
|
95
|
-
gap: 16,
|
|
96
|
-
}}
|
|
97
|
-
>
|
|
98
|
-
<span style={{ color: theme.accent }}>$</span>
|
|
99
|
-
{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} />
|
|
100
94
|
</div>
|
|
101
95
|
|
|
102
96
|
{scene.repoUrl && (
|
|
@@ -105,14 +99,29 @@ export const CTA: React.FC<Props> = ({ scene, theme, project }) => {
|
|
|
105
99
|
opacity: urlFade,
|
|
106
100
|
fontFamily: theme.fontMono,
|
|
107
101
|
fontSize: 20,
|
|
108
|
-
color: theme.
|
|
102
|
+
color: theme.textMuted,
|
|
109
103
|
}}
|
|
110
104
|
>
|
|
111
105
|
{scene.repoUrl}
|
|
112
106
|
</div>
|
|
113
107
|
)}
|
|
114
108
|
|
|
115
|
-
{
|
|
116
|
-
|
|
109
|
+
{showWatermark(project.watermark) && (
|
|
110
|
+
<div
|
|
111
|
+
style={{
|
|
112
|
+
position: "absolute",
|
|
113
|
+
bottom: 40 + bottomInset,
|
|
114
|
+
left: 0,
|
|
115
|
+
right: 0,
|
|
116
|
+
textAlign: "center",
|
|
117
|
+
fontFamily: theme.fontMono,
|
|
118
|
+
fontSize: 18,
|
|
119
|
+
color: theme.textMuted,
|
|
120
|
+
}}
|
|
121
|
+
>
|
|
122
|
+
made with reelme
|
|
123
|
+
</div>
|
|
124
|
+
)}
|
|
125
|
+
</Stage>
|
|
117
126
|
);
|
|
118
127
|
};
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { AbsoluteFill, OffthreadVideo, staticFile } from "remotion";
|
|
3
|
+
import { Gif } from "@remotion/gif";
|
|
4
|
+
import { ClipScene } from "../../brief";
|
|
5
|
+
import { Theme } from "../../theme";
|
|
6
|
+
import { Caption } from "../primitives/Caption";
|
|
7
|
+
|
|
8
|
+
interface Props {
|
|
9
|
+
scene: ClipScene;
|
|
10
|
+
theme: Theme;
|
|
11
|
+
bottomInset?: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const isGif = (src: string) => src.endsWith(".gif");
|
|
15
|
+
|
|
16
|
+
const MediaElement: React.FC<{ scene: ClipScene }> = ({ scene }) => {
|
|
17
|
+
if (isGif(scene.src)) {
|
|
18
|
+
return (
|
|
19
|
+
<Gif
|
|
20
|
+
src={staticFile(scene.src)}
|
|
21
|
+
style={{ width: "100%", height: "100%", objectFit: "cover" }}
|
|
22
|
+
fit="cover"
|
|
23
|
+
/>
|
|
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;
|
|
30
|
+
return (
|
|
31
|
+
<OffthreadVideo
|
|
32
|
+
src={staticFile(scene.src)}
|
|
33
|
+
style={{ width: "100%", height: "100%", objectFit: "cover" }}
|
|
34
|
+
muted
|
|
35
|
+
startFrom={scene.startFrom}
|
|
36
|
+
endAt={endAt}
|
|
37
|
+
/>
|
|
38
|
+
);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const BrowserChrome: React.FC<{ children: React.ReactNode; theme: Theme }> = ({ children, theme }) => (
|
|
42
|
+
<div
|
|
43
|
+
style={{
|
|
44
|
+
width: "100%",
|
|
45
|
+
height: "100%",
|
|
46
|
+
display: "flex",
|
|
47
|
+
flexDirection: "column",
|
|
48
|
+
boxShadow: "0 24px 80px rgba(0,0,0,0.6)",
|
|
49
|
+
borderRadius: 12,
|
|
50
|
+
overflow: "hidden",
|
|
51
|
+
}}
|
|
52
|
+
>
|
|
53
|
+
<div
|
|
54
|
+
style={{
|
|
55
|
+
background: theme.surface,
|
|
56
|
+
padding: "12px 16px",
|
|
57
|
+
display: "flex",
|
|
58
|
+
alignItems: "center",
|
|
59
|
+
gap: 8,
|
|
60
|
+
borderBottom: `1px solid ${theme.border}`,
|
|
61
|
+
flexShrink: 0,
|
|
62
|
+
}}
|
|
63
|
+
>
|
|
64
|
+
{(["#ff5f56", "#ffbd2e", "#27c93f"] as const).map((c) => (
|
|
65
|
+
<div key={c} style={{ width: 12, height: 12, borderRadius: "50%", background: c }} />
|
|
66
|
+
))}
|
|
67
|
+
</div>
|
|
68
|
+
<div style={{ flex: 1, overflow: "hidden", position: "relative" }}>{children}</div>
|
|
69
|
+
</div>
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
const MobileChrome: React.FC<{ children: React.ReactNode; theme: Theme }> = ({ children, theme }) => (
|
|
73
|
+
<div
|
|
74
|
+
style={{
|
|
75
|
+
width: "100%",
|
|
76
|
+
height: "100%",
|
|
77
|
+
borderRadius: 40,
|
|
78
|
+
border: `3px solid ${theme.border}`,
|
|
79
|
+
overflow: "hidden",
|
|
80
|
+
boxShadow: "0 40px 100px rgba(0,0,0,0.55)",
|
|
81
|
+
position: "relative",
|
|
82
|
+
}}
|
|
83
|
+
>
|
|
84
|
+
{children}
|
|
85
|
+
</div>
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
export const Clip: React.FC<Props> = ({ scene, theme, bottomInset = 0 }) => {
|
|
89
|
+
const renderMedia = () => <MediaElement scene={scene} />;
|
|
90
|
+
|
|
91
|
+
if (scene.frame === "browser") {
|
|
92
|
+
return (
|
|
93
|
+
<AbsoluteFill
|
|
94
|
+
style={{
|
|
95
|
+
background: "transparent",
|
|
96
|
+
display: "flex",
|
|
97
|
+
alignItems: "center",
|
|
98
|
+
justifyContent: "center",
|
|
99
|
+
padding: "60px 120px",
|
|
100
|
+
}}
|
|
101
|
+
>
|
|
102
|
+
<BrowserChrome theme={theme}>{renderMedia()}</BrowserChrome>
|
|
103
|
+
{scene.caption && <Caption text={scene.caption} theme={theme} startFrame={30} bottomInset={bottomInset} />}
|
|
104
|
+
</AbsoluteFill>
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (scene.frame === "mobile") {
|
|
109
|
+
return (
|
|
110
|
+
<AbsoluteFill
|
|
111
|
+
style={{
|
|
112
|
+
background: "transparent",
|
|
113
|
+
display: "flex",
|
|
114
|
+
alignItems: "center",
|
|
115
|
+
justifyContent: "center",
|
|
116
|
+
padding: "40px 200px",
|
|
117
|
+
}}
|
|
118
|
+
>
|
|
119
|
+
<MobileChrome theme={theme}>{renderMedia()}</MobileChrome>
|
|
120
|
+
{scene.caption && <Caption text={scene.caption} theme={theme} startFrame={30} bottomInset={bottomInset} />}
|
|
121
|
+
</AbsoluteFill>
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return (
|
|
126
|
+
<AbsoluteFill style={{ background: "transparent", borderRadius: 12, overflow: "hidden" }}>
|
|
127
|
+
{renderMedia()}
|
|
128
|
+
{scene.caption && <Caption text={scene.caption} theme={theme} startFrame={30} />}
|
|
129
|
+
</AbsoluteFill>
|
|
130
|
+
);
|
|
131
|
+
};
|
|
@@ -4,21 +4,23 @@ 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;
|
|
10
11
|
theme: Theme;
|
|
12
|
+
bottomInset?: number;
|
|
11
13
|
}
|
|
12
14
|
|
|
13
|
-
export const CodeReveal: React.FC<Props> = ({ scene, theme }) => {
|
|
14
|
-
|
|
15
|
-
//
|
|
16
|
-
const captionStart =
|
|
15
|
+
export const CodeReveal: React.FC<Props> = ({ scene, theme, bottomInset = 0 }) => {
|
|
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);
|
|
17
19
|
|
|
18
20
|
return (
|
|
19
21
|
<AbsoluteFill
|
|
20
22
|
style={{
|
|
21
|
-
background:
|
|
23
|
+
background: "transparent",
|
|
22
24
|
display: "flex",
|
|
23
25
|
alignItems: "center",
|
|
24
26
|
justifyContent: "center",
|
|
@@ -31,11 +33,11 @@ export const CodeReveal: React.FC<Props> = ({ scene, theme }) => {
|
|
|
31
33
|
language={scene.language}
|
|
32
34
|
highlightLine={scene.highlightLine}
|
|
33
35
|
theme={theme}
|
|
34
|
-
startFrame={
|
|
35
|
-
framesPerLine={
|
|
36
|
+
startFrame={CODE_START}
|
|
37
|
+
framesPerLine={CODE_FRAMES_PER_LINE}
|
|
36
38
|
/>
|
|
37
39
|
</div>
|
|
38
|
-
{scene.caption && <Caption text={scene.caption} theme={theme} startFrame={captionStart} />}
|
|
40
|
+
{scene.caption && <Caption text={scene.caption} theme={theme} startFrame={captionStart} bottomInset={bottomInset} />}
|
|
39
41
|
</AbsoluteFill>
|
|
40
42
|
);
|
|
41
43
|
};
|
|
@@ -8,12 +8,13 @@ import { Caption } from "../primitives/Caption";
|
|
|
8
8
|
interface Props {
|
|
9
9
|
scene: DataFlowBrief;
|
|
10
10
|
theme: Theme;
|
|
11
|
+
bottomInset?: number;
|
|
11
12
|
}
|
|
12
13
|
|
|
13
14
|
const NODE_W = 200;
|
|
14
15
|
const NODE_H = 64;
|
|
15
16
|
|
|
16
|
-
export const DataFlow: React.FC<Props> = ({ scene, theme }) => {
|
|
17
|
+
export const DataFlow: React.FC<Props> = ({ scene, theme, bottomInset = 0 }) => {
|
|
17
18
|
const frame = useCurrentFrame();
|
|
18
19
|
const { fps, width, height } = useVideoConfig();
|
|
19
20
|
|
|
@@ -38,7 +39,7 @@ export const DataFlow: React.FC<Props> = ({ scene, theme }) => {
|
|
|
38
39
|
const captionStart = (nodeCount - 1) * STEP + NODE_SETTLE + 20;
|
|
39
40
|
|
|
40
41
|
return (
|
|
41
|
-
<AbsoluteFill style={{ background:
|
|
42
|
+
<AbsoluteFill style={{ background: "transparent" }}>
|
|
42
43
|
{/* SVG layer for arrows */}
|
|
43
44
|
<svg width={width} height={height} style={{ position: "absolute", inset: 0 }}>
|
|
44
45
|
{scene.edges.map((edge, i) => {
|
|
@@ -87,7 +88,7 @@ export const DataFlow: React.FC<Props> = ({ scene, theme }) => {
|
|
|
87
88
|
fontWeight: 600,
|
|
88
89
|
color: theme.text,
|
|
89
90
|
opacity,
|
|
90
|
-
|
|
91
|
+
scale: String(scale),
|
|
91
92
|
boxShadow: `0 4px 24px rgba(0,0,0,0.3)`,
|
|
92
93
|
}}
|
|
93
94
|
>
|
|
@@ -96,7 +97,7 @@ export const DataFlow: React.FC<Props> = ({ scene, theme }) => {
|
|
|
96
97
|
);
|
|
97
98
|
})}
|
|
98
99
|
|
|
99
|
-
{scene.caption && <Caption text={scene.caption} theme={theme} startFrame={captionStart} />}
|
|
100
|
+
{scene.caption && <Caption text={scene.caption} theme={theme} startFrame={captionStart} bottomInset={bottomInset} />}
|
|
100
101
|
</AbsoluteFill>
|
|
101
102
|
);
|
|
102
103
|
};
|