reelme 0.2.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 (39) hide show
  1. package/package.json +34 -0
  2. package/src/cache.mjs +148 -0
  3. package/src/cli.mjs +57 -0
  4. package/src/render.mjs +47 -0
  5. package/template/package.json +29 -0
  6. package/template/pnpm-lock.yaml +3522 -0
  7. package/template/pnpm-workspace.yaml +4 -0
  8. package/template/public/.gitkeep +0 -0
  9. package/template/remotion.config.ts +4 -0
  10. package/template/src/Root.tsx +127 -0
  11. package/template/src/brief.json +131 -0
  12. package/template/src/brief.ts +195 -0
  13. package/template/src/components/primitives/Arrow.tsx +83 -0
  14. package/template/src/components/primitives/Caption.tsx +52 -0
  15. package/template/src/components/primitives/CodeBlock.tsx +159 -0
  16. package/template/src/components/primitives/Icon.tsx +68 -0
  17. package/template/src/components/primitives/KeyPill.tsx +44 -0
  18. package/template/src/components/primitives/Label.tsx +49 -0
  19. package/template/src/components/primitives/Terminal.tsx +110 -0
  20. package/template/src/components/scenes/BrowserFrame.tsx +139 -0
  21. package/template/src/components/scenes/CTA.tsx +118 -0
  22. package/template/src/components/scenes/CodeReveal.tsx +41 -0
  23. package/template/src/components/scenes/DataFlow.tsx +102 -0
  24. package/template/src/components/scenes/FeatureList.tsx +110 -0
  25. package/template/src/components/scenes/FileTree.tsx +125 -0
  26. package/template/src/components/scenes/HotkeyScene.tsx +77 -0
  27. package/template/src/components/scenes/MobileScreen.tsx +212 -0
  28. package/template/src/components/scenes/OSWindowScene.tsx +166 -0
  29. package/template/src/components/scenes/Problem.tsx +105 -0
  30. package/template/src/components/scenes/SplitComparison.tsx +136 -0
  31. package/template/src/components/scenes/StatCallout.tsx +110 -0
  32. package/template/src/components/scenes/TerminalScene.tsx +41 -0
  33. package/template/src/duration.ts +55 -0
  34. package/template/src/fonts.ts +49 -0
  35. package/template/src/index.ts +106 -0
  36. package/template/src/platforms.json +78 -0
  37. package/template/src/platforms.ts +36 -0
  38. package/template/src/theme.ts +88 -0
  39. package/template/tsconfig.json +15 -0
@@ -0,0 +1,106 @@
1
+ import { registerRoot, Composition } from "remotion";
2
+ import React from "react";
3
+ import { Reel } from "./Root";
4
+ import { calcTotalDuration, TEASER_MAX_FRAMES } from "./duration";
5
+ import { Brief, Scene } from "./brief";
6
+ import { PLATFORMS, cutForPlatform } from "./platforms";
7
+ import briefJson from "./brief.json";
8
+
9
+ const brief = briefJson as unknown as Brief;
10
+
11
+ const VALID_PLATFORMS = Object.keys(PLATFORMS).join(", ");
12
+
13
+ const platformIds = brief.project?.platforms;
14
+ if (!Array.isArray(platformIds) || platformIds.length === 0) {
15
+ throw new Error(
16
+ `reelme: project.platforms is missing or empty in the brief. ` +
17
+ `Select at least one platform: ${VALID_PLATFORMS}.`
18
+ );
19
+ }
20
+ const unknown = platformIds.filter((id) => !PLATFORMS[id]);
21
+ if (unknown.length > 0) {
22
+ throw new Error(
23
+ `reelme: unknown platform(s) in the brief: ${unknown.join(", ")}. ` +
24
+ `Valid platforms: ${VALID_PLATFORMS}.`
25
+ );
26
+ }
27
+ if (!brief.cuts || !Array.isArray(brief.cuts.main) || brief.cuts.main.length === 0) {
28
+ throw new Error(
29
+ "reelme: cuts.main is missing or empty in the brief. " +
30
+ "Brief schema v2 requires cuts: { main: Scene[] } (the v1 top-level scenes array is no longer supported)."
31
+ );
32
+ }
33
+
34
+ // A vertical platform without cuts.vertical falls back to the main cut.
35
+ function scenesForCut(cut: "main" | "vertical"): Scene[] {
36
+ if (cut === "vertical") return brief.cuts.vertical ?? brief.cuts.main;
37
+ return brief.cuts.main;
38
+ }
39
+
40
+ const verticalFallback = platformIds.filter(
41
+ (id) => cutForPlatform(id) === "vertical" && !brief.cuts.vertical
42
+ );
43
+ if (verticalFallback.length > 0) {
44
+ console.warn(
45
+ `reelme: no cuts.vertical in the brief — ${verticalFallback.join(", ")} will render the main cut ` +
46
+ `letterboxed into 9:16. Author a vertical cut for better results.`
47
+ );
48
+ }
49
+
50
+ const compositions = platformIds.map((id) => {
51
+ const preset = PLATFORMS[id];
52
+ const scenes = scenesForCut(cutForPlatform(id));
53
+ if (preset.maxDurationSec !== undefined) {
54
+ const seconds = calcTotalDuration(scenes) / preset.fps;
55
+ if (seconds > preset.maxDurationSec) {
56
+ console.warn(
57
+ `reelme: the ${preset.label} cut runs ${seconds.toFixed(1)}s, over the platform's ` +
58
+ `${preset.maxDurationSec}s ceiling. Rendering anyway — consider trimming scenes.`
59
+ );
60
+ }
61
+ }
62
+ return React.createElement(Composition, {
63
+ key: `Reel-${id}`,
64
+ id: `Reel-${id}`,
65
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
66
+ component: Reel as React.FC<any>,
67
+ durationInFrames: calcTotalDuration(scenes),
68
+ fps: preset.fps,
69
+ width: preset.width,
70
+ height: preset.height,
71
+ defaultProps: { brief, platform: preset },
72
+ });
73
+ });
74
+
75
+ // Teaser pass: one Reel-<platform>-teaser composition per selected social
76
+ // platform (gif outputs excluded), same dimensions as the platform preset.
77
+ const teaser = brief.cuts.teaser;
78
+ if (teaser && teaser.length > 0 && calcTotalDuration(teaser) > TEASER_MAX_FRAMES) {
79
+ console.warn(
80
+ `reelme: the teaser cut runs ${calcTotalDuration(teaser)} frames, over the ${TEASER_MAX_FRAMES}-frame ` +
81
+ `(10s) ceiling. Rendering anyway — a teaser should be hook + CTA, nothing more.`
82
+ );
83
+ }
84
+ const teaserCompositions =
85
+ teaser && teaser.length > 0
86
+ ? platformIds
87
+ .filter((id) => PLATFORMS[id].output.codec !== "gif")
88
+ .map((id) => {
89
+ const preset = PLATFORMS[id];
90
+ return React.createElement(Composition, {
91
+ key: `Reel-${id}-teaser`,
92
+ id: `Reel-${id}-teaser`,
93
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
94
+ component: Reel as React.FC<any>,
95
+ durationInFrames: calcTotalDuration(teaser),
96
+ fps: preset.fps,
97
+ width: preset.width,
98
+ height: preset.height,
99
+ defaultProps: { brief, platform: preset, cut: "teaser" as const },
100
+ });
101
+ })
102
+ : [];
103
+
104
+ registerRoot(() => {
105
+ return React.createElement(React.Fragment, null, ...compositions, ...teaserCompositions);
106
+ });
@@ -0,0 +1,78 @@
1
+ {
2
+ "x": {
3
+ "id": "x",
4
+ "label": "X",
5
+ "cut": "main",
6
+ "width": 1920,
7
+ "height": 1080,
8
+ "fps": 30,
9
+ "output": { "file": "reelme-out/x.mp4", "codec": "h264" }
10
+ },
11
+ "linkedin": {
12
+ "id": "linkedin",
13
+ "label": "LinkedIn",
14
+ "cut": "main",
15
+ "width": 1920,
16
+ "height": 1080,
17
+ "fps": 30,
18
+ "output": { "file": "reelme-out/linkedin.mp4", "codec": "h264" }
19
+ },
20
+ "youtube": {
21
+ "id": "youtube",
22
+ "label": "YouTube",
23
+ "cut": "main",
24
+ "width": 1920,
25
+ "height": 1080,
26
+ "fps": 30,
27
+ "output": { "file": "reelme-out/youtube.mp4", "codec": "h264" }
28
+ },
29
+ "instagram-feed": {
30
+ "id": "instagram-feed",
31
+ "label": "Instagram Feed",
32
+ "cut": "main",
33
+ "width": 1080,
34
+ "height": 1080,
35
+ "fps": 30,
36
+ "output": { "file": "reelme-out/instagram-feed.mp4", "codec": "h264" }
37
+ },
38
+ "tiktok": {
39
+ "id": "tiktok",
40
+ "label": "TikTok",
41
+ "cut": "vertical",
42
+ "width": 1080,
43
+ "height": 1920,
44
+ "fps": 30,
45
+ "maxDurationSec": 60,
46
+ "safeArea": { "top": 160, "bottom": 320 },
47
+ "output": { "file": "reelme-out/tiktok.mp4", "codec": "h264" }
48
+ },
49
+ "instagram-reel": {
50
+ "id": "instagram-reel",
51
+ "label": "Instagram Reel",
52
+ "cut": "vertical",
53
+ "width": 1080,
54
+ "height": 1920,
55
+ "fps": 30,
56
+ "safeArea": { "top": 220, "bottom": 420 },
57
+ "output": { "file": "reelme-out/instagram-reel.mp4", "codec": "h264" }
58
+ },
59
+ "instagram-story": {
60
+ "id": "instagram-story",
61
+ "label": "Instagram Story",
62
+ "cut": "vertical",
63
+ "width": 1080,
64
+ "height": 1920,
65
+ "fps": 30,
66
+ "safeArea": { "top": 250, "bottom": 250 },
67
+ "output": { "file": "reelme-out/instagram-story.mp4", "codec": "h264" }
68
+ },
69
+ "github-readme": {
70
+ "id": "github-readme",
71
+ "label": "GitHub README",
72
+ "cut": "main",
73
+ "width": 1920,
74
+ "height": 1080,
75
+ "fps": 30,
76
+ "output": { "file": "reelme-out/github-readme.gif", "codec": "gif" }
77
+ }
78
+ }
@@ -0,0 +1,36 @@
1
+ // Platform presets: turn a PlatformId into concrete render parameters.
2
+ // Users pick platforms by name; aspect ratio, fps, duration ceilings, and
3
+ // safe areas are internal preset details, never user-facing questions.
4
+ //
5
+ // The data lives in platforms.json — the single source of truth shared with
6
+ // the CLI (which reads the JSON directly; it has no TypeScript build step).
7
+
8
+ import presets from "./platforms.json";
9
+
10
+ export type PlatformId =
11
+ | "x"
12
+ | "linkedin"
13
+ | "youtube"
14
+ | "tiktok"
15
+ | "instagram-reel"
16
+ | "instagram-story"
17
+ | "instagram-feed"
18
+ | "github-readme";
19
+
20
+ export interface PlatformPreset {
21
+ id: PlatformId;
22
+ label: string;
23
+ cut: "main" | "vertical";
24
+ width: number;
25
+ height: number;
26
+ fps: number;
27
+ maxDurationSec?: number;
28
+ safeArea?: { top: number; bottom: number }; // px insets at preset resolution
29
+ output: { file: string; codec: "h264" | "gif" };
30
+ }
31
+
32
+ export const PLATFORMS = presets as Record<PlatformId, PlatformPreset>;
33
+
34
+ export function cutForPlatform(id: PlatformId): "main" | "vertical" {
35
+ return PLATFORMS[id].cut;
36
+ }
@@ -0,0 +1,88 @@
1
+ import chroma from "chroma-js";
2
+ import { resolveSansFont, resolveMonoFont } from "./fonts";
3
+
4
+ export interface MotionProfile {
5
+ damping: number;
6
+ stiffness: number;
7
+ mass: number;
8
+ }
9
+
10
+ export interface Theme {
11
+ bg: string;
12
+ surface: string;
13
+ accent: string;
14
+ accentMuted: string;
15
+ text: string;
16
+ textMuted: string;
17
+ textInverse: string;
18
+ border: string;
19
+ fontMono: string;
20
+ fontSans: string;
21
+ motion: MotionProfile;
22
+ }
23
+
24
+ const MOTION_PROFILES: Record<string, MotionProfile> = {
25
+ professional: { damping: 22, stiffness: 100, mass: 1.0 },
26
+ playful: { damping: 11, stiffness: 130, mass: 0.7 },
27
+ technical: { damping: 30, stiffness: 140, mass: 1.2 },
28
+ };
29
+
30
+ const TONE_FONTS: Record<string, { sans: string; mono: string }> = {
31
+ professional: { sans: "Inter", mono: "JetBrains Mono" },
32
+ playful: { sans: "Nunito", mono: "JetBrains Mono" },
33
+ technical: { sans: "IBM Plex Sans", mono: "Space Mono" },
34
+ };
35
+
36
+ export function buildTheme(
37
+ primaryHex: string,
38
+ font?: string,
39
+ monoFont?: string,
40
+ tone?: string,
41
+ bgStyle?: "deep" | "branded" | "light",
42
+ ): Theme {
43
+ const accent = chroma(primaryHex);
44
+ const isLightBg = bgStyle === "light";
45
+
46
+ let bg: string;
47
+ let surface: string;
48
+ let text: string;
49
+ let textMuted: string;
50
+ let textInverse: string;
51
+ let border: string;
52
+
53
+ if (isLightBg) {
54
+ bg = chroma.mix(accent, "#fafafa", 0.9, "lab").hex();
55
+ surface = chroma.mix(accent, "#efefef", 0.85, "lab").hex();
56
+ border = chroma.mix(accent, "#000000", 0.12, "lab").hex();
57
+ text = "#111118";
58
+ textMuted = "#555566";
59
+ textInverse = "#f0f0f6";
60
+ } else {
61
+ const mixRatio = bgStyle === "branded" ? 0.76 : 0.92;
62
+ bg = chroma.mix(accent, "#0f0f13", mixRatio, "lab").hex();
63
+ surface = chroma.mix(accent, "#1a1a24", mixRatio - 0.04, "lab").hex();
64
+ border = chroma.mix(accent, "#ffffff", 0.1, "lab").hex();
65
+ text = "#f0f0f6";
66
+ textMuted = "#888899";
67
+ textInverse = "#111118";
68
+ }
69
+
70
+ const accentMuted = accent.alpha(0.18).css();
71
+ const toneKey = tone ?? "professional";
72
+ const motion = MOTION_PROFILES[toneKey] ?? MOTION_PROFILES.professional;
73
+ const toneFont = TONE_FONTS[toneKey] ?? TONE_FONTS.professional;
74
+
75
+ return {
76
+ bg,
77
+ surface,
78
+ accent: accent.hex(),
79
+ accentMuted,
80
+ text,
81
+ textMuted,
82
+ textInverse,
83
+ border,
84
+ fontMono: resolveMonoFont(monoFont ?? toneFont.mono),
85
+ fontSans: resolveSansFont(font ?? toneFont.sans),
86
+ motion,
87
+ };
88
+ }
@@ -0,0 +1,15 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "lib": ["ES2020", "DOM"],
5
+ "jsx": "react",
6
+ "module": "CommonJS",
7
+ "moduleResolution": "node",
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "outDir": "dist",
12
+ "resolveJsonModule": true
13
+ },
14
+ "include": ["src"]
15
+ }