@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 +51 -0
- package/public/sfx/bell.wav +0 -0
- package/public/sfx/blip.wav +0 -0
- package/public/sfx/click.wav +0 -0
- package/public/sfx/key_a.wav +0 -0
- package/public/sfx/key_b.wav +0 -0
- package/public/sfx/key_c.wav +0 -0
- package/public/sfx/key_enter.wav +0 -0
- package/public/sfx/music-bed.mp3 +0 -0
- package/public/sfx/success.wav +0 -0
- package/public/sfx/whoosh.wav +0 -0
- package/scripts/gen-sfx.sh +74 -0
- package/src/ChecklistPanel.tsx +97 -0
- package/src/CinematicScene.tsx +53 -0
- package/src/CodePanel.tsx +122 -0
- package/src/CursorActor.tsx +90 -0
- package/src/DeviceFrame.tsx +74 -0
- package/src/KineticText.tsx +142 -0
- package/src/RepoCta.tsx +128 -0
- package/src/TerminalReplay.tsx +263 -0
- package/src/camera.tsx +141 -0
- package/src/index.ts +60 -0
- package/src/look.tsx +90 -0
- package/src/motion.ts +50 -0
- package/src/sfx.tsx +106 -0
- package/src/tokens.ts +41 -0
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
|
+
};
|