reelme 0.3.0 → 0.5.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 (69) hide show
  1. package/README.md +10 -3
  2. package/assets/audio/PROVENANCE.md +18 -11
  3. package/assets/audio/generate-sfx.mjs +128 -0
  4. package/assets/audio/light-steps.mp3 +0 -0
  5. package/assets/audio/lofi-dusk.mp3 +0 -0
  6. package/assets/audio/manifest.json +56 -63
  7. package/assets/audio/neon-pulse.mp3 +0 -0
  8. package/assets/audio/night-drive.mp3 +0 -0
  9. package/assets/audio/sfx/SOURCES.md +16 -0
  10. package/assets/audio/sfx/pop.mp3 +0 -0
  11. package/assets/audio/sfx/rise.mp3 +0 -0
  12. package/assets/audio/sfx/whoosh.mp3 +0 -0
  13. package/assets/audio/sunny-bounce.mp3 +0 -0
  14. package/assets/audio/warm-memories.mp3 +0 -0
  15. package/package.json +8 -2
  16. package/src/cache.mjs +246 -16
  17. package/src/cli.mjs +20 -11
  18. package/src/render.mjs +98 -19
  19. package/template/package.json +6 -6
  20. package/template/pnpm-lock.yaml +177 -122
  21. package/template/pnpm-workspace.yaml +2 -0
  22. package/template/src/Root.tsx +101 -21
  23. package/template/src/brief.json +6 -6
  24. package/template/src/brief.ts +39 -6
  25. package/template/src/cinematic/Atmosphere.tsx +176 -30
  26. package/template/src/cinematic/Camera.tsx +22 -10
  27. package/template/src/cinematic/look.ts +51 -10
  28. package/template/src/cinematic/transitions.tsx +29 -9
  29. package/template/src/components/primitives/Arrow.tsx +8 -5
  30. package/template/src/components/primitives/Bar.tsx +6 -2
  31. package/template/src/components/primitives/Caption.tsx +2 -2
  32. package/template/src/components/primitives/CodeBlock.tsx +21 -7
  33. package/template/src/components/primitives/Icon.tsx +7 -0
  34. package/template/src/components/primitives/KeyPill.tsx +1 -1
  35. package/template/src/components/primitives/Label.tsx +4 -1
  36. package/template/src/components/primitives/Loop.tsx +65 -0
  37. package/template/src/components/primitives/RevealText.tsx +35 -7
  38. package/template/src/components/primitives/Stage.tsx +62 -0
  39. package/template/src/components/primitives/Terminal.tsx +24 -7
  40. package/template/src/components/scenes/BrowserFrame.tsx +13 -6
  41. package/template/src/components/scenes/CTA.tsx +47 -82
  42. package/template/src/components/scenes/Clip.tsx +31 -12
  43. package/template/src/components/scenes/CodeReveal.tsx +12 -13
  44. package/template/src/components/scenes/DataFlow.tsx +47 -19
  45. package/template/src/components/scenes/FeatureList.tsx +74 -77
  46. package/template/src/components/scenes/FileTree.tsx +23 -7
  47. package/template/src/components/scenes/Hook.tsx +4 -3
  48. package/template/src/components/scenes/HotkeyScene.tsx +4 -3
  49. package/template/src/components/scenes/MobileScreen.tsx +225 -118
  50. package/template/src/components/scenes/OSWindowScene.tsx +5 -5
  51. package/template/src/components/scenes/Problem.tsx +1 -1
  52. package/template/src/components/scenes/SplitComparison.tsx +41 -14
  53. package/template/src/components/scenes/StatCallout.tsx +123 -68
  54. package/template/src/components/scenes/TerminalScene.tsx +12 -11
  55. package/template/src/custom-scenes.ts +19 -0
  56. package/template/src/duration.ts +54 -5
  57. package/template/src/index.ts +16 -10
  58. package/template/src/theme.ts +10 -1
  59. package/template/src/timing.ts +49 -0
  60. package/assets/audio/bright-sparks.mp3 +0 -0
  61. package/assets/audio/calm-keys.mp3 +0 -0
  62. package/assets/audio/circuit-pulse.mp3 +0 -0
  63. package/assets/audio/clean-horizon.mp3 +0 -0
  64. package/assets/audio/generate-tracks.mjs +0 -213
  65. package/assets/audio/midnight-protocol.mp3 +0 -0
  66. package/assets/audio/pixel-bounce.mp3 +0 -0
  67. package/assets/audio/steady-launch.mp3 +0 -0
  68. package/assets/audio/sunny-loop.mp3 +0 -0
  69. package/assets/audio/vector-grid.mp3 +0 -0
@@ -1,11 +1,11 @@
1
1
  import React from "react";
2
- import { AbsoluteFill, useCurrentFrame, useVideoConfig, spring, interpolate } from "remotion";
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 { Caption } from "../primitives/Caption";
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 captionStart = HEADLINE_FRAMES + scene.items.length * FRAMES_PER_ITEM + 20;
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
- <AbsoluteFill
30
- style={{
31
- background: "transparent",
32
- display: "flex",
33
- flexDirection: "column",
34
- alignItems: left ? "flex-start" : "center",
35
- justifyContent: "center",
36
- padding: left ? "0 0 0 140px" : "0 200px",
37
- }}
38
- >
39
- {scene.headline && (
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
- <div style={{ display: "flex", flexDirection: "column", gap: 28, width: left ? "auto" : "100%", maxWidth: left ? 1200 : undefined }}>
46
- {scene.items.map((item, i) => {
47
- const text = typeof item === "string" ? item : item.text;
48
- const icon = typeof item === "string" ? undefined : item.icon;
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
- const itemStart = HEADLINE_FRAMES + i * FRAMES_PER_ITEM;
51
- const progress = spring({ frame: frame - itemStart, fps, config: theme.motion });
52
- const opacity = interpolate(Math.max(0, progress), [0, 1], [0, 1]);
53
- const translateX = interpolate(Math.max(0, progress), [0, 1], [-24, 0]);
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
- return (
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
- justifyContent: "center",
76
- flexShrink: 0,
59
+ gap: 26,
60
+ opacity,
61
+ translate: `${translateX}px 0`,
77
62
  }}
78
63
  >
79
- {icon ? (
80
- <Icon name={icon} size={18} color={theme.accent} />
81
- ) : (
82
- <span
83
- style={{
84
- fontFamily: theme.fontMono,
85
- fontSize: 14,
86
- fontWeight: 700,
87
- color: theme.accent,
88
- lineHeight: 1,
89
- }}
90
- >
91
- {i + 1}
92
- </span>
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
- <span
96
- style={{
97
- fontFamily: theme.fontSans,
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
- {scene.caption && <Caption text={scene.caption} theme={theme} startFrame={captionStart} bottomInset={bottomInset} />}
113
- </AbsoluteFill>
110
+ </Stage>
114
111
  );
115
112
  };
@@ -28,7 +28,7 @@ function getName(path: string): string {
28
28
  }
29
29
 
30
30
  const FolderIcon: React.FC<{ color: string }> = ({ color }) => (
31
- <svg width="18" height="18" viewBox="0 0 18 18" fill="none">
31
+ <svg width="24" height="24" viewBox="0 0 18 18" fill="none">
32
32
  <path
33
33
  d="M1 4.5C1 3.67 1.67 3 2.5 3H7L9 5h6.5c.83 0 1.5.67 1.5 1.5v7c0 .83-.67 1.5-1.5 1.5h-13C1.67 15 1 14.33 1 13.5v-9z"
34
34
  fill={color}
@@ -38,7 +38,7 @@ const FolderIcon: React.FC<{ color: string }> = ({ color }) => (
38
38
  );
39
39
 
40
40
  const FileIcon: React.FC<{ color: string }> = ({ color }) => (
41
- <svg width="15" height="18" viewBox="0 0 15 18" fill="none">
41
+ <svg width="20" height="24" viewBox="0 0 15 18" fill="none">
42
42
  <path d="M2 1h8l4 4v12a1 1 0 01-1 1H2a1 1 0 01-1-1V2a1 1 0 011-1z" stroke={color} strokeWidth="1.5" fill="none" />
43
43
  <path d="M9 1v5h5" stroke={color} strokeWidth="1.5" />
44
44
  </svg>
@@ -62,12 +62,28 @@ export const FileTree: React.FC<Props> = ({ scene, theme, bottomInset = 0 }) =>
62
62
  }}
63
63
  >
64
64
  {scene.headline && (
65
- <div style={{ marginBottom: 48, alignSelf: "flex-start" }}>
66
- <Label text={scene.headline} theme={theme} size="lg" startFrame={0} align="left" />
65
+ <div style={{ marginBottom: 48 }}>
66
+ <Label text={scene.headline} theme={theme} size="lg" startFrame={0} align="center" />
67
67
  </div>
68
68
  )}
69
69
 
70
- <div style={{ display: "flex", flexDirection: "column", gap: 2, width: "100%" }}>
70
+ {/* The tree lives in a real panel (solid surface, brand-lit) so it has
71
+ graphic mass — a bare list of faint rows floating on the stage reads
72
+ as a wireframe, not a shot. */}
73
+ <div
74
+ style={{
75
+ display: "flex",
76
+ flexDirection: "column",
77
+ gap: 6,
78
+ minWidth: 560,
79
+ maxWidth: "100%",
80
+ background: theme.surface,
81
+ border: `1px solid ${theme.border}`,
82
+ borderRadius: 16,
83
+ padding: "32px 40px",
84
+ boxShadow: `0 30px 90px -18px ${theme.accent}59, 0 24px 60px rgba(0,0,0,0.45)`,
85
+ }}
86
+ >
71
87
  {scene.entries.map((entry, i) => {
72
88
  const entryStart = HEADLINE_FRAMES + i * FRAMES_PER_ENTRY;
73
89
  const progress = spring({
@@ -93,7 +109,7 @@ export const FileTree: React.FC<Props> = ({ scene, theme, bottomInset = 0 }) =>
93
109
  alignItems: "center",
94
110
  gap: 10,
95
111
  opacity,
96
- transform: `translateX(${translateX}px)`,
112
+ translate: `${translateX}px 0`,
97
113
  paddingTop: isHighlighted ? 6 : 4,
98
114
  paddingBottom: isHighlighted ? 6 : 4,
99
115
  paddingLeft: depth * INDENT + 8,
@@ -107,7 +123,7 @@ export const FileTree: React.FC<Props> = ({ scene, theme, bottomInset = 0 }) =>
107
123
  <span
108
124
  style={{
109
125
  fontFamily: theme.fontMono,
110
- fontSize: 22,
126
+ fontSize: 30,
111
127
  fontWeight: isDir ? 600 : 400,
112
128
  color: textColor,
113
129
  letterSpacing: "-0.01em",
@@ -19,10 +19,11 @@ export const Hook: React.FC<Props> = ({ scene, theme, platform }) => {
19
19
 
20
20
  const fontSize = width * 0.1 * typeScale(platform);
21
21
  // The whole block eases out of a slight over-scale so the words don't just
22
- // appear — they land. A touch of continuous drift keeps the hold alive.
22
+ // appear — they land. No continuous drift on the hold: a sub-pixel sine on
23
+ // giant type reads as vibration, not life (gallery feedback); the Atmosphere
24
+ // behind carries the motion instead.
23
25
  const settle = spring({ frame, fps, config: { damping: 26, stiffness: 90, mass: 1 } });
24
26
  const scale = interpolate(settle, [0, 1], [1.06, 1]);
25
- const drift = Math.sin(frame / 40) * 0.4;
26
27
 
27
28
  return (
28
29
  <AbsoluteFill
@@ -34,7 +35,7 @@ export const Hook: React.FC<Props> = ({ scene, theme, platform }) => {
34
35
  padding: left ? "0 0 0 140px" : "0 80px",
35
36
  }}
36
37
  >
37
- <div style={{ transform: `scale(${scale}) translateY(${drift}px)`, transformOrigin: left ? "left center" : "center", willChange: "transform" }}>
38
+ <div style={{ transform: `scale(${scale})`, transformOrigin: left ? "left center" : "center", willChange: "transform" }}>
38
39
  {scene.kicker && <Kicker text={scene.kicker} theme={theme} startFrame={0} align={left ? "left" : "center"} />}
39
40
  <RevealText
40
41
  text={scene.text}
@@ -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 actionStart = scene.keys.length * FRAMES_PER_KEY + ACTION_DELAY;
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
- {scene.keys.map((key, i) => (
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
- transform: `translateY(${actionY}px)`,
64
+ translate: `0 ${actionY}px`,
64
65
  fontFamily: theme.fontSans,
65
66
  fontSize: 28,
66
67
  fontWeight: 400,