@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,97 @@
|
|
|
1
|
+
// GitHub-style Community Standards checklist panel. Items flip ✗ → ✓ at flipAt frames.
|
|
2
|
+
|
|
3
|
+
import React from "react";
|
|
4
|
+
import { useCurrentFrame, useVideoConfig } from "remotion";
|
|
5
|
+
import { loadFont as loadInter } from "@remotion/google-fonts/Inter";
|
|
6
|
+
import { pop, SPRING } from "./motion";
|
|
7
|
+
import { Blip } from "./sfx";
|
|
8
|
+
import { color, panelShadow, radius } from "./tokens";
|
|
9
|
+
|
|
10
|
+
const { fontFamily: uiFamily } = loadInter("normal", {
|
|
11
|
+
weights: ["500", "700"],
|
|
12
|
+
subsets: ["latin"],
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
export type ChecklistItem = {
|
|
16
|
+
label: string;
|
|
17
|
+
/** absolute scene frame at which this item flips to ✓ (omit = stays ✗) */
|
|
18
|
+
flipAt?: number;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const ChecklistPanel: React.FC<{
|
|
22
|
+
title?: string;
|
|
23
|
+
items: ChecklistItem[];
|
|
24
|
+
width?: number;
|
|
25
|
+
fontSize?: number;
|
|
26
|
+
sfx?: boolean;
|
|
27
|
+
}> = ({
|
|
28
|
+
title = "Community Standards",
|
|
29
|
+
items,
|
|
30
|
+
width = 760,
|
|
31
|
+
fontSize = 27,
|
|
32
|
+
sfx = true,
|
|
33
|
+
}) => {
|
|
34
|
+
const frame = useCurrentFrame();
|
|
35
|
+
const { fps } = useVideoConfig();
|
|
36
|
+
return (
|
|
37
|
+
<div
|
|
38
|
+
style={{
|
|
39
|
+
width,
|
|
40
|
+
borderRadius: radius.window,
|
|
41
|
+
background: "#10141D",
|
|
42
|
+
border: `1px solid ${color.panelBorder}`,
|
|
43
|
+
boxShadow: panelShadow(true),
|
|
44
|
+
fontFamily: uiFamily,
|
|
45
|
+
padding: `${fontSize}px ${fontSize * 1.2}px`,
|
|
46
|
+
}}
|
|
47
|
+
>
|
|
48
|
+
<div style={{ fontSize: fontSize * 1.15, fontWeight: 700, color: color.textPrimary, marginBottom: fontSize * 0.8 }}>
|
|
49
|
+
{title}
|
|
50
|
+
</div>
|
|
51
|
+
{items.map((item, i) => {
|
|
52
|
+
const flipped = item.flipAt !== undefined && frame >= item.flipAt;
|
|
53
|
+
const s = flipped ? pop(frame, fps, item.flipAt, SPRING.pop) : 1;
|
|
54
|
+
return (
|
|
55
|
+
<div
|
|
56
|
+
key={i}
|
|
57
|
+
style={{
|
|
58
|
+
display: "flex",
|
|
59
|
+
alignItems: "center",
|
|
60
|
+
gap: fontSize * 0.6,
|
|
61
|
+
padding: `${fontSize * 0.38}px 0`,
|
|
62
|
+
fontSize,
|
|
63
|
+
color: color.textSecondary,
|
|
64
|
+
borderBottom: i < items.length - 1 ? `1px solid ${color.panelBorder}55` : undefined,
|
|
65
|
+
}}
|
|
66
|
+
>
|
|
67
|
+
<span
|
|
68
|
+
style={{
|
|
69
|
+
display: "inline-flex",
|
|
70
|
+
alignItems: "center",
|
|
71
|
+
justifyContent: "center",
|
|
72
|
+
width: fontSize * 1.15,
|
|
73
|
+
height: fontSize * 1.15,
|
|
74
|
+
borderRadius: "50%",
|
|
75
|
+
fontWeight: 700,
|
|
76
|
+
fontSize: fontSize * 0.8,
|
|
77
|
+
transform: `scale(${0.7 + s * 0.3})`,
|
|
78
|
+
background: flipped ? `${color.success}22` : `${color.danger}22`,
|
|
79
|
+
color: flipped ? color.success : color.danger,
|
|
80
|
+
border: `1.5px solid ${flipped ? color.success : color.danger}`,
|
|
81
|
+
boxShadow: flipped ? `0 0 18px ${color.success}44` : undefined,
|
|
82
|
+
}}
|
|
83
|
+
>
|
|
84
|
+
{flipped ? "✓" : "✗"}
|
|
85
|
+
</span>
|
|
86
|
+
<span style={{ color: flipped ? color.textPrimary : color.textSecondary }}>{item.label}</span>
|
|
87
|
+
</div>
|
|
88
|
+
);
|
|
89
|
+
})}
|
|
90
|
+
{sfx
|
|
91
|
+
? items
|
|
92
|
+
.filter((i) => i.flipAt !== undefined)
|
|
93
|
+
.map((i, k) => <Blip key={k} at={i.flipAt as number} volume={0.22} />)
|
|
94
|
+
: null}
|
|
95
|
+
</div>
|
|
96
|
+
);
|
|
97
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// Mandatory wrapper for every scene (STYLE.md): dark radial stage + camera rig + look pass.
|
|
2
|
+
// Content renders on an oversized stage; camera keyframes drive the shot.
|
|
3
|
+
|
|
4
|
+
import React from "react";
|
|
5
|
+
import { AbsoluteFill } from "remotion";
|
|
6
|
+
import { CameraRig, CamKeyframe } from "./camera";
|
|
7
|
+
import { LookPass } from "./look";
|
|
8
|
+
import { color } from "./tokens";
|
|
9
|
+
|
|
10
|
+
export const CinematicScene: React.FC<{
|
|
11
|
+
camera: CamKeyframe[];
|
|
12
|
+
stageWidth?: number;
|
|
13
|
+
stageHeight?: number;
|
|
14
|
+
/** dark (default) or light stage */
|
|
15
|
+
variant?: "dark" | "light";
|
|
16
|
+
grain?: number;
|
|
17
|
+
vignette?: number;
|
|
18
|
+
/** screen-space layer (captions, title cards, dips/fades), NOT camera-transformed */
|
|
19
|
+
overlay?: React.ReactNode;
|
|
20
|
+
children: React.ReactNode;
|
|
21
|
+
}> = ({
|
|
22
|
+
camera,
|
|
23
|
+
stageWidth = 1920,
|
|
24
|
+
stageHeight = 1080,
|
|
25
|
+
variant = "dark",
|
|
26
|
+
grain = 0.05,
|
|
27
|
+
vignette = 0.32,
|
|
28
|
+
overlay,
|
|
29
|
+
children,
|
|
30
|
+
}) => {
|
|
31
|
+
const bg =
|
|
32
|
+
variant === "dark"
|
|
33
|
+
? `radial-gradient(ellipse 90% 80% at 50% 40%, ${color.bg1} 0%, ${color.bg0} 100%)`
|
|
34
|
+
: color.lightBg;
|
|
35
|
+
return (
|
|
36
|
+
<AbsoluteFill style={{ background: bg }}>
|
|
37
|
+
<CameraRig
|
|
38
|
+
keyframes={camera}
|
|
39
|
+
stageWidth={stageWidth}
|
|
40
|
+
stageHeight={stageHeight}
|
|
41
|
+
>
|
|
42
|
+
{children}
|
|
43
|
+
</CameraRig>
|
|
44
|
+
{overlay ? (
|
|
45
|
+
<div style={{ position: "absolute", inset: 0 }}>{overlay}</div>
|
|
46
|
+
) : null}
|
|
47
|
+
<LookPass
|
|
48
|
+
grain={grain}
|
|
49
|
+
vignette={variant === "light" ? vignette * 0.5 : vignette}
|
|
50
|
+
/>
|
|
51
|
+
</AbsoluteFill>
|
|
52
|
+
);
|
|
53
|
+
};
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// Code panel: manually-tokenized lines (no highlighter dep), optional scroll + row highlight.
|
|
2
|
+
|
|
3
|
+
import React from "react";
|
|
4
|
+
import { color, glow, panelShadow, radius } from "./tokens";
|
|
5
|
+
|
|
6
|
+
// System-safe fallbacks, avoid @remotion/google-fonts' network fetch (hangs render offline).
|
|
7
|
+
const monoFamily = "JetBrains Mono, Consolas, Menlo, monospace";
|
|
8
|
+
const uiFamily = "Inter, -apple-system, Segoe UI, Roboto, sans-serif";
|
|
9
|
+
|
|
10
|
+
export type Token = { t: string; c?: "kw" | "str" | "fn" | "cm" | "num" | "plain" };
|
|
11
|
+
export type CodeLine = Token[];
|
|
12
|
+
|
|
13
|
+
const TOKEN_COLORS: Record<NonNullable<Token["c"]>, string> = {
|
|
14
|
+
kw: "#C792EA",
|
|
15
|
+
str: "#8BD49C",
|
|
16
|
+
fn: "#82AAFF",
|
|
17
|
+
cm: "#5B6572",
|
|
18
|
+
num: "#F78C6C",
|
|
19
|
+
plain: "#D6DEEB",
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const CodePanel: React.FC<{
|
|
23
|
+
title: string;
|
|
24
|
+
lines: CodeLine[];
|
|
25
|
+
width?: number;
|
|
26
|
+
height?: number;
|
|
27
|
+
fontSize?: number;
|
|
28
|
+
/** px translateY applied to code body (drive with a tween for scrolling) */
|
|
29
|
+
scroll?: number;
|
|
30
|
+
/** 0-based line indexes to highlight with an accent row + glow */
|
|
31
|
+
highlight?: number[];
|
|
32
|
+
}> = ({
|
|
33
|
+
title,
|
|
34
|
+
lines,
|
|
35
|
+
width = 860,
|
|
36
|
+
height = 640,
|
|
37
|
+
fontSize = 22,
|
|
38
|
+
scroll = 0,
|
|
39
|
+
highlight = [],
|
|
40
|
+
}) => {
|
|
41
|
+
return (
|
|
42
|
+
<div
|
|
43
|
+
style={{
|
|
44
|
+
width,
|
|
45
|
+
height,
|
|
46
|
+
borderRadius: radius.window,
|
|
47
|
+
background: "#10141D",
|
|
48
|
+
border: `1px solid ${color.panelBorder}`,
|
|
49
|
+
boxShadow: panelShadow(true),
|
|
50
|
+
overflow: "hidden",
|
|
51
|
+
}}
|
|
52
|
+
>
|
|
53
|
+
<div
|
|
54
|
+
style={{
|
|
55
|
+
height: 48,
|
|
56
|
+
display: "flex",
|
|
57
|
+
alignItems: "center",
|
|
58
|
+
padding: "0 18px",
|
|
59
|
+
gap: 10,
|
|
60
|
+
background: "#151A24",
|
|
61
|
+
borderBottom: `1px solid ${color.panelBorder}`,
|
|
62
|
+
fontFamily: uiFamily,
|
|
63
|
+
fontSize: 16,
|
|
64
|
+
color: color.textSecondary,
|
|
65
|
+
}}
|
|
66
|
+
>
|
|
67
|
+
<div style={{ display: "flex", gap: 8 }}>
|
|
68
|
+
{["#FF5F57", "#FEBC2E", "#28C840"].map((c) => (
|
|
69
|
+
<div key={c} style={{ width: 12, height: 12, borderRadius: 6, background: c }} />
|
|
70
|
+
))}
|
|
71
|
+
</div>
|
|
72
|
+
<span style={{ marginLeft: 8 }}>{title}</span>
|
|
73
|
+
</div>
|
|
74
|
+
<div style={{ height: height - 48, overflow: "hidden" }}>
|
|
75
|
+
<div style={{ padding: `${fontSize * 0.7}px 0`, transform: `translateY(${-scroll}px)` }}>
|
|
76
|
+
{lines.map((line, i) => {
|
|
77
|
+
const hl = highlight.includes(i);
|
|
78
|
+
return (
|
|
79
|
+
<div
|
|
80
|
+
key={i}
|
|
81
|
+
style={{
|
|
82
|
+
display: "flex",
|
|
83
|
+
fontFamily: monoFamily,
|
|
84
|
+
fontSize,
|
|
85
|
+
lineHeight: 1.65,
|
|
86
|
+
background: hl ? `${color.accent}1A` : undefined,
|
|
87
|
+
borderLeft: hl ? `3px solid ${color.accent}` : "3px solid transparent",
|
|
88
|
+
}}
|
|
89
|
+
>
|
|
90
|
+
<span
|
|
91
|
+
style={{
|
|
92
|
+
width: fontSize * 2.4,
|
|
93
|
+
textAlign: "right",
|
|
94
|
+
paddingRight: fontSize * 0.8,
|
|
95
|
+
color: "#3A4252",
|
|
96
|
+
userSelect: "none",
|
|
97
|
+
flexShrink: 0,
|
|
98
|
+
}}
|
|
99
|
+
>
|
|
100
|
+
{i + 1}
|
|
101
|
+
</span>
|
|
102
|
+
<span style={{ whiteSpace: "pre" }}>
|
|
103
|
+
{line.map((tok, j) => (
|
|
104
|
+
<span
|
|
105
|
+
key={j}
|
|
106
|
+
style={{
|
|
107
|
+
color: TOKEN_COLORS[tok.c ?? "plain"],
|
|
108
|
+
...(hl && tok.c === "str" ? glow(color.accent, 0.6) : {}),
|
|
109
|
+
}}
|
|
110
|
+
>
|
|
111
|
+
{tok.t}
|
|
112
|
+
</span>
|
|
113
|
+
))}
|
|
114
|
+
</span>
|
|
115
|
+
</div>
|
|
116
|
+
);
|
|
117
|
+
})}
|
|
118
|
+
</div>
|
|
119
|
+
</div>
|
|
120
|
+
</div>
|
|
121
|
+
);
|
|
122
|
+
};
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// Cursor as an actor (STYLE.md rule 4): oversized macOS cursor gliding between keyframes
|
|
2
|
+
// with cinematic easing, click = press-scale + expanding ring + click SFX.
|
|
3
|
+
|
|
4
|
+
import React from "react";
|
|
5
|
+
import { useCurrentFrame } from "remotion";
|
|
6
|
+
import { E, tween } from "./motion";
|
|
7
|
+
import { Click } from "./sfx";
|
|
8
|
+
import { color } from "./tokens";
|
|
9
|
+
|
|
10
|
+
export type CursorKeyframe = { frame: number; x: number; y: number };
|
|
11
|
+
|
|
12
|
+
export const CursorActor: React.FC<{
|
|
13
|
+
path: CursorKeyframe[];
|
|
14
|
+
/** frames at which a click happens (ring + press + sfx) */
|
|
15
|
+
clicks?: number[];
|
|
16
|
+
scale?: number;
|
|
17
|
+
sfx?: boolean;
|
|
18
|
+
}> = ({ path, clicks = [], scale = 1.6, sfx = true }) => {
|
|
19
|
+
const frame = useCurrentFrame();
|
|
20
|
+
const sorted = [...path].sort((a, b) => a.frame - b.frame);
|
|
21
|
+
if (frame < sorted[0].frame) return null;
|
|
22
|
+
|
|
23
|
+
let x = sorted[sorted.length - 1].x;
|
|
24
|
+
let y = sorted[sorted.length - 1].y;
|
|
25
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
26
|
+
if (sorted[i].frame >= frame) {
|
|
27
|
+
const a = sorted[i - 1];
|
|
28
|
+
const b = sorted[i];
|
|
29
|
+
x = tween(frame, [a.frame, b.frame], [a.x, b.x], E.cinematic);
|
|
30
|
+
y = tween(frame, [a.frame, b.frame], [a.y, b.y], E.cinematic);
|
|
31
|
+
break;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// press scale near a click
|
|
36
|
+
const press = clicks.reduce((acc, c) => {
|
|
37
|
+
if (frame >= c && frame <= c + 8) {
|
|
38
|
+
return Math.min(acc, 1 - 0.18 * Math.sin(((frame - c) / 8) * Math.PI));
|
|
39
|
+
}
|
|
40
|
+
return acc;
|
|
41
|
+
}, 1);
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<>
|
|
45
|
+
{/* click rings */}
|
|
46
|
+
{clicks.map((c, i) => {
|
|
47
|
+
if (frame < c || frame > c + 22) return null;
|
|
48
|
+
const p = tween(frame, [c, c + 22], [0, 1], E.snap);
|
|
49
|
+
return (
|
|
50
|
+
<div
|
|
51
|
+
key={i}
|
|
52
|
+
style={{
|
|
53
|
+
position: "absolute",
|
|
54
|
+
left: x - 60 * p,
|
|
55
|
+
top: y - 60 * p,
|
|
56
|
+
width: 120 * p,
|
|
57
|
+
height: 120 * p,
|
|
58
|
+
borderRadius: "50%",
|
|
59
|
+
border: `3px solid ${color.accent}`,
|
|
60
|
+
opacity: 1 - p,
|
|
61
|
+
boxShadow: `0 0 24px ${color.accent}66`,
|
|
62
|
+
}}
|
|
63
|
+
/>
|
|
64
|
+
);
|
|
65
|
+
})}
|
|
66
|
+
{/* macOS arrow cursor */}
|
|
67
|
+
<svg
|
|
68
|
+
width={28 * scale}
|
|
69
|
+
height={40 * scale}
|
|
70
|
+
viewBox="0 0 28 40"
|
|
71
|
+
style={{
|
|
72
|
+
position: "absolute",
|
|
73
|
+
left: x,
|
|
74
|
+
top: y,
|
|
75
|
+
transform: `scale(${press})`,
|
|
76
|
+
transformOrigin: "top left",
|
|
77
|
+
filter: "drop-shadow(0 4px 10px rgba(0,0,0,0.5))",
|
|
78
|
+
}}
|
|
79
|
+
>
|
|
80
|
+
<path
|
|
81
|
+
d="M2 2 L2 32 L10 25 L15 37 L20 35 L15 23 L26 23 Z"
|
|
82
|
+
fill="#FFFFFF"
|
|
83
|
+
stroke="#000000"
|
|
84
|
+
strokeWidth={2}
|
|
85
|
+
/>
|
|
86
|
+
</svg>
|
|
87
|
+
{sfx ? clicks.map((c, i) => <Click key={i} at={c} />) : null}
|
|
88
|
+
</>
|
|
89
|
+
);
|
|
90
|
+
};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// Browser window frame (macOS chrome + URL pill) hosting arbitrary DOM content.
|
|
2
|
+
|
|
3
|
+
import React from "react";
|
|
4
|
+
import { color, panelShadow, radius } from "./tokens";
|
|
5
|
+
|
|
6
|
+
// System-safe fallback, avoids @remotion/google-fonts' network fetch (hangs render offline).
|
|
7
|
+
const uiFamily = "Inter, -apple-system, Segoe UI, Roboto, sans-serif";
|
|
8
|
+
|
|
9
|
+
export const BrowserFrame: React.FC<{
|
|
10
|
+
url: string;
|
|
11
|
+
width?: number;
|
|
12
|
+
height?: number;
|
|
13
|
+
dark?: boolean;
|
|
14
|
+
children: React.ReactNode;
|
|
15
|
+
}> = ({ url, width = 1360, height = 850, dark = true, children }) => {
|
|
16
|
+
const chrome = dark ? "#151A24" : "#EDF0F4";
|
|
17
|
+
const body = dark ? "#0E1219" : "#FFFFFF";
|
|
18
|
+
const border = dark ? color.panelBorder : "#D8DEE7";
|
|
19
|
+
const text = dark ? color.textSecondary : "#5B6572";
|
|
20
|
+
return (
|
|
21
|
+
<div
|
|
22
|
+
style={{
|
|
23
|
+
width,
|
|
24
|
+
height,
|
|
25
|
+
borderRadius: radius.window,
|
|
26
|
+
background: body,
|
|
27
|
+
border: `1px solid ${border}`,
|
|
28
|
+
boxShadow: panelShadow(dark),
|
|
29
|
+
overflow: "hidden",
|
|
30
|
+
fontFamily: uiFamily,
|
|
31
|
+
}}
|
|
32
|
+
>
|
|
33
|
+
<div
|
|
34
|
+
style={{
|
|
35
|
+
height: 56,
|
|
36
|
+
display: "flex",
|
|
37
|
+
alignItems: "center",
|
|
38
|
+
gap: 16,
|
|
39
|
+
padding: "0 20px",
|
|
40
|
+
background: chrome,
|
|
41
|
+
borderBottom: `1px solid ${border}`,
|
|
42
|
+
}}
|
|
43
|
+
>
|
|
44
|
+
<div style={{ display: "flex", gap: 10 }}>
|
|
45
|
+
{["#FF5F57", "#FEBC2E", "#28C840"].map((c) => (
|
|
46
|
+
<div key={c} style={{ width: 14, height: 14, borderRadius: 7, background: c }} />
|
|
47
|
+
))}
|
|
48
|
+
</div>
|
|
49
|
+
<div
|
|
50
|
+
style={{
|
|
51
|
+
flex: 1,
|
|
52
|
+
maxWidth: 640,
|
|
53
|
+
margin: "0 auto",
|
|
54
|
+
height: 34,
|
|
55
|
+
borderRadius: 17,
|
|
56
|
+
background: dark ? "#0E1219" : "#FFFFFF",
|
|
57
|
+
border: `1px solid ${border}`,
|
|
58
|
+
display: "flex",
|
|
59
|
+
alignItems: "center",
|
|
60
|
+
justifyContent: "center",
|
|
61
|
+
fontSize: 17,
|
|
62
|
+
color: text,
|
|
63
|
+
}}
|
|
64
|
+
>
|
|
65
|
+
{url}
|
|
66
|
+
</div>
|
|
67
|
+
<div style={{ width: 62 }} />
|
|
68
|
+
</div>
|
|
69
|
+
<div style={{ position: "relative", width: "100%", height: height - 56, overflow: "hidden" }}>
|
|
70
|
+
{children}
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
);
|
|
74
|
+
};
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
// Kinetic typography: word-staggered title slams + caption bars.
|
|
2
|
+
// Mask/clip reveals only, plain opacity fades are banned (STYLE.md).
|
|
3
|
+
|
|
4
|
+
import React from "react";
|
|
5
|
+
import { useCurrentFrame, useVideoConfig } from "remotion";
|
|
6
|
+
import { pop, SPRING, staggerDelay, tween, E } from "./motion";
|
|
7
|
+
import { Blip } from "./sfx";
|
|
8
|
+
import { color } from "./tokens";
|
|
9
|
+
|
|
10
|
+
// System-safe fallback stack, not the @remotion/google-fonts network loader: that loader fetches
|
|
11
|
+
// from fonts.gstatic.com at render time, which hangs the whole render (30s timeout, then a fatal
|
|
12
|
+
// crash-loop) with no internet. Same visual family, no network dependency.
|
|
13
|
+
const uiFamily = "Inter, -apple-system, Segoe UI, Roboto, sans-serif";
|
|
14
|
+
|
|
15
|
+
/** Big title: each word slams in with a spring + clip wipe. */
|
|
16
|
+
export const TitleSlam: React.FC<{
|
|
17
|
+
text: string;
|
|
18
|
+
at: number;
|
|
19
|
+
fontSize?: number;
|
|
20
|
+
color?: string;
|
|
21
|
+
glowColor?: string;
|
|
22
|
+
align?: "center" | "left";
|
|
23
|
+
sfx?: boolean;
|
|
24
|
+
}> = ({
|
|
25
|
+
text,
|
|
26
|
+
at,
|
|
27
|
+
fontSize = 120,
|
|
28
|
+
color: textColor = color.textPrimary,
|
|
29
|
+
glowColor,
|
|
30
|
+
align = "center",
|
|
31
|
+
sfx = true,
|
|
32
|
+
}) => {
|
|
33
|
+
const frame = useCurrentFrame();
|
|
34
|
+
const { fps } = useVideoConfig();
|
|
35
|
+
const words = text.split(" ");
|
|
36
|
+
if (frame < at) return null;
|
|
37
|
+
return (
|
|
38
|
+
<div
|
|
39
|
+
style={{
|
|
40
|
+
display: "flex",
|
|
41
|
+
gap: fontSize * 0.28,
|
|
42
|
+
justifyContent: align === "center" ? "center" : "flex-start",
|
|
43
|
+
flexWrap: "wrap",
|
|
44
|
+
fontFamily: uiFamily,
|
|
45
|
+
fontWeight: 900,
|
|
46
|
+
fontSize,
|
|
47
|
+
letterSpacing: "-0.03em",
|
|
48
|
+
color: textColor,
|
|
49
|
+
textShadow: glowColor
|
|
50
|
+
? `0 0 24px ${glowColor}66, 0 0 80px ${glowColor}33`
|
|
51
|
+
: undefined,
|
|
52
|
+
}}
|
|
53
|
+
>
|
|
54
|
+
{words.map((w, i) => {
|
|
55
|
+
const d = at + staggerDelay(i, 4);
|
|
56
|
+
const s = pop(frame, fps, d, SPRING.pop);
|
|
57
|
+
const wipe = tween(frame, [d, d + 6], [100, 0], E.snap);
|
|
58
|
+
return (
|
|
59
|
+
<span
|
|
60
|
+
key={i}
|
|
61
|
+
style={{
|
|
62
|
+
display: "inline-block",
|
|
63
|
+
transform: `scale(${0.7 + s * 0.3}) translateY(${(1 - s) * 40}px)`,
|
|
64
|
+
clipPath: `inset(0 ${wipe}% 0 0)`,
|
|
65
|
+
}}
|
|
66
|
+
>
|
|
67
|
+
{w}
|
|
68
|
+
</span>
|
|
69
|
+
);
|
|
70
|
+
})}
|
|
71
|
+
{sfx ? words.map((_, i) => <Blip key={i} at={at + staggerDelay(i, 4)} volume={0.18} />) : null}
|
|
72
|
+
</div>
|
|
73
|
+
);
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
/** Bottom caption bar: single line, word-stagger clip reveal, auto-out. */
|
|
77
|
+
export const Caption: React.FC<{
|
|
78
|
+
text: string;
|
|
79
|
+
at: number;
|
|
80
|
+
out?: number;
|
|
81
|
+
fontSize?: number;
|
|
82
|
+
}> = ({ text, at, out, fontSize = 34 }) => {
|
|
83
|
+
const frame = useCurrentFrame();
|
|
84
|
+
const words = text.split(" ");
|
|
85
|
+
if (frame < at || (out !== undefined && frame > out + 10)) return null;
|
|
86
|
+
const exit =
|
|
87
|
+
out !== undefined ? tween(frame, [out, out + 10], [0, 1], E.snap) : 0;
|
|
88
|
+
return (
|
|
89
|
+
<div
|
|
90
|
+
style={{
|
|
91
|
+
position: "absolute",
|
|
92
|
+
bottom: 84,
|
|
93
|
+
left: 0,
|
|
94
|
+
right: 0,
|
|
95
|
+
display: "flex",
|
|
96
|
+
justifyContent: "center",
|
|
97
|
+
transform: `translateY(${exit * 24}px)`,
|
|
98
|
+
opacity: 1 - exit,
|
|
99
|
+
}}
|
|
100
|
+
>
|
|
101
|
+
<div
|
|
102
|
+
style={{
|
|
103
|
+
fontFamily: uiFamily,
|
|
104
|
+
fontWeight: 500,
|
|
105
|
+
fontSize,
|
|
106
|
+
color: color.textPrimary,
|
|
107
|
+
// glassmorphism caption bar, opaque enough to read over white browser-chrome
|
|
108
|
+
// recordings (was 0.45, washed out against BrowserFrame's white body)
|
|
109
|
+
background: "rgba(12,15,22,0.82)",
|
|
110
|
+
backdropFilter: "blur(18px) saturate(1.3)",
|
|
111
|
+
WebkitBackdropFilter: "blur(18px) saturate(1.3)",
|
|
112
|
+
border: "1px solid rgba(255,255,255,0.12)",
|
|
113
|
+
boxShadow: "0 8px 40px rgba(0,0,0,0.5), inset 0 1px 0 rgba(255,255,255,0.08)",
|
|
114
|
+
borderRadius: 14,
|
|
115
|
+
padding: `${fontSize * 0.45}px ${fontSize * 0.9}px`,
|
|
116
|
+
display: "flex",
|
|
117
|
+
gap: fontSize * 0.26,
|
|
118
|
+
flexWrap: "wrap",
|
|
119
|
+
justifyContent: "center",
|
|
120
|
+
maxWidth: 1400,
|
|
121
|
+
}}
|
|
122
|
+
>
|
|
123
|
+
{words.map((w, i) => {
|
|
124
|
+
const d = at + staggerDelay(i, 2);
|
|
125
|
+
const wipe = tween(frame, [d, d + 5], [100, 0], E.snap);
|
|
126
|
+
return (
|
|
127
|
+
<span
|
|
128
|
+
key={i}
|
|
129
|
+
style={{
|
|
130
|
+
clipPath: `inset(0 ${wipe}% 0 0)`,
|
|
131
|
+
display: "inline-block",
|
|
132
|
+
textShadow: "0 1px 3px rgba(0,0,0,0.6)",
|
|
133
|
+
}}
|
|
134
|
+
>
|
|
135
|
+
{w}
|
|
136
|
+
</span>
|
|
137
|
+
);
|
|
138
|
+
})}
|
|
139
|
+
</div>
|
|
140
|
+
</div>
|
|
141
|
+
);
|
|
142
|
+
};
|