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.
Files changed (50) hide show
  1. package/assets/audio/PROVENANCE.md +16 -0
  2. package/assets/audio/bright-sparks.mp3 +0 -0
  3. package/assets/audio/calm-keys.mp3 +0 -0
  4. package/assets/audio/circuit-pulse.mp3 +0 -0
  5. package/assets/audio/clean-horizon.mp3 +0 -0
  6. package/assets/audio/generate-tracks.mjs +213 -0
  7. package/assets/audio/manifest.json +83 -0
  8. package/assets/audio/midnight-protocol.mp3 +0 -0
  9. package/assets/audio/pixel-bounce.mp3 +0 -0
  10. package/assets/audio/steady-launch.mp3 +0 -0
  11. package/assets/audio/sunny-loop.mp3 +0 -0
  12. package/assets/audio/vector-grid.mp3 +0 -0
  13. package/package.json +2 -1
  14. package/src/cache.mjs +1 -0
  15. package/src/render.mjs +95 -4
  16. package/template/package.json +1 -0
  17. package/template/pnpm-lock.yaml +15 -0
  18. package/template/src/Root.tsx +55 -45
  19. package/template/src/audio.ts +24 -0
  20. package/template/src/benchmark.ts +18 -0
  21. package/template/src/brief.json +1 -0
  22. package/template/src/brief.ts +61 -3
  23. package/template/src/cinematic/Atmosphere.tsx +139 -0
  24. package/template/src/cinematic/Camera.tsx +54 -0
  25. package/template/src/cinematic/look.ts +89 -0
  26. package/template/src/cinematic/transitions.tsx +92 -0
  27. package/template/src/components/primitives/Bar.tsx +82 -0
  28. package/template/src/components/primitives/Caption.tsx +3 -2
  29. package/template/src/components/primitives/Kicker.tsx +46 -0
  30. package/template/src/components/primitives/Label.tsx +18 -24
  31. package/template/src/components/primitives/RevealText.tsx +101 -0
  32. package/template/src/components/primitives/Terminal.tsx +11 -0
  33. package/template/src/components/scenes/Benchmark.tsx +83 -0
  34. package/template/src/components/scenes/BrowserFrame.tsx +4 -3
  35. package/template/src/components/scenes/CTA.tsx +56 -10
  36. package/template/src/components/scenes/Clip.tsx +125 -0
  37. package/template/src/components/scenes/CodeReveal.tsx +4 -3
  38. package/template/src/components/scenes/DataFlow.tsx +4 -3
  39. package/template/src/components/scenes/FeatureList.tsx +13 -8
  40. package/template/src/components/scenes/FileTree.tsx +4 -3
  41. package/template/src/components/scenes/Hook.tsx +55 -0
  42. package/template/src/components/scenes/HotkeyScene.tsx +4 -3
  43. package/template/src/components/scenes/MobileScreen.tsx +120 -82
  44. package/template/src/components/scenes/OSWindowScene.tsx +4 -3
  45. package/template/src/components/scenes/Problem.tsx +28 -29
  46. package/template/src/components/scenes/SplitComparison.tsx +65 -92
  47. package/template/src/components/scenes/StatCallout.tsx +93 -4
  48. package/template/src/components/scenes/TerminalScene.tsx +5 -6
  49. package/template/src/duration.ts +12 -1
  50. 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: theme.bg, display: "flex", alignItems: "center", justifyContent: "center" }}
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: theme.bg,
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
- {isAnnouncement ? (
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
- <div
73
- style={{
74
- fontFamily: theme.fontSans,
75
- fontSize: 104,
76
- fontWeight: 800,
77
- color: theme.text,
78
- textAlign: "center",
79
- lineHeight: 1.05,
80
- letterSpacing: "-0.03em",
81
- opacity: interpolate(
82
- spring({ frame: frame + SNAP_OFFSET, fps, config: theme.motion }),
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: theme.bg,
84
+ background: "transparent",
30
85
  display: "flex",
31
86
  alignItems: "center",
32
87
  justifyContent: "center",
33
- padding: "60px 80px",
88
+ padding: "0 100px",
34
89
  }}
35
90
  >
36
- <div style={{ display: "flex", width: "100%", height: "100%", maxHeight: 700, alignItems: "stretch", gap: 0 }}>
37
- {/* Before panel */}
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: "stretch",
96
+ alignSelf: "center",
97
+ height: 200,
84
98
  background: theme.border,
85
- margin: "36px 32px 0",
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: theme.bg,
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: theme.bg,
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
- <div style={{ width: "100%" }}>
36
- <Terminal lines={lines} theme={theme} startFrame={8} />
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
  };
@@ -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
+ }