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.
- package/package.json +34 -0
- package/src/cache.mjs +148 -0
- package/src/cli.mjs +57 -0
- package/src/render.mjs +47 -0
- package/template/package.json +29 -0
- package/template/pnpm-lock.yaml +3522 -0
- package/template/pnpm-workspace.yaml +4 -0
- package/template/public/.gitkeep +0 -0
- package/template/remotion.config.ts +4 -0
- package/template/src/Root.tsx +127 -0
- package/template/src/brief.json +131 -0
- package/template/src/brief.ts +195 -0
- package/template/src/components/primitives/Arrow.tsx +83 -0
- package/template/src/components/primitives/Caption.tsx +52 -0
- package/template/src/components/primitives/CodeBlock.tsx +159 -0
- package/template/src/components/primitives/Icon.tsx +68 -0
- package/template/src/components/primitives/KeyPill.tsx +44 -0
- package/template/src/components/primitives/Label.tsx +49 -0
- package/template/src/components/primitives/Terminal.tsx +110 -0
- package/template/src/components/scenes/BrowserFrame.tsx +139 -0
- package/template/src/components/scenes/CTA.tsx +118 -0
- package/template/src/components/scenes/CodeReveal.tsx +41 -0
- package/template/src/components/scenes/DataFlow.tsx +102 -0
- package/template/src/components/scenes/FeatureList.tsx +110 -0
- package/template/src/components/scenes/FileTree.tsx +125 -0
- package/template/src/components/scenes/HotkeyScene.tsx +77 -0
- package/template/src/components/scenes/MobileScreen.tsx +212 -0
- package/template/src/components/scenes/OSWindowScene.tsx +166 -0
- package/template/src/components/scenes/Problem.tsx +105 -0
- package/template/src/components/scenes/SplitComparison.tsx +136 -0
- package/template/src/components/scenes/StatCallout.tsx +110 -0
- package/template/src/components/scenes/TerminalScene.tsx +41 -0
- package/template/src/duration.ts +55 -0
- package/template/src/fonts.ts +49 -0
- package/template/src/index.ts +106 -0
- package/template/src/platforms.json +78 -0
- package/template/src/platforms.ts +36 -0
- package/template/src/theme.ts +88 -0
- package/template/tsconfig.json +15 -0
|
File without changes
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { AbsoluteFill, Sequence, useCurrentFrame, interpolate } from "remotion";
|
|
3
|
+
import { Brief, ProjectMeta, Scene } from "./brief";
|
|
4
|
+
import { PlatformPreset } from "./platforms";
|
|
5
|
+
import { buildTheme } from "./theme";
|
|
6
|
+
import { sceneDuration } from "./duration";
|
|
7
|
+
import "./fonts";
|
|
8
|
+
import { Problem } from "./components/scenes/Problem";
|
|
9
|
+
import { CodeReveal } from "./components/scenes/CodeReveal";
|
|
10
|
+
import { TerminalScene } from "./components/scenes/TerminalScene";
|
|
11
|
+
import { DataFlow } from "./components/scenes/DataFlow";
|
|
12
|
+
import { CTA } from "./components/scenes/CTA";
|
|
13
|
+
import { BrowserFrame } from "./components/scenes/BrowserFrame";
|
|
14
|
+
import { SplitComparison } from "./components/scenes/SplitComparison";
|
|
15
|
+
import { FeatureList } from "./components/scenes/FeatureList";
|
|
16
|
+
import { StatCallout } from "./components/scenes/StatCallout";
|
|
17
|
+
import { FileTree } from "./components/scenes/FileTree";
|
|
18
|
+
import { MobileScreen } from "./components/scenes/MobileScreen";
|
|
19
|
+
import { OSWindow } from "./components/scenes/OSWindowScene";
|
|
20
|
+
import { Hotkey } from "./components/scenes/HotkeyScene";
|
|
21
|
+
|
|
22
|
+
const FADE_IN = 12;
|
|
23
|
+
const FADE_OUT = 15;
|
|
24
|
+
|
|
25
|
+
const TransitionEnvelope: React.FC<{
|
|
26
|
+
durationInFrames: number;
|
|
27
|
+
transition: "fade" | "slide" | "zoom";
|
|
28
|
+
children: React.ReactNode;
|
|
29
|
+
}> = ({ durationInFrames, transition, children }) => {
|
|
30
|
+
const frame = useCurrentFrame();
|
|
31
|
+
const keys = [0, FADE_IN, durationInFrames - FADE_OUT, durationInFrames];
|
|
32
|
+
const opts = { extrapolateLeft: "clamp" as const, extrapolateRight: "clamp" as const };
|
|
33
|
+
|
|
34
|
+
const opacity = interpolate(frame, keys, [0, 1, 1, 0], opts);
|
|
35
|
+
|
|
36
|
+
if (transition === "slide") {
|
|
37
|
+
const translateY = interpolate(frame, keys, [30, 0, 0, -30], opts);
|
|
38
|
+
return <AbsoluteFill style={{ opacity, transform: `translateY(${translateY}px)` }}>{children}</AbsoluteFill>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (transition === "zoom") {
|
|
42
|
+
const scale = interpolate(frame, keys, [0.95, 1, 1, 1.04], opts);
|
|
43
|
+
return <AbsoluteFill style={{ opacity, transform: `scale(${scale})` }}>{children}</AbsoluteFill>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return <AbsoluteFill style={{ opacity }}>{children}</AbsoluteFill>;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
interface ReelProps {
|
|
50
|
+
brief: Brief;
|
|
51
|
+
/** The platform preset of the composition being rendered. */
|
|
52
|
+
platform: PlatformPreset;
|
|
53
|
+
/** When "teaser", sequence cuts.teaser instead of the platform-resolved cut. */
|
|
54
|
+
cut?: "teaser";
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export const Reel: React.FC<ReelProps> = ({ brief, platform, cut }) => {
|
|
58
|
+
const theme = buildTheme(brief.project.primaryColor || "#6366f1", brief.project.font, brief.project.monoFont, brief.project.tone, brief.project.bgStyle);
|
|
59
|
+
|
|
60
|
+
// Cut selection: teaser when requested, otherwise the cut named by the
|
|
61
|
+
// platform preset, falling back to main when vertical is absent.
|
|
62
|
+
const scenes: Scene[] =
|
|
63
|
+
cut === "teaser"
|
|
64
|
+
? brief.cuts.teaser ?? []
|
|
65
|
+
: platform.cut === "vertical"
|
|
66
|
+
? brief.cuts.vertical ?? brief.cuts.main
|
|
67
|
+
: brief.cuts.main;
|
|
68
|
+
|
|
69
|
+
let cursor = 0;
|
|
70
|
+
const sequenced = scenes.map((scene) => {
|
|
71
|
+
const from = cursor;
|
|
72
|
+
const duration = sceneDuration(scene);
|
|
73
|
+
cursor += duration;
|
|
74
|
+
return { scene, from, duration };
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<AbsoluteFill style={{ background: theme.bg }}>
|
|
79
|
+
{sequenced.map(({ scene, from, duration }, i) => (
|
|
80
|
+
<Sequence key={i} from={from} durationInFrames={duration}>
|
|
81
|
+
<TransitionEnvelope durationInFrames={duration} transition={brief.project.transition ?? "fade"}>
|
|
82
|
+
<SceneRenderer scene={scene} theme={theme} project={brief.project} />
|
|
83
|
+
</TransitionEnvelope>
|
|
84
|
+
</Sequence>
|
|
85
|
+
))}
|
|
86
|
+
</AbsoluteFill>
|
|
87
|
+
);
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
interface SceneRendererProps {
|
|
91
|
+
scene: Scene;
|
|
92
|
+
theme: ReturnType<typeof buildTheme>;
|
|
93
|
+
project: ProjectMeta;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const SceneRenderer: React.FC<SceneRendererProps> = ({ scene, theme, project }) => {
|
|
97
|
+
switch (scene.type) {
|
|
98
|
+
case "problem":
|
|
99
|
+
return <Problem scene={scene} theme={theme} project={project} />;
|
|
100
|
+
case "code-reveal":
|
|
101
|
+
return <CodeReveal scene={scene} theme={theme} />;
|
|
102
|
+
case "terminal":
|
|
103
|
+
return <TerminalScene scene={scene} theme={theme} />;
|
|
104
|
+
case "data-flow":
|
|
105
|
+
return <DataFlow scene={scene} theme={theme} />;
|
|
106
|
+
case "cta":
|
|
107
|
+
return <CTA scene={scene} theme={theme} project={project} />;
|
|
108
|
+
case "browser":
|
|
109
|
+
return <BrowserFrame scene={scene} theme={theme} />;
|
|
110
|
+
case "split":
|
|
111
|
+
return <SplitComparison scene={scene} theme={theme} />;
|
|
112
|
+
case "feature-list":
|
|
113
|
+
return <FeatureList scene={scene} theme={theme} />;
|
|
114
|
+
case "stat-callout":
|
|
115
|
+
return <StatCallout scene={scene} theme={theme} />;
|
|
116
|
+
case "file-tree":
|
|
117
|
+
return <FileTree scene={scene} theme={theme} />;
|
|
118
|
+
case "mobile":
|
|
119
|
+
return <MobileScreen scene={scene} theme={theme} />;
|
|
120
|
+
case "os-window":
|
|
121
|
+
return <OSWindow scene={scene} theme={theme} />;
|
|
122
|
+
case "hotkey":
|
|
123
|
+
return <Hotkey scene={scene} theme={theme} />;
|
|
124
|
+
default:
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
};
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 2,
|
|
3
|
+
"project": {
|
|
4
|
+
"name": "Sprout",
|
|
5
|
+
"tagline": "Full-stack apps. Zero config.",
|
|
6
|
+
"problem": "Starting a full-stack project wastes days on setup before a line of product code.",
|
|
7
|
+
"installCommand": "npx create-sprout-app my-app",
|
|
8
|
+
"repoUrl": "github.com/sproutjs/sprout",
|
|
9
|
+
"primaryColor": "#10b981",
|
|
10
|
+
"tone": "playful",
|
|
11
|
+
"mode": "intro",
|
|
12
|
+
"bgStyle": "branded",
|
|
13
|
+
"platforms": ["x", "tiktok", "github-readme"]
|
|
14
|
+
},
|
|
15
|
+
"cuts": {
|
|
16
|
+
"main": [
|
|
17
|
+
{
|
|
18
|
+
"type": "problem",
|
|
19
|
+
"headline": "Every new project starts the same way.",
|
|
20
|
+
"hero": true,
|
|
21
|
+
"caption": "Config files. Boilerplate. Hours gone before a line of product code."
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"type": "terminal",
|
|
25
|
+
"commands": [
|
|
26
|
+
{
|
|
27
|
+
"input": "npx create-sprout-app my-app",
|
|
28
|
+
"output": "✔ Framework Next.js 15\n✔ Database PostgreSQL + Prisma\n✔ Auth NextAuth v5\n✔ Styles Tailwind CSS\n\nCreating project..."
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"input": "cd my-app && pnpm dev",
|
|
32
|
+
"output": " ▲ Sprout ready in 0.6s\n ➜ Local: http://localhost:3000\n ➜ Studio: http://localhost:5555"
|
|
33
|
+
}
|
|
34
|
+
],
|
|
35
|
+
"caption": "One command. Production stack. Under 60 seconds."
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"type": "file-tree",
|
|
39
|
+
"headline": "A structure that scales.",
|
|
40
|
+
"entries": [
|
|
41
|
+
{ "path": "src/app/", "type": "dir", "highlight": true },
|
|
42
|
+
{ "path": "src/app/(auth)/login/page.tsx", "type": "file" },
|
|
43
|
+
{ "path": "src/app/dashboard/page.tsx", "type": "file", "highlight": true },
|
|
44
|
+
{ "path": "src/server/routers/", "type": "dir", "highlight": true },
|
|
45
|
+
{ "path": "src/server/routers/user.ts", "type": "file" },
|
|
46
|
+
{ "path": "prisma/schema.prisma", "type": "file", "highlight": true },
|
|
47
|
+
{ "path": "sprout.config.ts", "type": "file" }
|
|
48
|
+
],
|
|
49
|
+
"caption": "Convention over configuration. Every folder has a job."
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"type": "code-reveal",
|
|
53
|
+
"language": "typescript",
|
|
54
|
+
"code": "// src/server/routers/user.ts\nexport const userRouter = router({\n profile: protectedProcedure\n .input(z.object({ id: z.string() }))\n .query(async ({ input, ctx }) => {\n return ctx.db.user.findUnique({\n where: { id: input.id },\n select: { name: true, email: true }\n });\n })\n});",
|
|
55
|
+
"highlightLine": 3,
|
|
56
|
+
"caption": "End-to-end type safety. No schema drift, no runtime surprises."
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
"type": "os-window",
|
|
60
|
+
"title": "Sprout Commands",
|
|
61
|
+
"searchQuery": "sprout",
|
|
62
|
+
"items": [
|
|
63
|
+
{ "icon": "play", "label": "sprout dev", "value": "Start dev server + Prisma Studio", "highlighted": true },
|
|
64
|
+
{ "icon": "database", "label": "sprout db:push", "value": "Sync schema to database" },
|
|
65
|
+
{ "icon": "user-check", "label": "sprout auth:setup", "value": "Configure OAuth providers" },
|
|
66
|
+
{ "icon": "package", "label": "sprout add", "value": "Add a feature module" },
|
|
67
|
+
{ "icon": "rocket", "label": "sprout deploy", "value": "Deploy to Vercel, Railway, or Fly" }
|
|
68
|
+
],
|
|
69
|
+
"caption": "Every task is one command away."
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
"type": "hotkey",
|
|
73
|
+
"keys": ["⌘", "Shift", "P"],
|
|
74
|
+
"action": "Open Sprout command palette",
|
|
75
|
+
"caption": "VS Code extension included — no context switching."
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
"type": "feature-list",
|
|
79
|
+
"headline": "Built for teams who ship",
|
|
80
|
+
"items": [
|
|
81
|
+
{ "text": "Type-safe API layer with tRPC out of the box", "icon": "shield" },
|
|
82
|
+
{ "text": "Auth in one command — email, GitHub, Google", "icon": "lock" },
|
|
83
|
+
{ "text": "Database ORM with auto-generated migrations", "icon": "database" },
|
|
84
|
+
{ "text": "CI/CD templates for GitHub Actions baked in", "icon": "git-branch" },
|
|
85
|
+
{ "text": "One-click deploy to Vercel, Fly, or Railway", "icon": "rocket" }
|
|
86
|
+
],
|
|
87
|
+
"caption": "Start building features on day one."
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
"type": "cta",
|
|
91
|
+
"installCommand": "npx create-sprout-app my-app",
|
|
92
|
+
"repoUrl": "github.com/sproutjs/sprout",
|
|
93
|
+
"caption": "Less setup. More shipping."
|
|
94
|
+
}
|
|
95
|
+
],
|
|
96
|
+
"vertical": [
|
|
97
|
+
{
|
|
98
|
+
"type": "problem",
|
|
99
|
+
"headline": "Every new project starts the same way.",
|
|
100
|
+
"hero": true,
|
|
101
|
+
"caption": "Hours gone before a line of product code."
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
"type": "terminal",
|
|
105
|
+
"commands": [
|
|
106
|
+
{
|
|
107
|
+
"input": "npx create-sprout-app my-app",
|
|
108
|
+
"output": "✔ Framework Next.js 15\n✔ Database PostgreSQL + Prisma\n✔ Auth NextAuth v5\n\nCreating project..."
|
|
109
|
+
}
|
|
110
|
+
],
|
|
111
|
+
"caption": "One command. Production stack."
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
"type": "feature-list",
|
|
115
|
+
"headline": "Built for teams who ship",
|
|
116
|
+
"items": [
|
|
117
|
+
{ "text": "Type-safe API layer out of the box", "icon": "shield" },
|
|
118
|
+
{ "text": "Auth in one command", "icon": "lock" },
|
|
119
|
+
{ "text": "One-click deploy", "icon": "rocket" }
|
|
120
|
+
],
|
|
121
|
+
"caption": "Start building on day one."
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
"type": "cta",
|
|
125
|
+
"installCommand": "npx create-sprout-app my-app",
|
|
126
|
+
"repoUrl": "github.com/sproutjs/sprout",
|
|
127
|
+
"caption": "Less setup. More shipping."
|
|
128
|
+
}
|
|
129
|
+
]
|
|
130
|
+
}
|
|
131
|
+
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { PlatformId } from "./platforms";
|
|
2
|
+
|
|
3
|
+
// Brief schema version. Bump on breaking changes to the reelme.json contract;
|
|
4
|
+
// the CLI refuses briefs whose schemaVersion doesn't match.
|
|
5
|
+
export const BRIEF_SCHEMA_VERSION = 2;
|
|
6
|
+
|
|
7
|
+
export interface ProjectMeta {
|
|
8
|
+
name: string;
|
|
9
|
+
tagline: string;
|
|
10
|
+
problem: string;
|
|
11
|
+
installCommand: string;
|
|
12
|
+
repoUrl: string;
|
|
13
|
+
primaryColor: string;
|
|
14
|
+
tone: "professional" | "playful" | "technical";
|
|
15
|
+
/** Publishing targets; required, at least one. Presets derive dimensions. */
|
|
16
|
+
platforms: PlatformId[];
|
|
17
|
+
/** Bundled CC0 track selection; false disables audio. (Playback lands in Phase 2.) */
|
|
18
|
+
audio?: { track: string; volume?: number } | false;
|
|
19
|
+
/** "made with reelme" credit in the CTA footer; default true. (Rendering lands in Phase 2.) */
|
|
20
|
+
watermark?: boolean;
|
|
21
|
+
mode?: "intro" | "announcement";
|
|
22
|
+
version?: string;
|
|
23
|
+
logo?: string;
|
|
24
|
+
font?: string;
|
|
25
|
+
monoFont?: string;
|
|
26
|
+
transition?: "fade" | "slide" | "zoom";
|
|
27
|
+
bgStyle?: "deep" | "branded" | "light";
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface ProblemScene {
|
|
31
|
+
type: "problem";
|
|
32
|
+
headline: string;
|
|
33
|
+
subtext?: string;
|
|
34
|
+
caption?: string;
|
|
35
|
+
hero?: boolean;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface CodeRevealScene {
|
|
39
|
+
type: "code-reveal";
|
|
40
|
+
language: string;
|
|
41
|
+
code: string;
|
|
42
|
+
highlightLine?: number;
|
|
43
|
+
caption?: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface TerminalCommand {
|
|
47
|
+
input: string;
|
|
48
|
+
output: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface TerminalScene {
|
|
52
|
+
type: "terminal";
|
|
53
|
+
commands: TerminalCommand[];
|
|
54
|
+
caption?: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface DataFlowNode {
|
|
58
|
+
id: string;
|
|
59
|
+
label: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface DataFlowEdge {
|
|
63
|
+
from: string;
|
|
64
|
+
to: string;
|
|
65
|
+
label?: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface DataFlowScene {
|
|
69
|
+
type: "data-flow";
|
|
70
|
+
nodes: DataFlowNode[];
|
|
71
|
+
edges: DataFlowEdge[];
|
|
72
|
+
caption?: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface CTAScene {
|
|
76
|
+
type: "cta";
|
|
77
|
+
installCommand: string;
|
|
78
|
+
repoUrl: string;
|
|
79
|
+
caption?: string;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface BrowserScene {
|
|
83
|
+
type: "browser";
|
|
84
|
+
url: string;
|
|
85
|
+
image?: string;
|
|
86
|
+
caption?: string;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface SplitPanel {
|
|
90
|
+
label: string;
|
|
91
|
+
content: string;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface SplitScene {
|
|
95
|
+
type: "split";
|
|
96
|
+
before: SplitPanel;
|
|
97
|
+
after: SplitPanel;
|
|
98
|
+
caption?: string;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export interface FeatureItem {
|
|
102
|
+
text: string;
|
|
103
|
+
icon?: string;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface FeatureListScene {
|
|
107
|
+
type: "feature-list";
|
|
108
|
+
headline?: string;
|
|
109
|
+
items: Array<string | FeatureItem>;
|
|
110
|
+
caption?: string;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export interface StatItem {
|
|
114
|
+
value: string;
|
|
115
|
+
label: string;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export interface StatCalloutScene {
|
|
119
|
+
type: "stat-callout";
|
|
120
|
+
headline?: string;
|
|
121
|
+
stats: StatItem[];
|
|
122
|
+
caption?: string;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export interface FileTreeEntry {
|
|
126
|
+
path: string;
|
|
127
|
+
type?: "file" | "dir";
|
|
128
|
+
highlight?: boolean;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export interface FileTreeScene {
|
|
132
|
+
type: "file-tree";
|
|
133
|
+
headline?: string;
|
|
134
|
+
entries: FileTreeEntry[];
|
|
135
|
+
caption?: string;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export interface MobileScene {
|
|
139
|
+
type: "mobile";
|
|
140
|
+
title?: string;
|
|
141
|
+
image?: string;
|
|
142
|
+
caption?: string;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export interface OSWindowItem {
|
|
146
|
+
icon?: string;
|
|
147
|
+
label: string;
|
|
148
|
+
value?: string;
|
|
149
|
+
highlighted?: boolean;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export interface OSWindowScene {
|
|
153
|
+
type: "os-window";
|
|
154
|
+
title?: string;
|
|
155
|
+
searchQuery?: string;
|
|
156
|
+
items: OSWindowItem[];
|
|
157
|
+
caption?: string;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export interface HotkeyScene {
|
|
161
|
+
type: "hotkey";
|
|
162
|
+
keys: string[];
|
|
163
|
+
action?: string;
|
|
164
|
+
caption?: string;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export type Scene =
|
|
168
|
+
| ProblemScene
|
|
169
|
+
| CodeRevealScene
|
|
170
|
+
| TerminalScene
|
|
171
|
+
| DataFlowScene
|
|
172
|
+
| CTAScene
|
|
173
|
+
| BrowserScene
|
|
174
|
+
| SplitScene
|
|
175
|
+
| FeatureListScene
|
|
176
|
+
| StatCalloutScene
|
|
177
|
+
| FileTreeScene
|
|
178
|
+
| MobileScene
|
|
179
|
+
| OSWindowScene
|
|
180
|
+
| HotkeyScene;
|
|
181
|
+
|
|
182
|
+
export interface Cuts {
|
|
183
|
+
/** The full narrative arc. Always required. */
|
|
184
|
+
main: Scene[];
|
|
185
|
+
/** Hook-first, fewer scenes, less text per second. For 9:16 platforms. */
|
|
186
|
+
vertical?: Scene[];
|
|
187
|
+
/** ≤10s (300 frames at 30fps), hook + CTA. Rendered per social platform. */
|
|
188
|
+
teaser?: Scene[];
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export interface Brief {
|
|
192
|
+
schemaVersion: number;
|
|
193
|
+
project: ProjectMeta;
|
|
194
|
+
cuts: Cuts;
|
|
195
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { useCurrentFrame, useVideoConfig, spring, interpolate } from "remotion";
|
|
3
|
+
import { Theme } from "../../theme";
|
|
4
|
+
|
|
5
|
+
interface ArrowProps {
|
|
6
|
+
x1: number;
|
|
7
|
+
y1: number;
|
|
8
|
+
x2: number;
|
|
9
|
+
y2: number;
|
|
10
|
+
label?: string;
|
|
11
|
+
theme: Theme;
|
|
12
|
+
startFrame?: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const ARROW_HEAD_SIZE = 10;
|
|
16
|
+
|
|
17
|
+
export const Arrow: React.FC<ArrowProps> = ({ x1, y1, x2, y2, label, theme, startFrame = 0 }) => {
|
|
18
|
+
const frame = useCurrentFrame();
|
|
19
|
+
const { fps } = useVideoConfig();
|
|
20
|
+
const elapsed = frame - startFrame;
|
|
21
|
+
|
|
22
|
+
const progress = spring({ frame: elapsed, fps, config: theme.motion });
|
|
23
|
+
|
|
24
|
+
const dx = x2 - x1;
|
|
25
|
+
const dy = y2 - y1;
|
|
26
|
+
const len = Math.sqrt(dx * dx + dy * dy);
|
|
27
|
+
const angleRad = Math.atan2(dy, dx);
|
|
28
|
+
const angleDeg = (angleRad * 180) / Math.PI;
|
|
29
|
+
const midX = (x1 + x2) / 2;
|
|
30
|
+
const midY = (y1 + y2) / 2;
|
|
31
|
+
|
|
32
|
+
// Shorten the line so it doesn't peek out behind the arrowhead
|
|
33
|
+
const x2short = x2 - Math.cos(angleRad) * ARROW_HEAD_SIZE;
|
|
34
|
+
const y2short = y2 - Math.sin(angleRad) * ARROW_HEAD_SIZE;
|
|
35
|
+
|
|
36
|
+
const dashOffset = interpolate(progress, [0, 1], [len, 0]);
|
|
37
|
+
|
|
38
|
+
// Arrowhead appears only in the last 15% of the draw animation
|
|
39
|
+
const headOpacity = interpolate(progress, [0.85, 1], [0, 1], {
|
|
40
|
+
extrapolateLeft: "clamp",
|
|
41
|
+
extrapolateRight: "clamp",
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<g>
|
|
46
|
+
{/* Animated line, shortened so arrowhead sits cleanly at the tip */}
|
|
47
|
+
<line
|
|
48
|
+
x1={x1}
|
|
49
|
+
y1={y1}
|
|
50
|
+
x2={x2short}
|
|
51
|
+
y2={y2short}
|
|
52
|
+
stroke={theme.accent}
|
|
53
|
+
strokeWidth={2.5}
|
|
54
|
+
strokeDasharray={len}
|
|
55
|
+
strokeDashoffset={dashOffset}
|
|
56
|
+
strokeLinecap="round"
|
|
57
|
+
/>
|
|
58
|
+
|
|
59
|
+
{/* Arrowhead — a triangle rendered at (x2, y2), rotated to face the direction of travel */}
|
|
60
|
+
<polygon
|
|
61
|
+
points={`0,0 ${-ARROW_HEAD_SIZE},${-ARROW_HEAD_SIZE / 2} ${-ARROW_HEAD_SIZE},${ARROW_HEAD_SIZE / 2}`}
|
|
62
|
+
fill={theme.accent}
|
|
63
|
+
transform={`translate(${x2}, ${y2}) rotate(${angleDeg})`}
|
|
64
|
+
opacity={headOpacity}
|
|
65
|
+
/>
|
|
66
|
+
|
|
67
|
+
{/* Edge label — appears in the second half of the draw */}
|
|
68
|
+
{label && progress > 0.5 && (
|
|
69
|
+
<text
|
|
70
|
+
x={midX}
|
|
71
|
+
y={midY - 10}
|
|
72
|
+
fill={theme.textMuted}
|
|
73
|
+
fontSize={15}
|
|
74
|
+
textAnchor="middle"
|
|
75
|
+
fontFamily={theme.fontSans}
|
|
76
|
+
opacity={interpolate(progress, [0.5, 1], [0, 1])}
|
|
77
|
+
>
|
|
78
|
+
{label}
|
|
79
|
+
</text>
|
|
80
|
+
)}
|
|
81
|
+
</g>
|
|
82
|
+
);
|
|
83
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { useCurrentFrame, useVideoConfig, spring, interpolate } from "remotion";
|
|
3
|
+
import { Theme } from "../../theme";
|
|
4
|
+
|
|
5
|
+
interface CaptionProps {
|
|
6
|
+
text: string;
|
|
7
|
+
theme: Theme;
|
|
8
|
+
startFrame?: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const Caption: React.FC<CaptionProps> = ({ text, theme, startFrame = 0 }) => {
|
|
12
|
+
const frame = useCurrentFrame();
|
|
13
|
+
const { fps } = useVideoConfig();
|
|
14
|
+
const elapsed = frame - startFrame;
|
|
15
|
+
|
|
16
|
+
const progress = spring({ frame: elapsed, fps, config: theme.motion });
|
|
17
|
+
const opacity = interpolate(Math.max(0, progress), [0, 1], [0, 1]);
|
|
18
|
+
const translateY = interpolate(Math.max(0, progress), [0, 1], [12, 0]);
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<div
|
|
22
|
+
style={{
|
|
23
|
+
position: "absolute",
|
|
24
|
+
bottom: 72,
|
|
25
|
+
left: 0,
|
|
26
|
+
right: 0,
|
|
27
|
+
display: "flex",
|
|
28
|
+
justifyContent: "center",
|
|
29
|
+
opacity,
|
|
30
|
+
transform: `translateY(${translateY}px)`,
|
|
31
|
+
}}
|
|
32
|
+
>
|
|
33
|
+
<div
|
|
34
|
+
style={{
|
|
35
|
+
background: "rgba(0,0,0,0.45)",
|
|
36
|
+
backdropFilter: "blur(8px)",
|
|
37
|
+
borderRadius: 999,
|
|
38
|
+
padding: "10px 28px",
|
|
39
|
+
fontFamily: theme.fontSans,
|
|
40
|
+
fontSize: 22,
|
|
41
|
+
fontWeight: 500,
|
|
42
|
+
color: theme.text,
|
|
43
|
+
letterSpacing: "-0.01em",
|
|
44
|
+
maxWidth: 900,
|
|
45
|
+
textAlign: "center",
|
|
46
|
+
}}
|
|
47
|
+
>
|
|
48
|
+
{text}
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
);
|
|
52
|
+
};
|