@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.
Files changed (94) hide show
  1. package/dist/beats.d.ts +47 -0
  2. package/dist/beats.js +9 -0
  3. package/dist/capture.d.ts +116 -0
  4. package/dist/capture.js +148 -0
  5. package/dist/config.d.ts +58 -0
  6. package/dist/config.js +130 -0
  7. package/dist/paths.d.ts +20 -0
  8. package/dist/paths.js +57 -0
  9. package/dist/scenes/pngSize.d.ts +11 -0
  10. package/dist/scenes/pngSize.js +75 -0
  11. package/dist/scenes/templates.d.ts +42 -0
  12. package/dist/scenes/templates.js +468 -0
  13. package/dist/server.d.ts +16 -0
  14. package/dist/server.js +37 -0
  15. package/dist/stdio.d.ts +2 -0
  16. package/dist/stdio.js +33 -0
  17. package/dist/tools/captureScreenRecording.d.ts +28 -0
  18. package/dist/tools/captureScreenRecording.js +181 -0
  19. package/dist/tools/captureScreenshot.d.ts +34 -0
  20. package/dist/tools/captureScreenshot.js +148 -0
  21. package/dist/tools/contactSheet.d.ts +36 -0
  22. package/dist/tools/contactSheet.js +116 -0
  23. package/dist/tools/generateNarration.d.ts +54 -0
  24. package/dist/tools/generateNarration.js +185 -0
  25. package/dist/tools/importHiggsfieldClip.d.ts +41 -0
  26. package/dist/tools/importHiggsfieldClip.js +108 -0
  27. package/dist/tools/index.d.ts +13 -0
  28. package/dist/tools/index.js +47 -0
  29. package/dist/tools/initProject.d.ts +37 -0
  30. package/dist/tools/initProject.js +166 -0
  31. package/dist/tools/mcp.d.ts +33 -0
  32. package/dist/tools/mcp.js +36 -0
  33. package/dist/tools/planBeats.d.ts +40 -0
  34. package/dist/tools/planBeats.js +143 -0
  35. package/dist/tools/preflight.d.ts +30 -0
  36. package/dist/tools/preflight.js +170 -0
  37. package/dist/tools/qcExtractFrames.d.ts +25 -0
  38. package/dist/tools/qcExtractFrames.js +129 -0
  39. package/dist/tools/renderVideo.d.ts +43 -0
  40. package/dist/tools/renderVideo.js +120 -0
  41. package/dist/tools/scaffoldScene.d.ts +23 -0
  42. package/dist/tools/scaffoldScene.js +147 -0
  43. package/dist/tools/stitchComposition.d.ts +24 -0
  44. package/dist/tools/stitchComposition.js +230 -0
  45. package/dist/tools/validateBeats.d.ts +14 -0
  46. package/dist/tools/validateBeats.js +164 -0
  47. package/dist/tools/validateScenes.d.ts +31 -0
  48. package/dist/tools/validateScenes.js +191 -0
  49. package/dist/tools/writeBeatsFile.d.ts +20 -0
  50. package/dist/tools/writeBeatsFile.js +81 -0
  51. package/dist/util.d.ts +43 -0
  52. package/dist/util.js +163 -0
  53. package/docs/API.md +344 -0
  54. package/docs/CAPTURE.md +106 -0
  55. package/docs/HIGGSFIELD.md +118 -0
  56. package/docs/NARRATION.md +111 -0
  57. package/docs/OVERVIEW.md +112 -0
  58. package/docs/PIPELINE.md +173 -0
  59. package/docs/PLANNING.md +206 -0
  60. package/docs/SCRIPT.md +50 -0
  61. package/docs/STYLE.md +88 -0
  62. package/package.json +54 -0
  63. package/templates/default/CLAUDE.md +63 -0
  64. package/templates/default/openvidstudio-core/package.json +51 -0
  65. package/templates/default/openvidstudio-core/scripts/gen-sfx.sh +74 -0
  66. package/templates/default/openvidstudio-core/src/ChecklistPanel.tsx +97 -0
  67. package/templates/default/openvidstudio-core/src/CinematicScene.tsx +53 -0
  68. package/templates/default/openvidstudio-core/src/CodePanel.tsx +122 -0
  69. package/templates/default/openvidstudio-core/src/CursorActor.tsx +90 -0
  70. package/templates/default/openvidstudio-core/src/DeviceFrame.tsx +74 -0
  71. package/templates/default/openvidstudio-core/src/KineticText.tsx +142 -0
  72. package/templates/default/openvidstudio-core/src/RepoCta.tsx +128 -0
  73. package/templates/default/openvidstudio-core/src/TerminalReplay.tsx +263 -0
  74. package/templates/default/openvidstudio-core/src/camera.tsx +141 -0
  75. package/templates/default/openvidstudio-core/src/index.ts +60 -0
  76. package/templates/default/openvidstudio-core/src/look.tsx +90 -0
  77. package/templates/default/openvidstudio-core/src/motion.ts +50 -0
  78. package/templates/default/openvidstudio-core/src/sfx.tsx +106 -0
  79. package/templates/default/openvidstudio-core/src/tokens.ts +41 -0
  80. package/templates/default/package.json +31 -0
  81. package/templates/default/public/sfx/bell.wav +0 -0
  82. package/templates/default/public/sfx/blip.wav +0 -0
  83. package/templates/default/public/sfx/click.wav +0 -0
  84. package/templates/default/public/sfx/key_a.wav +0 -0
  85. package/templates/default/public/sfx/key_b.wav +0 -0
  86. package/templates/default/public/sfx/key_c.wav +0 -0
  87. package/templates/default/public/sfx/key_enter.wav +0 -0
  88. package/templates/default/public/sfx/music-bed.mp3 +0 -0
  89. package/templates/default/public/sfx/success.wav +0 -0
  90. package/templates/default/public/sfx/whoosh.wav +0 -0
  91. package/templates/default/remotion.config.ts +13 -0
  92. package/templates/default/src/Root.tsx +9 -0
  93. package/templates/default/src/index.ts +4 -0
  94. 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
+ );