@openvidstudio/mcp-server 0.1.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/dist/beats.d.ts +47 -0
- package/dist/beats.js +9 -0
- package/dist/capture.d.ts +116 -0
- package/dist/capture.js +148 -0
- package/dist/config.d.ts +58 -0
- package/dist/config.js +130 -0
- package/dist/paths.d.ts +20 -0
- package/dist/paths.js +57 -0
- package/dist/scenes/pngSize.d.ts +11 -0
- package/dist/scenes/pngSize.js +75 -0
- package/dist/scenes/templates.d.ts +42 -0
- package/dist/scenes/templates.js +468 -0
- package/dist/server.d.ts +16 -0
- package/dist/server.js +37 -0
- package/dist/stdio.d.ts +2 -0
- package/dist/stdio.js +33 -0
- package/dist/tools/captureScreenRecording.d.ts +28 -0
- package/dist/tools/captureScreenRecording.js +181 -0
- package/dist/tools/captureScreenshot.d.ts +34 -0
- package/dist/tools/captureScreenshot.js +148 -0
- package/dist/tools/contactSheet.d.ts +36 -0
- package/dist/tools/contactSheet.js +116 -0
- package/dist/tools/generateNarration.d.ts +54 -0
- package/dist/tools/generateNarration.js +185 -0
- package/dist/tools/importHiggsfieldClip.d.ts +41 -0
- package/dist/tools/importHiggsfieldClip.js +108 -0
- package/dist/tools/index.d.ts +13 -0
- package/dist/tools/index.js +47 -0
- package/dist/tools/initProject.d.ts +37 -0
- package/dist/tools/initProject.js +166 -0
- package/dist/tools/mcp.d.ts +33 -0
- package/dist/tools/mcp.js +36 -0
- package/dist/tools/planBeats.d.ts +40 -0
- package/dist/tools/planBeats.js +143 -0
- package/dist/tools/preflight.d.ts +30 -0
- package/dist/tools/preflight.js +170 -0
- package/dist/tools/qcExtractFrames.d.ts +25 -0
- package/dist/tools/qcExtractFrames.js +129 -0
- package/dist/tools/renderVideo.d.ts +43 -0
- package/dist/tools/renderVideo.js +120 -0
- package/dist/tools/scaffoldScene.d.ts +23 -0
- package/dist/tools/scaffoldScene.js +147 -0
- package/dist/tools/stitchComposition.d.ts +24 -0
- package/dist/tools/stitchComposition.js +230 -0
- package/dist/tools/validateBeats.d.ts +14 -0
- package/dist/tools/validateBeats.js +164 -0
- package/dist/tools/validateScenes.d.ts +31 -0
- package/dist/tools/validateScenes.js +191 -0
- package/dist/tools/writeBeatsFile.d.ts +20 -0
- package/dist/tools/writeBeatsFile.js +81 -0
- package/dist/util.d.ts +43 -0
- package/dist/util.js +163 -0
- package/docs/API.md +344 -0
- package/docs/CAPTURE.md +106 -0
- package/docs/HIGGSFIELD.md +118 -0
- package/docs/NARRATION.md +111 -0
- package/docs/OVERVIEW.md +112 -0
- package/docs/PIPELINE.md +173 -0
- package/docs/PLANNING.md +206 -0
- package/docs/SCRIPT.md +50 -0
- package/docs/STYLE.md +88 -0
- package/package.json +54 -0
- package/templates/default/CLAUDE.md +63 -0
- package/templates/default/openvidstudio-core/package.json +51 -0
- package/templates/default/openvidstudio-core/scripts/gen-sfx.sh +74 -0
- package/templates/default/openvidstudio-core/src/ChecklistPanel.tsx +97 -0
- package/templates/default/openvidstudio-core/src/CinematicScene.tsx +53 -0
- package/templates/default/openvidstudio-core/src/CodePanel.tsx +122 -0
- package/templates/default/openvidstudio-core/src/CursorActor.tsx +90 -0
- package/templates/default/openvidstudio-core/src/DeviceFrame.tsx +74 -0
- package/templates/default/openvidstudio-core/src/KineticText.tsx +142 -0
- package/templates/default/openvidstudio-core/src/RepoCta.tsx +128 -0
- package/templates/default/openvidstudio-core/src/TerminalReplay.tsx +263 -0
- package/templates/default/openvidstudio-core/src/camera.tsx +141 -0
- package/templates/default/openvidstudio-core/src/index.ts +60 -0
- package/templates/default/openvidstudio-core/src/look.tsx +90 -0
- package/templates/default/openvidstudio-core/src/motion.ts +50 -0
- package/templates/default/openvidstudio-core/src/sfx.tsx +106 -0
- package/templates/default/openvidstudio-core/src/tokens.ts +41 -0
- package/templates/default/package.json +31 -0
- package/templates/default/public/sfx/bell.wav +0 -0
- package/templates/default/public/sfx/blip.wav +0 -0
- package/templates/default/public/sfx/click.wav +0 -0
- package/templates/default/public/sfx/key_a.wav +0 -0
- package/templates/default/public/sfx/key_b.wav +0 -0
- package/templates/default/public/sfx/key_c.wav +0 -0
- package/templates/default/public/sfx/key_enter.wav +0 -0
- package/templates/default/public/sfx/music-bed.mp3 +0 -0
- package/templates/default/public/sfx/success.wav +0 -0
- package/templates/default/public/sfx/whoosh.wav +0 -0
- package/templates/default/remotion.config.ts +13 -0
- package/templates/default/src/Root.tsx +9 -0
- package/templates/default/src/index.ts +4 -0
- package/templates/default/tsconfig.json +16 -0
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
// Shared CTA scene: glass repo card (informational, no star action, reads as
|
|
2
|
+
// self-promo otherwise) → dip to a modest title card.
|
|
3
|
+
|
|
4
|
+
import React from "react";
|
|
5
|
+
import { useCurrentFrame, useVideoConfig } from "remotion";
|
|
6
|
+
import { loadFont as loadInter } from "@remotion/google-fonts/Inter";
|
|
7
|
+
import { CinematicScene } from "./CinematicScene";
|
|
8
|
+
import { TitleSlam, Caption } from "./KineticText";
|
|
9
|
+
import { Layer } from "./camera";
|
|
10
|
+
import { E, pop, tween } from "./motion";
|
|
11
|
+
import { Whoosh } from "./sfx";
|
|
12
|
+
import { color } from "./tokens";
|
|
13
|
+
|
|
14
|
+
const { fontFamily: uiFamily } = loadInter("normal", {
|
|
15
|
+
weights: ["500", "700", "900"],
|
|
16
|
+
subsets: ["latin"],
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const DIP_AT = 150;
|
|
20
|
+
|
|
21
|
+
export const RepoCta: React.FC<{
|
|
22
|
+
owner: string;
|
|
23
|
+
name: string;
|
|
24
|
+
description: string;
|
|
25
|
+
chips: string[];
|
|
26
|
+
titleText: string;
|
|
27
|
+
durationInFrames?: number;
|
|
28
|
+
}> = ({ owner, name, description, chips, titleText, durationInFrames = 270 }) => {
|
|
29
|
+
const frame = useCurrentFrame();
|
|
30
|
+
const { fps } = useVideoConfig();
|
|
31
|
+
const cardIn = pop(frame, fps, 8);
|
|
32
|
+
const dip = tween(frame, [DIP_AT, DIP_AT + 16], [0, 1], E.cinematic);
|
|
33
|
+
const endFade = tween(frame, [durationInFrames - 18, durationInFrames], [0, 1], E.cinematic);
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<CinematicScene
|
|
37
|
+
camera={[
|
|
38
|
+
{ frame: 0, x: 960, y: 540, scale: 1.0 },
|
|
39
|
+
{ frame: durationInFrames, x: 960, y: 530, scale: 1.12, easing: E.drift },
|
|
40
|
+
]}
|
|
41
|
+
overlay={
|
|
42
|
+
<>
|
|
43
|
+
<div style={{ position: "absolute", inset: 0, background: "#07090E", opacity: dip, pointerEvents: "none" }} />
|
|
44
|
+
{frame >= DIP_AT + 14 ? (
|
|
45
|
+
<div
|
|
46
|
+
style={{
|
|
47
|
+
position: "absolute",
|
|
48
|
+
inset: 0,
|
|
49
|
+
display: "flex",
|
|
50
|
+
flexDirection: "column",
|
|
51
|
+
alignItems: "center",
|
|
52
|
+
justifyContent: "center",
|
|
53
|
+
gap: 26,
|
|
54
|
+
}}
|
|
55
|
+
>
|
|
56
|
+
<TitleSlam text={titleText} at={DIP_AT + 16} fontSize={76} color={color.textPrimary} glowColor={color.accent} />
|
|
57
|
+
<Caption text={`github.com/${owner}/${name}`} at={DIP_AT + 38} fontSize={28} />
|
|
58
|
+
</div>
|
|
59
|
+
) : null}
|
|
60
|
+
<div style={{ position: "absolute", inset: 0, background: "#000", opacity: endFade, pointerEvents: "none" }} />
|
|
61
|
+
</>
|
|
62
|
+
}
|
|
63
|
+
>
|
|
64
|
+
<Layer depth={0.65} maxBlur={16}>
|
|
65
|
+
<div
|
|
66
|
+
style={{
|
|
67
|
+
position: "absolute",
|
|
68
|
+
left: 560,
|
|
69
|
+
top: 180,
|
|
70
|
+
width: 800,
|
|
71
|
+
height: 800,
|
|
72
|
+
borderRadius: "50%",
|
|
73
|
+
background: `radial-gradient(circle, ${color.accent}2e 0%, transparent 65%)`,
|
|
74
|
+
}}
|
|
75
|
+
/>
|
|
76
|
+
</Layer>
|
|
77
|
+
|
|
78
|
+
<Layer depth={0}>
|
|
79
|
+
<div
|
|
80
|
+
style={{
|
|
81
|
+
position: "absolute",
|
|
82
|
+
left: 560,
|
|
83
|
+
top: 350,
|
|
84
|
+
width: 800,
|
|
85
|
+
transform: `scale(${0.85 + cardIn * 0.15}) translateY(${(1 - cardIn) * 60}px)`,
|
|
86
|
+
opacity: Math.min(1, cardIn * 1.3),
|
|
87
|
+
borderRadius: 18,
|
|
88
|
+
background: "rgba(21,26,36,0.5)",
|
|
89
|
+
backdropFilter: "blur(20px) saturate(1.3)",
|
|
90
|
+
WebkitBackdropFilter: "blur(20px) saturate(1.3)",
|
|
91
|
+
border: "1px solid rgba(255,255,255,0.12)",
|
|
92
|
+
boxShadow: "0 30px 90px rgba(0,0,0,0.5), inset 0 1px 0 rgba(255,255,255,0.08)",
|
|
93
|
+
padding: 36,
|
|
94
|
+
fontFamily: uiFamily,
|
|
95
|
+
}}
|
|
96
|
+
>
|
|
97
|
+
<div style={{ display: "flex", alignItems: "center", gap: 18 }}>
|
|
98
|
+
<div
|
|
99
|
+
style={{
|
|
100
|
+
width: 52,
|
|
101
|
+
height: 52,
|
|
102
|
+
borderRadius: 26,
|
|
103
|
+
background: `linear-gradient(135deg, ${color.accent} 0%, ${color.accentAlt} 100%)`,
|
|
104
|
+
}}
|
|
105
|
+
/>
|
|
106
|
+
<div style={{ fontSize: 30, fontWeight: 700, color: color.textPrimary }}>
|
|
107
|
+
{owner} <span style={{ color: color.textSecondary }}>/</span> {name}
|
|
108
|
+
</div>
|
|
109
|
+
</div>
|
|
110
|
+
<div style={{ marginTop: 20, fontSize: 23, color: color.textSecondary, lineHeight: 1.5 }}>
|
|
111
|
+
{description}
|
|
112
|
+
</div>
|
|
113
|
+
<div style={{ display: "flex", gap: 24, marginTop: 22, fontSize: 19, color: color.textSecondary }}>
|
|
114
|
+
{chips.map((c, i) => (
|
|
115
|
+
<span key={i} style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
|
116
|
+
{i === 0 ? <span style={{ width: 12, height: 12, borderRadius: 6, background: "#3178C6" }} /> : null}
|
|
117
|
+
{c}
|
|
118
|
+
</span>
|
|
119
|
+
))}
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
</Layer>
|
|
123
|
+
|
|
124
|
+
<Whoosh at={0} volume={0.22} />
|
|
125
|
+
<Whoosh at={DIP_AT} volume={0.3} />
|
|
126
|
+
</CinematicScene>
|
|
127
|
+
);
|
|
128
|
+
};
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
// TerminalReplay: DOM-rendered terminal with typed commands, staggered output reveals,
|
|
2
|
+
// blinking cursor, and frame-exact typing SFX. Never screen-recorded HTML (STYLE.md).
|
|
3
|
+
// Content must fit the window height (auto-scroll: v2).
|
|
4
|
+
|
|
5
|
+
import React from "react";
|
|
6
|
+
import { useCurrentFrame, useVideoConfig } from "remotion";
|
|
7
|
+
import { loadFont as loadMono } from "@remotion/google-fonts/JetBrainsMono";
|
|
8
|
+
import { E, jitter, tween } from "./motion";
|
|
9
|
+
import { Blip, TypingSfx } from "./sfx";
|
|
10
|
+
import { color, font, glow, panelShadow, radius } from "./tokens";
|
|
11
|
+
|
|
12
|
+
const { fontFamily: monoFamily } = loadMono("normal", {
|
|
13
|
+
weights: ["400", "700"],
|
|
14
|
+
subsets: ["latin"],
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
export type TermLine = {
|
|
18
|
+
text: string;
|
|
19
|
+
color?: string;
|
|
20
|
+
glow?: boolean;
|
|
21
|
+
bold?: boolean;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export type TermStep =
|
|
25
|
+
| { type: "cmd"; text: string }
|
|
26
|
+
| { type: "out"; lines: TermLine[]; stagger?: number; preDelay?: number }
|
|
27
|
+
| { type: "pause"; frames: number };
|
|
28
|
+
|
|
29
|
+
type ResolvedCmd = {
|
|
30
|
+
type: "cmd";
|
|
31
|
+
text: string;
|
|
32
|
+
start: number;
|
|
33
|
+
charFrames: number[];
|
|
34
|
+
end: number;
|
|
35
|
+
};
|
|
36
|
+
type ResolvedOut = {
|
|
37
|
+
type: "out";
|
|
38
|
+
lines: TermLine[];
|
|
39
|
+
lineFrames: number[];
|
|
40
|
+
start: number;
|
|
41
|
+
end: number;
|
|
42
|
+
};
|
|
43
|
+
type Resolved = ResolvedCmd | ResolvedOut;
|
|
44
|
+
|
|
45
|
+
export const computeTerminalTimeline = (
|
|
46
|
+
steps: TermStep[],
|
|
47
|
+
fps: number,
|
|
48
|
+
cps = 16,
|
|
49
|
+
) => {
|
|
50
|
+
const fpc = fps / cps;
|
|
51
|
+
const items: Resolved[] = [];
|
|
52
|
+
let t = 0;
|
|
53
|
+
for (const s of steps) {
|
|
54
|
+
if (s.type === "pause") {
|
|
55
|
+
t += s.frames;
|
|
56
|
+
} else if (s.type === "cmd") {
|
|
57
|
+
const charFrames = Array.from({ length: s.text.length }, (_, i) =>
|
|
58
|
+
Math.round(t + i * fpc + jitter(i, 9) * fpc * 0.6),
|
|
59
|
+
);
|
|
60
|
+
const end = (charFrames[charFrames.length - 1] ?? t) + Math.round(fps * 0.25);
|
|
61
|
+
items.push({ type: "cmd", text: s.text, start: t, charFrames, end });
|
|
62
|
+
t = end;
|
|
63
|
+
} else {
|
|
64
|
+
const preDelay = s.preDelay ?? Math.round(fps * 0.3);
|
|
65
|
+
const stagger = s.stagger ?? 3;
|
|
66
|
+
const lineFrames = s.lines.map((_, j) => t + preDelay + j * stagger);
|
|
67
|
+
const end = (lineFrames[lineFrames.length - 1] ?? t) + Math.round(fps * 0.2);
|
|
68
|
+
items.push({ type: "out", lines: s.lines, lineFrames, start: t, end });
|
|
69
|
+
t = end;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return { items, totalFrames: t };
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const LineReveal: React.FC<{
|
|
76
|
+
at: number;
|
|
77
|
+
children: React.ReactNode;
|
|
78
|
+
}> = ({ at, children }) => {
|
|
79
|
+
const frame = useCurrentFrame();
|
|
80
|
+
if (frame < at) return null;
|
|
81
|
+
const p = tween(frame, [at, at + 7], [0, 1], E.snap);
|
|
82
|
+
return (
|
|
83
|
+
<div
|
|
84
|
+
style={{
|
|
85
|
+
clipPath: `inset(0 ${(1 - p) * 100}% 0 0)`,
|
|
86
|
+
transform: `translateY(${(1 - p) * 6}px)`,
|
|
87
|
+
}}
|
|
88
|
+
>
|
|
89
|
+
{children}
|
|
90
|
+
</div>
|
|
91
|
+
);
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
export const TerminalReplay: React.FC<{
|
|
95
|
+
steps: TermStep[];
|
|
96
|
+
title?: string;
|
|
97
|
+
width?: number;
|
|
98
|
+
fontSize?: number;
|
|
99
|
+
cps?: number;
|
|
100
|
+
/** render SFX for typing/reveals (default true) */
|
|
101
|
+
sfx?: boolean;
|
|
102
|
+
/** show a fresh blinking prompt after all output is done (default true) */
|
|
103
|
+
trailingPrompt?: boolean;
|
|
104
|
+
}> = ({
|
|
105
|
+
steps,
|
|
106
|
+
title = "Project",
|
|
107
|
+
width = 1280,
|
|
108
|
+
fontSize = 26,
|
|
109
|
+
cps = 16,
|
|
110
|
+
sfx = true,
|
|
111
|
+
trailingPrompt = true,
|
|
112
|
+
}) => {
|
|
113
|
+
const frame = useCurrentFrame();
|
|
114
|
+
const { fps } = useVideoConfig();
|
|
115
|
+
const { items, totalFrames } = computeTerminalTimeline(steps, fps, cps);
|
|
116
|
+
|
|
117
|
+
const lastCmd = [...items].reverse().find((i) => i.type === "cmd") as
|
|
118
|
+
| ResolvedCmd
|
|
119
|
+
| undefined;
|
|
120
|
+
const typingActive =
|
|
121
|
+
lastCmd && frame >= lastCmd.start && frame <= lastCmd.end;
|
|
122
|
+
const cursorOn = Math.floor(frame / 8) % 2 === 0 || typingActive;
|
|
123
|
+
// cursor sits on the command line only while typing (+ a short beat after);
|
|
124
|
+
// once output streams, it disappears; a fresh prompt appears at the end.
|
|
125
|
+
const showCmdCursor = lastCmd && frame <= lastCmd.end + 12;
|
|
126
|
+
const trailingAt = totalFrames + 14;
|
|
127
|
+
|
|
128
|
+
return (
|
|
129
|
+
<div
|
|
130
|
+
style={{
|
|
131
|
+
width,
|
|
132
|
+
borderRadius: radius.window,
|
|
133
|
+
background: "#10141D",
|
|
134
|
+
border: `1px solid ${color.panelBorder}`,
|
|
135
|
+
boxShadow: panelShadow(true),
|
|
136
|
+
overflow: "hidden",
|
|
137
|
+
fontFamily: monoFamily,
|
|
138
|
+
}}
|
|
139
|
+
>
|
|
140
|
+
{/* chrome */}
|
|
141
|
+
<div
|
|
142
|
+
style={{
|
|
143
|
+
height: 52,
|
|
144
|
+
display: "flex",
|
|
145
|
+
alignItems: "center",
|
|
146
|
+
padding: "0 20px",
|
|
147
|
+
background: "#151A24",
|
|
148
|
+
borderBottom: `1px solid ${color.panelBorder}`,
|
|
149
|
+
}}
|
|
150
|
+
>
|
|
151
|
+
<div style={{ display: "flex", gap: 10 }}>
|
|
152
|
+
{["#FF5F57", "#FEBC2E", "#28C840"].map((c) => (
|
|
153
|
+
<div
|
|
154
|
+
key={c}
|
|
155
|
+
style={{ width: 14, height: 14, borderRadius: 7, background: c }}
|
|
156
|
+
/>
|
|
157
|
+
))}
|
|
158
|
+
</div>
|
|
159
|
+
<div
|
|
160
|
+
style={{
|
|
161
|
+
flex: 1,
|
|
162
|
+
textAlign: "center",
|
|
163
|
+
color: color.textSecondary,
|
|
164
|
+
fontSize: fontSize * 0.62,
|
|
165
|
+
fontFamily: font.ui,
|
|
166
|
+
}}
|
|
167
|
+
>
|
|
168
|
+
{title}
|
|
169
|
+
</div>
|
|
170
|
+
<div style={{ width: 62 }} />
|
|
171
|
+
</div>
|
|
172
|
+
{/* body */}
|
|
173
|
+
<div
|
|
174
|
+
style={{
|
|
175
|
+
padding: `${fontSize * 1.1}px ${fontSize * 1.3}px`,
|
|
176
|
+
fontSize,
|
|
177
|
+
lineHeight: 1.75,
|
|
178
|
+
color: color.textPrimary,
|
|
179
|
+
}}
|
|
180
|
+
>
|
|
181
|
+
{items.map((item, idx) => {
|
|
182
|
+
if (frame < item.start) return null;
|
|
183
|
+
if (item.type === "cmd") {
|
|
184
|
+
const visible = item.charFrames.filter((f) => f <= frame).length;
|
|
185
|
+
const isCurrent = item === lastCmd;
|
|
186
|
+
// slash-command prefix (e.g. "/components") renders accent+bold
|
|
187
|
+
const prefixMatch = item.text.match(/^\/\S+/);
|
|
188
|
+
const prefixLen = prefixMatch ? prefixMatch[0].length : 0;
|
|
189
|
+
return (
|
|
190
|
+
<div key={idx} style={{ whiteSpace: "pre-wrap" }}>
|
|
191
|
+
<span style={{ color: color.accent, fontWeight: 700 }}>
|
|
192
|
+
{"❯ "}
|
|
193
|
+
</span>
|
|
194
|
+
{prefixLen > 0 ? (
|
|
195
|
+
<span style={{ color: color.accentAlt, fontWeight: 700 }}>
|
|
196
|
+
{item.text.slice(0, Math.min(visible, prefixLen))}
|
|
197
|
+
</span>
|
|
198
|
+
) : null}
|
|
199
|
+
<span>{item.text.slice(Math.min(visible, prefixLen), visible)}</span>
|
|
200
|
+
{isCurrent && showCmdCursor && cursorOn ? (
|
|
201
|
+
<span
|
|
202
|
+
style={{
|
|
203
|
+
display: "inline-block",
|
|
204
|
+
width: fontSize * 0.55,
|
|
205
|
+
height: fontSize * 1.05,
|
|
206
|
+
background: color.textPrimary,
|
|
207
|
+
verticalAlign: "text-bottom",
|
|
208
|
+
marginLeft: 2,
|
|
209
|
+
}}
|
|
210
|
+
/>
|
|
211
|
+
) : null}
|
|
212
|
+
</div>
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
return (
|
|
216
|
+
<div key={idx}>
|
|
217
|
+
{item.lines.map((l, j) => (
|
|
218
|
+
<LineReveal key={j} at={item.lineFrames[j]}>
|
|
219
|
+
<span
|
|
220
|
+
style={{
|
|
221
|
+
whiteSpace: "pre-wrap",
|
|
222
|
+
color: l.color ?? color.textSecondary,
|
|
223
|
+
fontWeight: l.bold ? 700 : 400,
|
|
224
|
+
...(l.glow ? glow(l.color ?? color.accent) : {}),
|
|
225
|
+
}}
|
|
226
|
+
>
|
|
227
|
+
{l.text}
|
|
228
|
+
</span>
|
|
229
|
+
</LineReveal>
|
|
230
|
+
))}
|
|
231
|
+
</div>
|
|
232
|
+
);
|
|
233
|
+
})}
|
|
234
|
+
{trailingPrompt && frame >= trailingAt ? (
|
|
235
|
+
<div>
|
|
236
|
+
<span style={{ color: color.accent, fontWeight: 700 }}>{"❯ "}</span>
|
|
237
|
+
{cursorOn ? (
|
|
238
|
+
<span
|
|
239
|
+
style={{
|
|
240
|
+
display: "inline-block",
|
|
241
|
+
width: fontSize * 0.55,
|
|
242
|
+
height: fontSize * 1.05,
|
|
243
|
+
background: color.textPrimary,
|
|
244
|
+
verticalAlign: "text-bottom",
|
|
245
|
+
}}
|
|
246
|
+
/>
|
|
247
|
+
) : null}
|
|
248
|
+
</div>
|
|
249
|
+
) : null}
|
|
250
|
+
</div>
|
|
251
|
+
{/* SFX */}
|
|
252
|
+
{sfx
|
|
253
|
+
? items.map((item, idx) =>
|
|
254
|
+
item.type === "cmd" ? (
|
|
255
|
+
<TypingSfx key={`sfx-${idx}`} charFrames={item.charFrames} />
|
|
256
|
+
) : (
|
|
257
|
+
<Blip key={`sfx-${idx}`} at={item.lineFrames[0]} />
|
|
258
|
+
),
|
|
259
|
+
)
|
|
260
|
+
: null}
|
|
261
|
+
</div>
|
|
262
|
+
);
|
|
263
|
+
};
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// Camera rig: renders children on an oversized "stage" and moves a virtual camera over it.
|
|
2
|
+
// The camera is defined by keyframes (focal point on stage + zoom + tilt). Between keyframes,
|
|
3
|
+
// values interpolate with a cinematic easing per segment. Layers can opt into parallax + DoF blur.
|
|
4
|
+
|
|
5
|
+
import React, { createContext, useContext } from "react";
|
|
6
|
+
import { useCurrentFrame, useVideoConfig } from "remotion";
|
|
7
|
+
import { E, tween } from "./motion";
|
|
8
|
+
|
|
9
|
+
export type CamKeyframe = {
|
|
10
|
+
frame: number;
|
|
11
|
+
/** focal point on the stage, in stage px, this point lands at frame center */
|
|
12
|
+
x: number;
|
|
13
|
+
y: number;
|
|
14
|
+
/** zoom: 1 = stage px == output px; 1.6 = 160% close-up */
|
|
15
|
+
scale: number;
|
|
16
|
+
rotX?: number;
|
|
17
|
+
rotY?: number;
|
|
18
|
+
rotZ?: number;
|
|
19
|
+
/** easing INTO this keyframe (from the previous one) */
|
|
20
|
+
easing?: (t: number) => number;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
type CamState = Required<Omit<CamKeyframe, "easing" | "frame">>;
|
|
24
|
+
|
|
25
|
+
const CameraContext = createContext<CamState | null>(null);
|
|
26
|
+
|
|
27
|
+
const resolve = (kfs: CamKeyframe[], frame: number): CamState => {
|
|
28
|
+
const sorted = [...kfs].sort((a, b) => a.frame - b.frame);
|
|
29
|
+
const first = sorted[0];
|
|
30
|
+
const last = sorted[sorted.length - 1];
|
|
31
|
+
const base = (k: CamKeyframe): CamState => ({
|
|
32
|
+
x: k.x,
|
|
33
|
+
y: k.y,
|
|
34
|
+
scale: k.scale,
|
|
35
|
+
rotX: k.rotX ?? 0,
|
|
36
|
+
rotY: k.rotY ?? 0,
|
|
37
|
+
rotZ: k.rotZ ?? 0,
|
|
38
|
+
});
|
|
39
|
+
if (frame <= first.frame) return base(first);
|
|
40
|
+
if (frame >= last.frame) return base(last);
|
|
41
|
+
let a = first;
|
|
42
|
+
let b = sorted[1];
|
|
43
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
44
|
+
if (sorted[i].frame >= frame) {
|
|
45
|
+
a = sorted[i - 1];
|
|
46
|
+
b = sorted[i];
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const ease = b.easing ?? E.cinematic;
|
|
51
|
+
const A = base(a);
|
|
52
|
+
const B = base(b);
|
|
53
|
+
const lerp = (ka: number, kb: number) =>
|
|
54
|
+
tween(frame, [a.frame, b.frame], [ka, kb], ease);
|
|
55
|
+
return {
|
|
56
|
+
x: lerp(A.x, B.x),
|
|
57
|
+
y: lerp(A.y, B.y),
|
|
58
|
+
scale: lerp(A.scale, B.scale),
|
|
59
|
+
rotX: lerp(A.rotX, B.rotX),
|
|
60
|
+
rotY: lerp(A.rotY, B.rotY),
|
|
61
|
+
rotZ: lerp(A.rotZ, B.rotZ),
|
|
62
|
+
};
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export const CameraRig: React.FC<{
|
|
66
|
+
keyframes: CamKeyframe[];
|
|
67
|
+
/** stage size in px; content is laid out at this size, camera crops into it */
|
|
68
|
+
stageWidth?: number;
|
|
69
|
+
stageHeight?: number;
|
|
70
|
+
children: React.ReactNode;
|
|
71
|
+
}> = ({ keyframes, stageWidth = 1920, stageHeight = 1080, children }) => {
|
|
72
|
+
const frame = useCurrentFrame();
|
|
73
|
+
const { width, height } = useVideoConfig();
|
|
74
|
+
const cam = resolve(keyframes, frame);
|
|
75
|
+
|
|
76
|
+
return (
|
|
77
|
+
<CameraContext.Provider value={cam}>
|
|
78
|
+
<div
|
|
79
|
+
style={{
|
|
80
|
+
width,
|
|
81
|
+
height,
|
|
82
|
+
overflow: "hidden",
|
|
83
|
+
perspective: 1400,
|
|
84
|
+
perspectiveOrigin: "50% 50%",
|
|
85
|
+
}}
|
|
86
|
+
>
|
|
87
|
+
<div
|
|
88
|
+
style={{
|
|
89
|
+
width: stageWidth,
|
|
90
|
+
height: stageHeight,
|
|
91
|
+
transformOrigin: "0 0",
|
|
92
|
+
transformStyle: "preserve-3d",
|
|
93
|
+
transform: [
|
|
94
|
+
`translate(${width / 2}px, ${height / 2}px)`,
|
|
95
|
+
`rotateX(${cam.rotX}deg)`,
|
|
96
|
+
`rotateY(${cam.rotY}deg)`,
|
|
97
|
+
`rotateZ(${cam.rotZ}deg)`,
|
|
98
|
+
`scale(${cam.scale})`,
|
|
99
|
+
`translate(${-cam.x}px, ${-cam.y}px)`,
|
|
100
|
+
].join(" "),
|
|
101
|
+
}}
|
|
102
|
+
>
|
|
103
|
+
{children}
|
|
104
|
+
</div>
|
|
105
|
+
</div>
|
|
106
|
+
</CameraContext.Provider>
|
|
107
|
+
);
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
export const useCamera = () => useContext(CameraContext);
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Parallax + depth-of-field layer. depth 0 = focal plane (sharp, no parallax).
|
|
114
|
+
* Positive depth = "behind" (moves less, blurred). Negative = foreground (moves more, blurred).
|
|
115
|
+
*/
|
|
116
|
+
export const Layer: React.FC<{
|
|
117
|
+
depth?: number;
|
|
118
|
+
/** max blur in px applied at |depth| = 1 */
|
|
119
|
+
maxBlur?: number;
|
|
120
|
+
style?: React.CSSProperties;
|
|
121
|
+
children: React.ReactNode;
|
|
122
|
+
}> = ({ depth = 0, maxBlur = 10, style, children }) => {
|
|
123
|
+
const cam = useCamera();
|
|
124
|
+
const parallax = 0.06; // fraction of camera offset transferred per unit depth
|
|
125
|
+
const dx = cam ? (cam.x - 960) * depth * parallax : 0;
|
|
126
|
+
const dy = cam ? (cam.y - 540) * depth * parallax : 0;
|
|
127
|
+
const blur = Math.abs(depth) * maxBlur;
|
|
128
|
+
return (
|
|
129
|
+
<div
|
|
130
|
+
style={{
|
|
131
|
+
position: "absolute",
|
|
132
|
+
inset: 0,
|
|
133
|
+
transform: `translate(${dx}px, ${dy}px)`,
|
|
134
|
+
filter: blur > 0.2 ? `blur(${blur.toFixed(1)}px)` : undefined,
|
|
135
|
+
...style,
|
|
136
|
+
}}
|
|
137
|
+
>
|
|
138
|
+
{children}
|
|
139
|
+
</div>
|
|
140
|
+
);
|
|
141
|
+
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// @openvidstudio/core: public API. Everything a video project needs to build
|
|
2
|
+
// scenes in the openvidstudio house style: camera rig, motion helpers, SFX,
|
|
3
|
+
// look pass, design tokens, and the shared scene/UI components.
|
|
4
|
+
|
|
5
|
+
// Camera + layers
|
|
6
|
+
export { CameraRig, Layer, useCamera } from "./camera";
|
|
7
|
+
export type { CamKeyframe } from "./camera";
|
|
8
|
+
|
|
9
|
+
// Motion helpers
|
|
10
|
+
export { E, SPRING, tween, pop, jitter, staggerDelay } from "./motion";
|
|
11
|
+
|
|
12
|
+
// SFX
|
|
13
|
+
export {
|
|
14
|
+
KeySound,
|
|
15
|
+
TypingSfx,
|
|
16
|
+
Click,
|
|
17
|
+
Whoosh,
|
|
18
|
+
Blip,
|
|
19
|
+
SuccessChime,
|
|
20
|
+
Bell,
|
|
21
|
+
EnterKey,
|
|
22
|
+
MusicBed,
|
|
23
|
+
} from "./sfx";
|
|
24
|
+
|
|
25
|
+
// Look pass
|
|
26
|
+
export { FilmGrain, Vignette, ColorGrade, LookPass } from "./look";
|
|
27
|
+
|
|
28
|
+
// Design tokens
|
|
29
|
+
export { color, font, glow, panelShadow, radius } from "./tokens";
|
|
30
|
+
|
|
31
|
+
// Scene wrapper
|
|
32
|
+
export { CinematicScene } from "./CinematicScene";
|
|
33
|
+
|
|
34
|
+
// Device / UI frames
|
|
35
|
+
export { BrowserFrame } from "./DeviceFrame";
|
|
36
|
+
|
|
37
|
+
// Kinetic typography
|
|
38
|
+
export { TitleSlam, Caption } from "./KineticText";
|
|
39
|
+
|
|
40
|
+
// Terminal replay
|
|
41
|
+
export {
|
|
42
|
+
TerminalReplay,
|
|
43
|
+
computeTerminalTimeline,
|
|
44
|
+
} from "./TerminalReplay";
|
|
45
|
+
export type { TermLine, TermStep } from "./TerminalReplay";
|
|
46
|
+
|
|
47
|
+
// Code panel
|
|
48
|
+
export { CodePanel } from "./CodePanel";
|
|
49
|
+
export type { Token, CodeLine } from "./CodePanel";
|
|
50
|
+
|
|
51
|
+
// Checklist panel
|
|
52
|
+
export { ChecklistPanel } from "./ChecklistPanel";
|
|
53
|
+
export type { ChecklistItem } from "./ChecklistPanel";
|
|
54
|
+
|
|
55
|
+
// Cursor actor
|
|
56
|
+
export { CursorActor } from "./CursorActor";
|
|
57
|
+
export type { CursorKeyframe } from "./CursorActor";
|
|
58
|
+
|
|
59
|
+
// Repo CTA scene
|
|
60
|
+
export { RepoCta } from "./RepoCta";
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// Global look pass: film grain (animated), vignette, subtle color grade.
|
|
2
|
+
// Always rendered by CinematicScene on top of scene content. Cheap on purpose:
|
|
3
|
+
// grain = static SVG-turbulence tile jittered per frame; vignette/grade = gradient overlays.
|
|
4
|
+
|
|
5
|
+
import React from "react";
|
|
6
|
+
import { useCurrentFrame } from "remotion";
|
|
7
|
+
import { jitter } from "./motion";
|
|
8
|
+
|
|
9
|
+
// 256px noise tile as inline SVG (no binary assets needed)
|
|
10
|
+
const NOISE_TILE =
|
|
11
|
+
"data:image/svg+xml;utf8," +
|
|
12
|
+
encodeURIComponent(
|
|
13
|
+
`<svg xmlns='http://www.w3.org/2000/svg' width='256' height='256'>` +
|
|
14
|
+
`<filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/>` +
|
|
15
|
+
`<feColorMatrix type='saturate' values='0'/></filter>` +
|
|
16
|
+
`<rect width='256' height='256' filter='url(#n)'/></svg>`,
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
export const FilmGrain: React.FC<{ opacity?: number }> = ({
|
|
20
|
+
opacity = 0.05,
|
|
21
|
+
}) => {
|
|
22
|
+
const frame = useCurrentFrame();
|
|
23
|
+
const ox = Math.floor(jitter(frame, 1) * 256);
|
|
24
|
+
const oy = Math.floor(jitter(frame, 2) * 256);
|
|
25
|
+
return (
|
|
26
|
+
<div
|
|
27
|
+
style={{
|
|
28
|
+
position: "absolute",
|
|
29
|
+
inset: 0,
|
|
30
|
+
pointerEvents: "none",
|
|
31
|
+
backgroundImage: `url("${NOISE_TILE}")`,
|
|
32
|
+
backgroundRepeat: "repeat",
|
|
33
|
+
backgroundPosition: `${ox}px ${oy}px`,
|
|
34
|
+
opacity,
|
|
35
|
+
mixBlendMode: "overlay",
|
|
36
|
+
}}
|
|
37
|
+
/>
|
|
38
|
+
);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export const Vignette: React.FC<{ strength?: number }> = ({
|
|
42
|
+
strength = 0.32,
|
|
43
|
+
}) => (
|
|
44
|
+
<div
|
|
45
|
+
style={{
|
|
46
|
+
position: "absolute",
|
|
47
|
+
inset: 0,
|
|
48
|
+
pointerEvents: "none",
|
|
49
|
+
background: `radial-gradient(ellipse 75% 65% at 50% 48%, transparent 55%, rgba(0,0,0,${strength}) 100%)`,
|
|
50
|
+
}}
|
|
51
|
+
/>
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
/** Subtle grade: cool shadows, slight warm lift top-left (warm/cool contrast per STYLE.md). */
|
|
55
|
+
export const ColorGrade: React.FC<{ warmth?: number }> = ({
|
|
56
|
+
warmth = 0.06,
|
|
57
|
+
}) => (
|
|
58
|
+
<>
|
|
59
|
+
<div
|
|
60
|
+
style={{
|
|
61
|
+
position: "absolute",
|
|
62
|
+
inset: 0,
|
|
63
|
+
pointerEvents: "none",
|
|
64
|
+
background: `linear-gradient(135deg, rgba(255,170,90,${warmth}) 0%, transparent 45%)`,
|
|
65
|
+
mixBlendMode: "soft-light",
|
|
66
|
+
}}
|
|
67
|
+
/>
|
|
68
|
+
<div
|
|
69
|
+
style={{
|
|
70
|
+
position: "absolute",
|
|
71
|
+
inset: 0,
|
|
72
|
+
pointerEvents: "none",
|
|
73
|
+
background: `linear-gradient(315deg, rgba(60,110,255,${warmth * 1.2}) 0%, transparent 50%)`,
|
|
74
|
+
mixBlendMode: "soft-light",
|
|
75
|
+
}}
|
|
76
|
+
/>
|
|
77
|
+
</>
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
export const LookPass: React.FC<{
|
|
81
|
+
grain?: number;
|
|
82
|
+
vignette?: number;
|
|
83
|
+
warmth?: number;
|
|
84
|
+
}> = ({ grain = 0.05, vignette = 0.32, warmth = 0.06 }) => (
|
|
85
|
+
<>
|
|
86
|
+
<ColorGrade warmth={warmth} />
|
|
87
|
+
<FilmGrain opacity={grain} />
|
|
88
|
+
<Vignette strength={vignette} />
|
|
89
|
+
</>
|
|
90
|
+
);
|