@vibeo/cli 0.1.1 → 0.1.2
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/commands/create.d.ts.map +1 -1
- package/dist/commands/create.js +222 -31
- package/dist/commands/create.js.map +1 -1
- package/package.json +1 -1
- package/src/commands/create.ts +230 -33
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"create.d.ts","sourceRoot":"","sources":["../../src/commands/create.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"create.d.ts","sourceRoot":"","sources":["../../src/commands/create.ts"],"names":[],"mappings":"AA+RA,wBAAsB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CA0GjE"}
|
package/dist/commands/create.js
CHANGED
|
@@ -1,22 +1,227 @@
|
|
|
1
1
|
import { resolve, join } from "node:path";
|
|
2
|
-
import { mkdir,
|
|
2
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
3
3
|
import { existsSync } from "node:fs";
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// Embedded templates (so `create` works from npm without the examples/ dir)
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
const TEMPLATE_BASIC = `import React from "react";
|
|
8
|
+
import {
|
|
9
|
+
Composition, Sequence, VibeoRoot,
|
|
10
|
+
useCurrentFrame, useVideoConfig, interpolate, easeInOut,
|
|
11
|
+
} from "@vibeo/core";
|
|
12
|
+
|
|
13
|
+
function TitleScene() {
|
|
14
|
+
const frame = useCurrentFrame();
|
|
15
|
+
const { width, height } = useVideoConfig();
|
|
16
|
+
const opacity = interpolate(frame, [0, 30], [0, 1], { extrapolateRight: "clamp" });
|
|
17
|
+
const y = interpolate(frame, [0, 30], [40, 0], { easing: easeInOut, extrapolateRight: "clamp" });
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<div style={{ width, height, display: "flex", justifyContent: "center", alignItems: "center", background: "linear-gradient(135deg, #0f0c29, #302b63, #24243e)" }}>
|
|
21
|
+
<h1 style={{ color: "white", fontSize: 72, fontFamily: "sans-serif", opacity, transform: \`translateY(\${y}px)\` }}>Hello, Vibeo!</h1>
|
|
22
|
+
</div>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function ContentScene() {
|
|
27
|
+
const frame = useCurrentFrame();
|
|
28
|
+
const { width, height, fps } = useVideoConfig();
|
|
29
|
+
const seconds = (frame / fps).toFixed(1);
|
|
30
|
+
const scale = interpolate(frame, [0, 20], [0.8, 1], { easing: easeInOut, extrapolateRight: "clamp" });
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<div style={{ width, height, display: "flex", flexDirection: "column", justifyContent: "center", alignItems: "center", background: "#24243e" }}>
|
|
34
|
+
<div style={{ transform: \`scale(\${scale})\`, color: "white", fontSize: 48, fontFamily: "sans-serif", textAlign: "center" }}>
|
|
35
|
+
<p>Scene 2</p>
|
|
36
|
+
<p style={{ fontSize: 32, opacity: 0.7 }}>{seconds}s elapsed</p>
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function MyVideo() {
|
|
43
|
+
return (
|
|
44
|
+
<>
|
|
45
|
+
<Sequence from={0} durationInFrames={75} name="Title"><TitleScene /></Sequence>
|
|
46
|
+
<Sequence from={75} durationInFrames={75} name="Content"><ContentScene /></Sequence>
|
|
47
|
+
</>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function Root() {
|
|
52
|
+
return (
|
|
53
|
+
<VibeoRoot>
|
|
54
|
+
<Composition id="BasicComposition" component={MyVideo} width={1920} height={1080} fps={30} durationInFrames={150} />
|
|
55
|
+
</VibeoRoot>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
`;
|
|
59
|
+
const TEMPLATE_AUDIO_REACTIVE = `import React from "react";
|
|
60
|
+
import { Composition, VibeoRoot, useCurrentFrame, useVideoConfig, interpolate } from "@vibeo/core";
|
|
61
|
+
import { Audio } from "@vibeo/audio";
|
|
62
|
+
import { useAudioData } from "@vibeo/effects";
|
|
63
|
+
|
|
64
|
+
const AUDIO_SRC = "/music.mp3";
|
|
65
|
+
|
|
66
|
+
function FrequencyBars() {
|
|
67
|
+
const { width, height } = useVideoConfig();
|
|
68
|
+
const audio = useAudioData(AUDIO_SRC, { fftSize: 1024 });
|
|
69
|
+
if (!audio) return <div style={{ width, height }} />;
|
|
70
|
+
|
|
71
|
+
const barCount = 48;
|
|
72
|
+
const step = Math.floor(audio.frequencies.length / barCount);
|
|
73
|
+
const barWidth = (width * 0.8) / barCount;
|
|
74
|
+
const maxBarHeight = height * 0.6;
|
|
75
|
+
|
|
76
|
+
return (
|
|
77
|
+
<div style={{ position: "absolute", bottom: 60, left: width * 0.1, display: "flex", alignItems: "flex-end", gap: 2 }}>
|
|
78
|
+
{Array.from({ length: barCount }, (_, i) => {
|
|
79
|
+
const db = audio.frequencies[i * step];
|
|
80
|
+
const normalized = Math.max(0, (db + 100) / 100);
|
|
81
|
+
const hue = interpolate(i, [0, barCount - 1], [220, 340]);
|
|
82
|
+
return (
|
|
83
|
+
<div key={i} style={{ width: barWidth - 2, height: Math.max(2, normalized * maxBarHeight), background: \`hsl(\${hue}, 80%, 60%)\`, borderRadius: 2 }} />
|
|
84
|
+
);
|
|
85
|
+
})}
|
|
86
|
+
</div>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function AudioViz() {
|
|
91
|
+
const frame = useCurrentFrame();
|
|
92
|
+
const { width, height, fps } = useVideoConfig();
|
|
93
|
+
const audio = useAudioData(AUDIO_SRC);
|
|
94
|
+
const hue = 240 + (audio ? audio.amplitude * 60 : 0);
|
|
95
|
+
const lightness = audio ? 8 + audio.amplitude * 12 : 8;
|
|
96
|
+
|
|
97
|
+
return (
|
|
98
|
+
<div style={{ width, height, background: \`radial-gradient(ellipse at center, hsl(\${hue}, 40%, \${lightness + 5}%), hsl(\${hue}, 30%, \${lightness}%))\`, position: "relative", overflow: "hidden" }}>
|
|
99
|
+
<div style={{ position: "absolute", top: 40, left: 40, color: "white", fontFamily: "sans-serif" }}>
|
|
100
|
+
<h1 style={{ fontSize: 36, margin: 0, opacity: 0.9 }}>Audio Visualizer</h1>
|
|
101
|
+
<p style={{ fontSize: 18, margin: "8px 0 0", opacity: 0.5 }}>{(frame / fps).toFixed(1)}s</p>
|
|
102
|
+
</div>
|
|
103
|
+
<FrequencyBars />
|
|
104
|
+
<Audio src={AUDIO_SRC} volume={0.8} />
|
|
105
|
+
</div>
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function Root() {
|
|
110
|
+
return (
|
|
111
|
+
<VibeoRoot>
|
|
112
|
+
<Composition id="AudioReactiveViz" component={AudioViz} width={1920} height={1080} fps={30} durationInFrames={900} />
|
|
113
|
+
</VibeoRoot>
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
`;
|
|
117
|
+
const TEMPLATE_TRANSITIONS = `import React from "react";
|
|
118
|
+
import { Composition, Sequence, VibeoRoot, useCurrentFrame, useVideoConfig, interpolate, easeOut } from "@vibeo/core";
|
|
119
|
+
import { Transition } from "@vibeo/effects";
|
|
120
|
+
|
|
121
|
+
function ColorScene({ title, color }: { title: string; color: string }) {
|
|
122
|
+
const frame = useCurrentFrame();
|
|
123
|
+
const { width, height } = useVideoConfig();
|
|
124
|
+
const opacity = interpolate(frame, [0, 20], [0, 1], { easing: easeOut, extrapolateRight: "clamp" });
|
|
125
|
+
|
|
126
|
+
return (
|
|
127
|
+
<div style={{ width, height, background: color, display: "flex", justifyContent: "center", alignItems: "center" }}>
|
|
128
|
+
<h1 style={{ color: "white", fontSize: 80, fontFamily: "sans-serif", opacity }}>{title}</h1>
|
|
129
|
+
</div>
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function SceneA() { return <ColorScene title="Scene One" color="linear-gradient(135deg, #667eea, #764ba2)" />; }
|
|
134
|
+
function SceneB() { return <ColorScene title="Scene Two" color="linear-gradient(135deg, #f093fb, #f5576c)" />; }
|
|
135
|
+
function SceneC() { return <ColorScene title="Scene Three" color="linear-gradient(135deg, #4facfe, #00f2fe)" />; }
|
|
136
|
+
|
|
137
|
+
function TransitionDemo() {
|
|
138
|
+
return (
|
|
139
|
+
<>
|
|
140
|
+
<Sequence from={0} durationInFrames={85}><SceneA /></Sequence>
|
|
141
|
+
<Sequence from={65} durationInFrames={20}>
|
|
142
|
+
<Transition type="fade" durationInFrames={20}><SceneA /><SceneB /></Transition>
|
|
143
|
+
</Sequence>
|
|
144
|
+
<Sequence from={85} durationInFrames={85}><SceneB /></Sequence>
|
|
145
|
+
<Sequence from={150} durationInFrames={20}>
|
|
146
|
+
<Transition type="slide" durationInFrames={20} direction="left"><SceneB /><SceneC /></Transition>
|
|
147
|
+
</Sequence>
|
|
148
|
+
<Sequence from={170} durationInFrames={70}><SceneC /></Sequence>
|
|
149
|
+
</>
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function Root() {
|
|
154
|
+
return (
|
|
155
|
+
<VibeoRoot>
|
|
156
|
+
<Composition id="TransitionDemo" component={TransitionDemo} width={1920} height={1080} fps={30} durationInFrames={240} />
|
|
157
|
+
</VibeoRoot>
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
`;
|
|
161
|
+
const TEMPLATE_SUBTITLES = `import React from "react";
|
|
162
|
+
import { Composition, Sequence, VibeoRoot, useCurrentFrame, useVideoConfig, interpolate } from "@vibeo/core";
|
|
163
|
+
import { Subtitle } from "@vibeo/extras";
|
|
164
|
+
|
|
165
|
+
const SUBTITLES_SRT = \`1
|
|
166
|
+
00:00:00,500 --> 00:00:03,000
|
|
167
|
+
Welcome to the Vibeo demo.
|
|
168
|
+
|
|
169
|
+
2
|
|
170
|
+
00:00:03,500 --> 00:00:06,000
|
|
171
|
+
This shows subtitle overlays.
|
|
172
|
+
|
|
173
|
+
3
|
|
174
|
+
00:00:06,500 --> 00:00:09,000
|
|
175
|
+
Subtitles are synced to the frame timeline.
|
|
176
|
+
|
|
177
|
+
4
|
|
178
|
+
00:00:09,500 --> 00:00:12,000
|
|
179
|
+
You can use <b>bold</b> and <i>italic</i> text.
|
|
180
|
+
|
|
181
|
+
5
|
|
182
|
+
00:00:13,000 --> 00:00:16,000
|
|
183
|
+
The end. Thanks for watching!\`;
|
|
184
|
+
|
|
185
|
+
function SubtitleVideo() {
|
|
186
|
+
const frame = useCurrentFrame();
|
|
187
|
+
const { width, height } = useVideoConfig();
|
|
188
|
+
const hue = interpolate(frame, [0, 480], [200, 280]);
|
|
189
|
+
|
|
190
|
+
return (
|
|
191
|
+
<div style={{ width, height, position: "relative" }}>
|
|
192
|
+
<div style={{ width, height, background: \`linear-gradient(135deg, hsl(\${hue}, 50%, 15%), hsl(\${hue + 40}, 40%, 10%))\` }} />
|
|
193
|
+
<div style={{ position: "absolute", top: 0, left: 0, width, height }}>
|
|
194
|
+
<Subtitle src={SUBTITLES_SRT} format="srt" position="bottom" fontSize={36} color="white" outlineColor="black" outlineWidth={2} style={{ padding: "0 80px 60px" }} />
|
|
195
|
+
</div>
|
|
196
|
+
</div>
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export function Root() {
|
|
201
|
+
return (
|
|
202
|
+
<VibeoRoot>
|
|
203
|
+
<Composition id="SubtitleOverlay" component={SubtitleVideo} width={1920} height={1080} fps={30} durationInFrames={480} />
|
|
204
|
+
</VibeoRoot>
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
`;
|
|
208
|
+
// ---------------------------------------------------------------------------
|
|
4
209
|
const TEMPLATES = {
|
|
5
210
|
basic: {
|
|
6
211
|
description: "Minimal composition with text animation and two scenes",
|
|
7
|
-
|
|
212
|
+
source: TEMPLATE_BASIC,
|
|
8
213
|
},
|
|
9
214
|
"audio-reactive": {
|
|
10
215
|
description: "Audio visualization with frequency bars and amplitude-driven effects",
|
|
11
|
-
|
|
216
|
+
source: TEMPLATE_AUDIO_REACTIVE,
|
|
12
217
|
},
|
|
13
218
|
transitions: {
|
|
14
219
|
description: "Scene transitions (fade, slide) between multiple scenes",
|
|
15
|
-
|
|
220
|
+
source: TEMPLATE_TRANSITIONS,
|
|
16
221
|
},
|
|
17
222
|
subtitles: {
|
|
18
223
|
description: "Video with SRT subtitle overlay",
|
|
19
|
-
|
|
224
|
+
source: TEMPLATE_SUBTITLES,
|
|
20
225
|
},
|
|
21
226
|
};
|
|
22
227
|
function parseArgs(args) {
|
|
@@ -63,18 +268,6 @@ Examples:
|
|
|
63
268
|
vibeo create intro --template transitions
|
|
64
269
|
`);
|
|
65
270
|
}
|
|
66
|
-
// Find the examples directory relative to the CLI package
|
|
67
|
-
function findExamplesDir() {
|
|
68
|
-
// Walk up from this file to find the repo root with examples/
|
|
69
|
-
let dir = import.meta.dir;
|
|
70
|
-
for (let i = 0; i < 6; i++) {
|
|
71
|
-
const candidate = join(dir, "examples");
|
|
72
|
-
if (existsSync(candidate))
|
|
73
|
-
return candidate;
|
|
74
|
-
dir = resolve(dir, "..");
|
|
75
|
-
}
|
|
76
|
-
throw new Error("Could not find examples directory");
|
|
77
|
-
}
|
|
78
271
|
export async function createCommand(args) {
|
|
79
272
|
const parsed = parseArgs(args);
|
|
80
273
|
if (!parsed.name) {
|
|
@@ -98,10 +291,8 @@ export async function createCommand(args) {
|
|
|
98
291
|
// Create project structure
|
|
99
292
|
await mkdir(join(projectDir, "src"), { recursive: true });
|
|
100
293
|
await mkdir(join(projectDir, "public"), { recursive: true });
|
|
101
|
-
//
|
|
102
|
-
|
|
103
|
-
const exampleSrc = await readFile(join(examplesDir, template.example), "utf-8");
|
|
104
|
-
await writeFile(join(projectDir, "src", "index.tsx"), exampleSrc);
|
|
294
|
+
// Write template source
|
|
295
|
+
await writeFile(join(projectDir, "src", "index.tsx"), template.source);
|
|
105
296
|
// Write package.json
|
|
106
297
|
const pkg = {
|
|
107
298
|
name: parsed.name,
|
|
@@ -109,19 +300,19 @@ export async function createCommand(args) {
|
|
|
109
300
|
private: true,
|
|
110
301
|
type: "module",
|
|
111
302
|
scripts: {
|
|
112
|
-
dev: "vibeo preview --entry src/index.tsx",
|
|
113
|
-
build: "vibeo render --entry src/index.tsx",
|
|
114
|
-
list: "vibeo list --entry src/index.tsx",
|
|
303
|
+
dev: "bunx @vibeo/cli preview --entry src/index.tsx",
|
|
304
|
+
build: "bunx @vibeo/cli render --entry src/index.tsx",
|
|
305
|
+
list: "bunx @vibeo/cli list --entry src/index.tsx",
|
|
115
306
|
typecheck: "bunx tsc --noEmit",
|
|
116
307
|
},
|
|
117
308
|
dependencies: {
|
|
118
|
-
"@vibeo/core": "
|
|
119
|
-
"@vibeo/audio": "
|
|
120
|
-
"@vibeo/effects": "
|
|
121
|
-
"@vibeo/extras": "
|
|
122
|
-
"@vibeo/player": "
|
|
123
|
-
"@vibeo/renderer": "
|
|
124
|
-
"@vibeo/cli": "
|
|
309
|
+
"@vibeo/core": "^0.1.0",
|
|
310
|
+
"@vibeo/audio": "^0.1.0",
|
|
311
|
+
"@vibeo/effects": "^0.1.0",
|
|
312
|
+
"@vibeo/extras": "^0.1.0",
|
|
313
|
+
"@vibeo/player": "^0.1.0",
|
|
314
|
+
"@vibeo/renderer": "^0.1.0",
|
|
315
|
+
"@vibeo/cli": "^0.1.0",
|
|
125
316
|
react: "^19.0.0",
|
|
126
317
|
"react-dom": "^19.0.0",
|
|
127
318
|
},
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"create.js","sourceRoot":"","sources":["../../src/commands/create.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,IAAI,
|
|
1
|
+
{"version":3,"file":"create.js","sourceRoot":"","sources":["../../src/commands/create.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
package/src/commands/create.ts
CHANGED
|
@@ -1,23 +1,234 @@
|
|
|
1
|
-
import { resolve, join
|
|
2
|
-
import {
|
|
1
|
+
import { resolve, join } from "node:path";
|
|
2
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
3
3
|
import { existsSync } from "node:fs";
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
// Embedded templates (so `create` works from npm without the examples/ dir)
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
|
|
9
|
+
const TEMPLATE_BASIC = `import React from "react";
|
|
10
|
+
import {
|
|
11
|
+
Composition, Sequence, VibeoRoot,
|
|
12
|
+
useCurrentFrame, useVideoConfig, interpolate, easeInOut,
|
|
13
|
+
} from "@vibeo/core";
|
|
14
|
+
|
|
15
|
+
function TitleScene() {
|
|
16
|
+
const frame = useCurrentFrame();
|
|
17
|
+
const { width, height } = useVideoConfig();
|
|
18
|
+
const opacity = interpolate(frame, [0, 30], [0, 1], { extrapolateRight: "clamp" });
|
|
19
|
+
const y = interpolate(frame, [0, 30], [40, 0], { easing: easeInOut, extrapolateRight: "clamp" });
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<div style={{ width, height, display: "flex", justifyContent: "center", alignItems: "center", background: "linear-gradient(135deg, #0f0c29, #302b63, #24243e)" }}>
|
|
23
|
+
<h1 style={{ color: "white", fontSize: 72, fontFamily: "sans-serif", opacity, transform: \`translateY(\${y}px)\` }}>Hello, Vibeo!</h1>
|
|
24
|
+
</div>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function ContentScene() {
|
|
29
|
+
const frame = useCurrentFrame();
|
|
30
|
+
const { width, height, fps } = useVideoConfig();
|
|
31
|
+
const seconds = (frame / fps).toFixed(1);
|
|
32
|
+
const scale = interpolate(frame, [0, 20], [0.8, 1], { easing: easeInOut, extrapolateRight: "clamp" });
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<div style={{ width, height, display: "flex", flexDirection: "column", justifyContent: "center", alignItems: "center", background: "#24243e" }}>
|
|
36
|
+
<div style={{ transform: \`scale(\${scale})\`, color: "white", fontSize: 48, fontFamily: "sans-serif", textAlign: "center" }}>
|
|
37
|
+
<p>Scene 2</p>
|
|
38
|
+
<p style={{ fontSize: 32, opacity: 0.7 }}>{seconds}s elapsed</p>
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function MyVideo() {
|
|
45
|
+
return (
|
|
46
|
+
<>
|
|
47
|
+
<Sequence from={0} durationInFrames={75} name="Title"><TitleScene /></Sequence>
|
|
48
|
+
<Sequence from={75} durationInFrames={75} name="Content"><ContentScene /></Sequence>
|
|
49
|
+
</>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function Root() {
|
|
54
|
+
return (
|
|
55
|
+
<VibeoRoot>
|
|
56
|
+
<Composition id="BasicComposition" component={MyVideo} width={1920} height={1080} fps={30} durationInFrames={150} />
|
|
57
|
+
</VibeoRoot>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
`;
|
|
61
|
+
|
|
62
|
+
const TEMPLATE_AUDIO_REACTIVE = `import React from "react";
|
|
63
|
+
import { Composition, VibeoRoot, useCurrentFrame, useVideoConfig, interpolate } from "@vibeo/core";
|
|
64
|
+
import { Audio } from "@vibeo/audio";
|
|
65
|
+
import { useAudioData } from "@vibeo/effects";
|
|
66
|
+
|
|
67
|
+
const AUDIO_SRC = "/music.mp3";
|
|
68
|
+
|
|
69
|
+
function FrequencyBars() {
|
|
70
|
+
const { width, height } = useVideoConfig();
|
|
71
|
+
const audio = useAudioData(AUDIO_SRC, { fftSize: 1024 });
|
|
72
|
+
if (!audio) return <div style={{ width, height }} />;
|
|
73
|
+
|
|
74
|
+
const barCount = 48;
|
|
75
|
+
const step = Math.floor(audio.frequencies.length / barCount);
|
|
76
|
+
const barWidth = (width * 0.8) / barCount;
|
|
77
|
+
const maxBarHeight = height * 0.6;
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<div style={{ position: "absolute", bottom: 60, left: width * 0.1, display: "flex", alignItems: "flex-end", gap: 2 }}>
|
|
81
|
+
{Array.from({ length: barCount }, (_, i) => {
|
|
82
|
+
const db = audio.frequencies[i * step];
|
|
83
|
+
const normalized = Math.max(0, (db + 100) / 100);
|
|
84
|
+
const hue = interpolate(i, [0, barCount - 1], [220, 340]);
|
|
85
|
+
return (
|
|
86
|
+
<div key={i} style={{ width: barWidth - 2, height: Math.max(2, normalized * maxBarHeight), background: \`hsl(\${hue}, 80%, 60%)\`, borderRadius: 2 }} />
|
|
87
|
+
);
|
|
88
|
+
})}
|
|
89
|
+
</div>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function AudioViz() {
|
|
94
|
+
const frame = useCurrentFrame();
|
|
95
|
+
const { width, height, fps } = useVideoConfig();
|
|
96
|
+
const audio = useAudioData(AUDIO_SRC);
|
|
97
|
+
const hue = 240 + (audio ? audio.amplitude * 60 : 0);
|
|
98
|
+
const lightness = audio ? 8 + audio.amplitude * 12 : 8;
|
|
99
|
+
|
|
100
|
+
return (
|
|
101
|
+
<div style={{ width, height, background: \`radial-gradient(ellipse at center, hsl(\${hue}, 40%, \${lightness + 5}%), hsl(\${hue}, 30%, \${lightness}%))\`, position: "relative", overflow: "hidden" }}>
|
|
102
|
+
<div style={{ position: "absolute", top: 40, left: 40, color: "white", fontFamily: "sans-serif" }}>
|
|
103
|
+
<h1 style={{ fontSize: 36, margin: 0, opacity: 0.9 }}>Audio Visualizer</h1>
|
|
104
|
+
<p style={{ fontSize: 18, margin: "8px 0 0", opacity: 0.5 }}>{(frame / fps).toFixed(1)}s</p>
|
|
105
|
+
</div>
|
|
106
|
+
<FrequencyBars />
|
|
107
|
+
<Audio src={AUDIO_SRC} volume={0.8} />
|
|
108
|
+
</div>
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function Root() {
|
|
113
|
+
return (
|
|
114
|
+
<VibeoRoot>
|
|
115
|
+
<Composition id="AudioReactiveViz" component={AudioViz} width={1920} height={1080} fps={30} durationInFrames={900} />
|
|
116
|
+
</VibeoRoot>
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
`;
|
|
120
|
+
|
|
121
|
+
const TEMPLATE_TRANSITIONS = `import React from "react";
|
|
122
|
+
import { Composition, Sequence, VibeoRoot, useCurrentFrame, useVideoConfig, interpolate, easeOut } from "@vibeo/core";
|
|
123
|
+
import { Transition } from "@vibeo/effects";
|
|
124
|
+
|
|
125
|
+
function ColorScene({ title, color }: { title: string; color: string }) {
|
|
126
|
+
const frame = useCurrentFrame();
|
|
127
|
+
const { width, height } = useVideoConfig();
|
|
128
|
+
const opacity = interpolate(frame, [0, 20], [0, 1], { easing: easeOut, extrapolateRight: "clamp" });
|
|
129
|
+
|
|
130
|
+
return (
|
|
131
|
+
<div style={{ width, height, background: color, display: "flex", justifyContent: "center", alignItems: "center" }}>
|
|
132
|
+
<h1 style={{ color: "white", fontSize: 80, fontFamily: "sans-serif", opacity }}>{title}</h1>
|
|
133
|
+
</div>
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function SceneA() { return <ColorScene title="Scene One" color="linear-gradient(135deg, #667eea, #764ba2)" />; }
|
|
138
|
+
function SceneB() { return <ColorScene title="Scene Two" color="linear-gradient(135deg, #f093fb, #f5576c)" />; }
|
|
139
|
+
function SceneC() { return <ColorScene title="Scene Three" color="linear-gradient(135deg, #4facfe, #00f2fe)" />; }
|
|
140
|
+
|
|
141
|
+
function TransitionDemo() {
|
|
142
|
+
return (
|
|
143
|
+
<>
|
|
144
|
+
<Sequence from={0} durationInFrames={85}><SceneA /></Sequence>
|
|
145
|
+
<Sequence from={65} durationInFrames={20}>
|
|
146
|
+
<Transition type="fade" durationInFrames={20}><SceneA /><SceneB /></Transition>
|
|
147
|
+
</Sequence>
|
|
148
|
+
<Sequence from={85} durationInFrames={85}><SceneB /></Sequence>
|
|
149
|
+
<Sequence from={150} durationInFrames={20}>
|
|
150
|
+
<Transition type="slide" durationInFrames={20} direction="left"><SceneB /><SceneC /></Transition>
|
|
151
|
+
</Sequence>
|
|
152
|
+
<Sequence from={170} durationInFrames={70}><SceneC /></Sequence>
|
|
153
|
+
</>
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function Root() {
|
|
158
|
+
return (
|
|
159
|
+
<VibeoRoot>
|
|
160
|
+
<Composition id="TransitionDemo" component={TransitionDemo} width={1920} height={1080} fps={30} durationInFrames={240} />
|
|
161
|
+
</VibeoRoot>
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
`;
|
|
165
|
+
|
|
166
|
+
const TEMPLATE_SUBTITLES = `import React from "react";
|
|
167
|
+
import { Composition, Sequence, VibeoRoot, useCurrentFrame, useVideoConfig, interpolate } from "@vibeo/core";
|
|
168
|
+
import { Subtitle } from "@vibeo/extras";
|
|
169
|
+
|
|
170
|
+
const SUBTITLES_SRT = \`1
|
|
171
|
+
00:00:00,500 --> 00:00:03,000
|
|
172
|
+
Welcome to the Vibeo demo.
|
|
173
|
+
|
|
174
|
+
2
|
|
175
|
+
00:00:03,500 --> 00:00:06,000
|
|
176
|
+
This shows subtitle overlays.
|
|
177
|
+
|
|
178
|
+
3
|
|
179
|
+
00:00:06,500 --> 00:00:09,000
|
|
180
|
+
Subtitles are synced to the frame timeline.
|
|
181
|
+
|
|
182
|
+
4
|
|
183
|
+
00:00:09,500 --> 00:00:12,000
|
|
184
|
+
You can use <b>bold</b> and <i>italic</i> text.
|
|
185
|
+
|
|
186
|
+
5
|
|
187
|
+
00:00:13,000 --> 00:00:16,000
|
|
188
|
+
The end. Thanks for watching!\`;
|
|
189
|
+
|
|
190
|
+
function SubtitleVideo() {
|
|
191
|
+
const frame = useCurrentFrame();
|
|
192
|
+
const { width, height } = useVideoConfig();
|
|
193
|
+
const hue = interpolate(frame, [0, 480], [200, 280]);
|
|
194
|
+
|
|
195
|
+
return (
|
|
196
|
+
<div style={{ width, height, position: "relative" }}>
|
|
197
|
+
<div style={{ width, height, background: \`linear-gradient(135deg, hsl(\${hue}, 50%, 15%), hsl(\${hue + 40}, 40%, 10%))\` }} />
|
|
198
|
+
<div style={{ position: "absolute", top: 0, left: 0, width, height }}>
|
|
199
|
+
<Subtitle src={SUBTITLES_SRT} format="srt" position="bottom" fontSize={36} color="white" outlineColor="black" outlineWidth={2} style={{ padding: "0 80px 60px" }} />
|
|
200
|
+
</div>
|
|
201
|
+
</div>
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function Root() {
|
|
206
|
+
return (
|
|
207
|
+
<VibeoRoot>
|
|
208
|
+
<Composition id="SubtitleOverlay" component={SubtitleVideo} width={1920} height={1080} fps={30} durationInFrames={480} />
|
|
209
|
+
</VibeoRoot>
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
`;
|
|
213
|
+
|
|
214
|
+
// ---------------------------------------------------------------------------
|
|
215
|
+
|
|
216
|
+
const TEMPLATES: Record<string, { description: string; source: string }> = {
|
|
6
217
|
basic: {
|
|
7
218
|
description: "Minimal composition with text animation and two scenes",
|
|
8
|
-
|
|
219
|
+
source: TEMPLATE_BASIC,
|
|
9
220
|
},
|
|
10
221
|
"audio-reactive": {
|
|
11
222
|
description: "Audio visualization with frequency bars and amplitude-driven effects",
|
|
12
|
-
|
|
223
|
+
source: TEMPLATE_AUDIO_REACTIVE,
|
|
13
224
|
},
|
|
14
225
|
transitions: {
|
|
15
226
|
description: "Scene transitions (fade, slide) between multiple scenes",
|
|
16
|
-
|
|
227
|
+
source: TEMPLATE_TRANSITIONS,
|
|
17
228
|
},
|
|
18
229
|
subtitles: {
|
|
19
230
|
description: "Video with SRT subtitle overlay",
|
|
20
|
-
|
|
231
|
+
source: TEMPLATE_SUBTITLES,
|
|
21
232
|
},
|
|
22
233
|
};
|
|
23
234
|
|
|
@@ -74,18 +285,6 @@ Examples:
|
|
|
74
285
|
`);
|
|
75
286
|
}
|
|
76
287
|
|
|
77
|
-
// Find the examples directory relative to the CLI package
|
|
78
|
-
function findExamplesDir(): string {
|
|
79
|
-
// Walk up from this file to find the repo root with examples/
|
|
80
|
-
let dir = import.meta.dir;
|
|
81
|
-
for (let i = 0; i < 6; i++) {
|
|
82
|
-
const candidate = join(dir, "examples");
|
|
83
|
-
if (existsSync(candidate)) return candidate;
|
|
84
|
-
dir = resolve(dir, "..");
|
|
85
|
-
}
|
|
86
|
-
throw new Error("Could not find examples directory");
|
|
87
|
-
}
|
|
88
|
-
|
|
89
288
|
export async function createCommand(args: string[]): Promise<void> {
|
|
90
289
|
const parsed = parseArgs(args);
|
|
91
290
|
|
|
@@ -115,10 +314,8 @@ export async function createCommand(args: string[]): Promise<void> {
|
|
|
115
314
|
await mkdir(join(projectDir, "src"), { recursive: true });
|
|
116
315
|
await mkdir(join(projectDir, "public"), { recursive: true });
|
|
117
316
|
|
|
118
|
-
//
|
|
119
|
-
|
|
120
|
-
const exampleSrc = await readFile(join(examplesDir, template.example), "utf-8");
|
|
121
|
-
await writeFile(join(projectDir, "src", "index.tsx"), exampleSrc);
|
|
317
|
+
// Write template source
|
|
318
|
+
await writeFile(join(projectDir, "src", "index.tsx"), template.source);
|
|
122
319
|
|
|
123
320
|
// Write package.json
|
|
124
321
|
const pkg = {
|
|
@@ -127,19 +324,19 @@ export async function createCommand(args: string[]): Promise<void> {
|
|
|
127
324
|
private: true,
|
|
128
325
|
type: "module",
|
|
129
326
|
scripts: {
|
|
130
|
-
dev: "vibeo preview --entry src/index.tsx",
|
|
131
|
-
build: "vibeo render --entry src/index.tsx",
|
|
132
|
-
list: "vibeo list --entry src/index.tsx",
|
|
327
|
+
dev: "bunx @vibeo/cli preview --entry src/index.tsx",
|
|
328
|
+
build: "bunx @vibeo/cli render --entry src/index.tsx",
|
|
329
|
+
list: "bunx @vibeo/cli list --entry src/index.tsx",
|
|
133
330
|
typecheck: "bunx tsc --noEmit",
|
|
134
331
|
},
|
|
135
332
|
dependencies: {
|
|
136
|
-
"@vibeo/core": "
|
|
137
|
-
"@vibeo/audio": "
|
|
138
|
-
"@vibeo/effects": "
|
|
139
|
-
"@vibeo/extras": "
|
|
140
|
-
"@vibeo/player": "
|
|
141
|
-
"@vibeo/renderer": "
|
|
142
|
-
"@vibeo/cli": "
|
|
333
|
+
"@vibeo/core": "^0.1.0",
|
|
334
|
+
"@vibeo/audio": "^0.1.0",
|
|
335
|
+
"@vibeo/effects": "^0.1.0",
|
|
336
|
+
"@vibeo/extras": "^0.1.0",
|
|
337
|
+
"@vibeo/player": "^0.1.0",
|
|
338
|
+
"@vibeo/renderer": "^0.1.0",
|
|
339
|
+
"@vibeo/cli": "^0.1.0",
|
|
143
340
|
react: "^19.0.0",
|
|
144
341
|
"react-dom": "^19.0.0",
|
|
145
342
|
},
|