@openvidstudio/core 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/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@openvidstudio/core",
3
+ "version": "0.1.0",
4
+ "description": "Camera rig, motion, SFX, look pass, and scene components shared by every openvidstudio video project.",
5
+ "license": "Apache-2.0",
6
+ "type": "commonjs",
7
+ "main": "src/index.ts",
8
+ "types": "src/index.ts",
9
+ "files": [
10
+ "src",
11
+ "scripts",
12
+ "public"
13
+ ],
14
+ "dependencies": {
15
+ "@remotion/google-fonts": "^4.0.490"
16
+ },
17
+ "peerDependencies": {
18
+ "react": "19.2.3",
19
+ "react-dom": "19.2.3",
20
+ "remotion": "4.0.490"
21
+ },
22
+ "devDependencies": {
23
+ "@types/react": "19.2.7",
24
+ "react": "19.2.3",
25
+ "react-dom": "19.2.3",
26
+ "remotion": "4.0.490",
27
+ "typescript": "5.9.3"
28
+ },
29
+ "scripts": {
30
+ "typecheck": "tsc --noEmit"
31
+ },
32
+ "publishConfig": {
33
+ "access": "public"
34
+ },
35
+ "repository": {
36
+ "type": "git",
37
+ "url": "git+https://github.com/AnayDhawan/openvidstudio.git",
38
+ "directory": "packages/core"
39
+ },
40
+ "homepage": "https://github.com/AnayDhawan/openvidstudio#readme",
41
+ "bugs": {
42
+ "url": "https://github.com/AnayDhawan/openvidstudio/issues"
43
+ },
44
+ "keywords": [
45
+ "remotion",
46
+ "video",
47
+ "demo-video",
48
+ "screencast",
49
+ "mcp"
50
+ ]
51
+ }
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
@@ -0,0 +1,74 @@
1
+ #!/bin/bash
2
+ # Generate synthesized UI SFX into public/sfx/ (no licensing, fully reproducible).
3
+ # Swap any file for a real recorded pack later, sfx.tsx API stays identical.
4
+ set -e
5
+ cd "$(dirname "$0")/.."
6
+ mkdir -p public/sfx
7
+
8
+ # Mechanical key thocks: filtered brown-noise bursts, 3 pitch variations
9
+ ffmpeg -v error -y -f lavfi -i "anoisesrc=d=0.06:c=brown:a=0.9" \
10
+ -af "highpass=f=180,lowpass=f=2400,afade=t=in:d=0.004,afade=t=out:st=0.025:d=0.035,volume=3.2" \
11
+ public/sfx/key_a.wav
12
+ ffmpeg -v error -y -f lavfi -i "anoisesrc=d=0.055:c=brown:a=0.9:seed=7" \
13
+ -af "highpass=f=200,lowpass=f=1900,afade=t=in:d=0.004,afade=t=out:st=0.022:d=0.033,volume=3.4" \
14
+ public/sfx/key_b.wav
15
+ ffmpeg -v error -y -f lavfi -i "anoisesrc=d=0.065:c=brown:a=0.9:seed=23" \
16
+ -af "highpass=f=160,lowpass=f=2900,afade=t=in:d=0.004,afade=t=out:st=0.028:d=0.037,volume=3.0" \
17
+ public/sfx/key_c.wav
18
+
19
+ # Click: sharp short tick
20
+ ffmpeg -v error -y -f lavfi -i "anoisesrc=d=0.03:c=white:a=0.8" \
21
+ -af "highpass=f=1400,lowpass=f=6500,afade=t=in:d=0.002,afade=t=out:st=0.01:d=0.02,volume=2.2" \
22
+ public/sfx/click.wav
23
+
24
+ # Whoosh: bandpassed pink noise swell
25
+ ffmpeg -v error -y -f lavfi -i "anoisesrc=d=0.55:c=pink:a=0.8" \
26
+ -af "bandpass=f=650:w=500,afade=t=in:d=0.18,afade=t=out:st=0.25:d=0.3,volume=2.6" \
27
+ public/sfx/whoosh.wav
28
+
29
+ # Blip: soft sine tick for UI reveals
30
+ ffmpeg -v error -y -f lavfi -i "sine=frequency=740:duration=0.07" \
31
+ -af "afade=t=in:d=0.008,afade=t=out:st=0.03:d=0.04,volume=0.9" \
32
+ public/sfx/blip.wav
33
+
34
+ # Success: rising two-tone
35
+ ffmpeg -v error -y \
36
+ -f lavfi -i "sine=frequency=660:duration=0.09" \
37
+ -f lavfi -i "sine=frequency=990:duration=0.14" \
38
+ -filter_complex "[0]afade=t=out:st=0.05:d=0.04[a];[1]afade=t=in:d=0.01,afade=t=out:st=0.08:d=0.06[b];[a][b]concat=n=2:v=0:a=1,volume=0.9" \
39
+ public/sfx/success.wav
40
+
41
+ echo "SFX generated:"
42
+ ls public/sfx/
43
+
44
+ # Bell: struck metallic tone, harmonic stack with a long decay
45
+ ffmpeg -v error -y \
46
+ -f lavfi -i "sine=frequency=1568:duration=1.6" \
47
+ -f lavfi -i "sine=frequency=2350:duration=1.6" \
48
+ -f lavfi -i "sine=frequency=3136:duration=1.6" \
49
+ -filter_complex "[0]volume=1.0[a];[1]volume=0.45[b];[2]volume=0.22[c];[a][b][c]amix=inputs=3:normalize=0,afade=t=in:d=0.004,afade=t=out:st=0.12:d=1.45,volume=1.2" \
50
+ public/sfx/bell.wav
51
+
52
+ # Enter key: deeper, heavier thock than a letter key
53
+ ffmpeg -v error -y -f lavfi -i "anoisesrc=d=0.085:c=brown:a=0.95:seed=41" \
54
+ -af "highpass=f=110,lowpass=f=1500,afade=t=in:d=0.004,afade=t=out:st=0.03:d=0.05,volume=3.6" \
55
+ public/sfx/key_enter.wav
56
+
57
+ # Music bed: slow ambient pad, two detuned sines under a filtered noise wash.
58
+ # Synthesized rather than licensed, so a generated video carries no third-party
59
+ # audio rights. 60s, loop it for longer videos.
60
+ ffmpeg -v error -y \
61
+ -f lavfi -i "sine=frequency=110:duration=60" \
62
+ -f lavfi -i "sine=frequency=164.81:duration=60" \
63
+ -f lavfi -i "sine=frequency=220.5:duration=60" \
64
+ -f lavfi -i "anoisesrc=d=60:c=pink:a=0.06" \
65
+ -filter_complex "\
66
+ [0]volume=0.34,tremolo=f=0.11:d=0.30[p1];\
67
+ [1]volume=0.20,tremolo=f=0.13:d=0.26[p2];\
68
+ [2]volume=0.13,tremolo=f=0.17:d=0.35[p3];\
69
+ [3]lowpass=f=900,volume=0.5[air];\
70
+ [p1][p2][p3][air]amix=inputs=4:normalize=0,\
71
+ lowpass=f=2600,afade=t=in:d=3,afade=t=out:st=56:d=4,volume=0.5" \
72
+ -c:a libmp3lame -q:a 5 public/sfx/music-bed.mp3
73
+
74
+ echo "generated: $(ls public/sfx | wc -l) files in public/sfx/"
@@ -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
+ };