@vibeo/cli 0.3.3 → 0.3.5

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.
@@ -0,0 +1,432 @@
1
+ # Vibeo Effects (`@vibeo/effects`)
2
+
3
+ ## Overview
4
+
5
+ `@vibeo/effects` provides advanced animation primitives that go beyond basic interpolation: declarative keyframes, spring physics, scene transitions, and audio-reactive animation hooks.
6
+
7
+ **When to use**: When you need keyframe animation, physics-based springs, transitions between scenes, or audio-driven visuals.
8
+
9
+ ---
10
+
11
+ ## API Reference
12
+
13
+ ### `useKeyframes(frame, keyframes, options?): number`
14
+
15
+ Declarative keyframe animation. Automatically interpolates between keyframe stops.
16
+
17
+ ```ts
18
+ import { useKeyframes } from "@vibeo/effects";
19
+ import { useCurrentFrame, easeInOut } from "@vibeo/core";
20
+
21
+ const frame = useCurrentFrame();
22
+ const y = useKeyframes(frame, { 0: 0, 30: 100, 60: 0 }, { easing: easeInOut });
23
+ ```
24
+
25
+ **Parameters**:
26
+ | Param | Type | Description |
27
+ |-------|------|-------------|
28
+ | `frame` | `number` | Current frame (pass `useCurrentFrame()`) |
29
+ | `keyframes` | `KeyframeMap` | `Record<number, KeyframeStop>` — frame-to-value mapping |
30
+ | `options?` | `KeyframeOptions` | Default easing function |
31
+
32
+ **`KeyframeStop`**: `number | { value: number; easing?: (t: number) => number }`
33
+
34
+ Per-segment easing uses the **starting** stop's easing function for that segment.
35
+
36
+ ```ts
37
+ const scale = useKeyframes(frame, {
38
+ 0: { value: 0, easing: easeIn },
39
+ 20: { value: 1.2, easing: easeOut },
40
+ 30: 1,
41
+ });
42
+ ```
43
+
44
+ ### `useSpring(options): number`
45
+
46
+ Physics-based spring animation. Simulates mass-spring-damper system per frame.
47
+
48
+ ```ts
49
+ import { useSpring } from "@vibeo/effects";
50
+
51
+ const x = useSpring({ from: 0, to: 100 });
52
+ const bouncy = useSpring({ from: 0, to: 1, config: { mass: 1, stiffness: 300, damping: 10 } });
53
+ ```
54
+
55
+ **`SpringOptions`**:
56
+ | Field | Type | Default | Description |
57
+ |-------|------|---------|-------------|
58
+ | `from` | `number` | — | Start value |
59
+ | `to` | `number` | — | Target value |
60
+ | `frame?` | `number` | `useCurrentFrame()` | Frame override |
61
+ | `fps?` | `number` | `useVideoConfig().fps` | FPS override |
62
+ | `config?` | `SpringConfig` | see below | Physics parameters |
63
+
64
+ **`SpringConfig`** (defaults):
65
+ | Field | Default | Description |
66
+ |-------|---------|-------------|
67
+ | `mass` | `1` | Mass of the object |
68
+ | `stiffness` | `170` | Spring stiffness |
69
+ | `damping` | `26` | Damping coefficient |
70
+
71
+ ### `springDuration(options): number`
72
+
73
+ Compute how many frames a spring takes to settle.
74
+
75
+ ```ts
76
+ const dur = springDuration({ fps: 30, config: { stiffness: 300, damping: 10 } });
77
+ ```
78
+
79
+ ### `Transition`
80
+
81
+ Component for transitioning between exactly two children.
82
+
83
+ ```tsx
84
+ import { Transition } from "@vibeo/effects";
85
+ import { Sequence } from "@vibeo/core";
86
+
87
+ <Sequence from={55} durationInFrames={20}>
88
+ <Transition type="fade" durationInFrames={20}>
89
+ <SceneA />
90
+ <SceneB />
91
+ </Transition>
92
+ </Sequence>
93
+ ```
94
+
95
+ **`TransitionProps`**:
96
+ | Prop | Type | Default | Description |
97
+ |------|------|---------|-------------|
98
+ | `type` | `TransitionType` | — | `"fade" \| "wipe" \| "slide" \| "dissolve"` |
99
+ | `durationInFrames` | `number` | — | Frames the transition lasts |
100
+ | `timing?` | `TransitionTiming` | `"in-and-out"` | `"in-and-out" \| "in" \| "out"` |
101
+ | `direction?` | `TransitionDirection` | `"left"` | `"left" \| "right" \| "up" \| "down"` (for wipe/slide) |
102
+ | `children` | `[ReactNode, ReactNode]` | — | Exactly 2 children |
103
+
104
+ ### Transition Strategies
105
+
106
+ ```ts
107
+ import { fade, wipe, slide, dissolve } from "@vibeo/effects";
108
+ ```
109
+
110
+ Each returns `{ childA: CSSProperties; childB: CSSProperties }` for a given `TransitionState`.
111
+
112
+ | Strategy | Effect |
113
+ |----------|--------|
114
+ | `fade` | Opacity crossfade |
115
+ | `wipe` | Clip-path reveal |
116
+ | `slide` | Transform slide in/out |
117
+ | `dissolve` | Mix-blend + opacity |
118
+
119
+ ### `useAudioData(audioSrc, options?): AudioAnalysis | null`
120
+
121
+ Pre-analyzes an audio file and returns per-frame frequency/amplitude data. Returns `null` while loading.
122
+
123
+ ```ts
124
+ import { useAudioData } from "@vibeo/effects";
125
+
126
+ const data = useAudioData("/music.mp3", { fftSize: 2048 });
127
+ if (data) {
128
+ // data.amplitude — overall RMS amplitude (0-1)
129
+ // data.bass — average energy in 20-250 Hz
130
+ // data.mid — average energy in 250-4000 Hz
131
+ // data.treble — average energy in 4000-20000 Hz
132
+ // data.frequencies — Float32Array of FFT magnitude data (dB)
133
+ }
134
+ ```
135
+
136
+ **`AudioDataOptions`**:
137
+ | Field | Type | Default | Description |
138
+ |-------|------|---------|-------------|
139
+ | `fftSize?` | `number` | `2048` | FFT window size (power of 2) |
140
+
141
+ **`AudioAnalysis`**:
142
+ | Field | Type | Description |
143
+ |-------|------|-------------|
144
+ | `amplitude` | `number` | Overall RMS amplitude (0-1 range) |
145
+ | `frequencies` | `Float32Array` | Full FFT magnitude data in dB |
146
+ | `bass` | `number` | Average energy 20-250 Hz |
147
+ | `mid` | `number` | Average energy 250-4000 Hz |
148
+ | `treble` | `number` | Average energy 4000-20000 Hz |
149
+
150
+ ### `useTransitionProgress(durationInFrames): number | null`
151
+
152
+ Returns the transition progress (0 to 1) within a transition window, or `null` outside.
153
+
154
+ ```ts
155
+ const progress = useTransitionProgress(20);
156
+ if (progress !== null) {
157
+ // 0 at start of transition, 1 at end
158
+ }
159
+ ```
160
+
161
+ ### Types
162
+
163
+ ```ts
164
+ import type {
165
+ KeyframeStop,
166
+ KeyframeMap,
167
+ KeyframeOptions,
168
+ SpringConfig,
169
+ SpringOptions,
170
+ TransitionTiming,
171
+ TransitionDirection,
172
+ TransitionType,
173
+ TransitionProps,
174
+ TransitionState,
175
+ AudioAnalysis,
176
+ AudioDataOptions,
177
+ } from "@vibeo/effects";
178
+ ```
179
+
180
+ ---
181
+
182
+ ## Keyframe Animation Recipes
183
+
184
+ ### Fade in
185
+
186
+ ```ts
187
+ const opacity = useKeyframes(frame, { 0: 0, 30: 1 });
188
+ ```
189
+
190
+ ### Bounce
191
+
192
+ ```ts
193
+ const y = useKeyframes(frame, {
194
+ 0: 0,
195
+ 15: { value: -100, easing: easeOut },
196
+ 30: { value: 0, easing: easeIn },
197
+ 45: { value: -40, easing: easeOut },
198
+ 60: 0,
199
+ });
200
+ ```
201
+
202
+ ### Slide in from left
203
+
204
+ ```ts
205
+ const x = useKeyframes(frame, {
206
+ 0: -1920,
207
+ 30: { value: 0, easing: easeOut },
208
+ });
209
+ ```
210
+
211
+ ### Scale pulse
212
+
213
+ ```ts
214
+ const scale = useKeyframes(frame, { 0: 1, 15: 1.2, 30: 1 }, { easing: easeInOut });
215
+ ```
216
+
217
+ ---
218
+
219
+ ## Spring Animation Guide
220
+
221
+ ### Snappy UI motion
222
+ ```ts
223
+ useSpring({ from: 0, to: 1, config: { stiffness: 300, damping: 30 } })
224
+ ```
225
+
226
+ ### Bouncy entrance
227
+ ```ts
228
+ useSpring({ from: 0, to: 1, config: { stiffness: 400, damping: 10, mass: 1 } })
229
+ ```
230
+
231
+ ### Slow, heavy drag
232
+ ```ts
233
+ useSpring({ from: 0, to: 1, config: { stiffness: 50, damping: 20, mass: 3 } })
234
+ ```
235
+
236
+ ### Gentle ease
237
+ ```ts
238
+ useSpring({ from: 0, to: 1, config: { stiffness: 100, damping: 26 } })
239
+ ```
240
+
241
+ **Tuning guide**:
242
+ - Higher `stiffness` = faster motion, more overshoot
243
+ - Higher `damping` = less bounce, settles faster
244
+ - Higher `mass` = slower, more momentum
245
+
246
+ ---
247
+
248
+ ## Transition Usage Between Scenes
249
+
250
+ ```tsx
251
+ function MyVideo() {
252
+ return (
253
+ <>
254
+ {/* Scene A plays frames 0-59 */}
255
+ <Sequence from={0} durationInFrames={75}>
256
+ <SceneA />
257
+ </Sequence>
258
+
259
+ {/* 15-frame fade transition overlapping scenes */}
260
+ <Sequence from={55} durationInFrames={20}>
261
+ <Transition type="fade" durationInFrames={20}>
262
+ <SceneA />
263
+ <SceneB />
264
+ </Transition>
265
+ </Sequence>
266
+
267
+ {/* Scene B continues */}
268
+ <Sequence from={75} durationInFrames={90}>
269
+ <SceneB />
270
+ </Sequence>
271
+ </>
272
+ );
273
+ }
274
+ ```
275
+
276
+ For directional transitions:
277
+ ```tsx
278
+ <Transition type="slide" durationInFrames={20} direction="right">
279
+ <OldScene />
280
+ <NewScene />
281
+ </Transition>
282
+ ```
283
+
284
+ ---
285
+
286
+ ## Audio-Reactive Animation Patterns
287
+
288
+ ### Beat-reactive scale
289
+
290
+ ```tsx
291
+ function BeatCircle() {
292
+ const frame = useCurrentFrame();
293
+ const audio = useAudioData("/track.mp3");
294
+ const baseScale = audio ? 1 + audio.bass * 2 : 1;
295
+
296
+ return (
297
+ <div style={{
298
+ width: 100,
299
+ height: 100,
300
+ borderRadius: "50%",
301
+ background: "white",
302
+ transform: `scale(${baseScale})`,
303
+ }} />
304
+ );
305
+ }
306
+ ```
307
+
308
+ ### Audio visualizer bars
309
+
310
+ ```tsx
311
+ function Visualizer() {
312
+ const audio = useAudioData("/music.mp3", { fftSize: 512 });
313
+ if (!audio) return null;
314
+
315
+ const barCount = 32;
316
+ const step = Math.floor(audio.frequencies.length / barCount);
317
+
318
+ return (
319
+ <div style={{ display: "flex", alignItems: "flex-end", height: 200 }}>
320
+ {Array.from({ length: barCount }, (_, i) => {
321
+ const db = audio.frequencies[i * step];
322
+ const height = Math.max(2, ((db + 100) / 100) * 200);
323
+ return <div key={i} style={{ width: 8, height, margin: 1, background: "cyan" }} />;
324
+ })}
325
+ </div>
326
+ );
327
+ }
328
+ ```
329
+
330
+ ### Background color shift with amplitude
331
+
332
+ ```tsx
333
+ function ReactiveBackground({ children }: { children: React.ReactNode }) {
334
+ const audio = useAudioData("/music.mp3");
335
+ const hue = audio ? Math.round(audio.amplitude * 360) : 200;
336
+ const lightness = audio ? 20 + audio.amplitude * 30 : 20;
337
+
338
+ return (
339
+ <div style={{
340
+ width: "100%",
341
+ height: "100%",
342
+ background: `hsl(${hue}, 70%, ${lightness}%)`,
343
+ }}>
344
+ {children}
345
+ </div>
346
+ );
347
+ }
348
+ ```
349
+
350
+ ---
351
+
352
+ ## Gotchas and Tips
353
+
354
+ 1. **`useAudioData` returns `null` initially** — always handle the loading state.
355
+
356
+ 2. **`useKeyframes` clamps at boundaries** — it won't extrapolate beyond the first/last keyframe.
357
+
358
+ 3. **`<Transition>` must have exactly 2 children** — it throws if you pass more or fewer.
359
+
360
+ 4. **Spring simulations are cached** at the module level for identical parameters, so repeated renders are cheap.
361
+
362
+ 5. **`useKeyframes` requires you to pass `frame` explicitly** — it doesn't call `useCurrentFrame()` internally. This gives you flexibility to use any frame value.
363
+
364
+ 6. **`springDuration()` is useful for sizing `<Sequence>` wrappers** to match how long a spring animation takes to settle.
365
+
366
+
367
+ ---
368
+
369
+ ## LLM & Agent Integration
370
+
371
+ Vibeo's CLI is built with [incur](https://github.com/wevm/incur), making it natively discoverable by AI agents and LLMs.
372
+
373
+ ### Discovering the API
374
+
375
+ ```bash
376
+ # Get a compact summary of all CLI commands (ideal for LLM system prompts)
377
+ bunx @vibeo/cli --llms
378
+
379
+ # Get the full manifest with schemas, examples, and argument details
380
+ bunx @vibeo/cli --llms-full
381
+
382
+ # Get JSON Schema for a specific command (useful for structured tool calls)
383
+ bunx @vibeo/cli render --schema
384
+ bunx @vibeo/cli create --schema
385
+ ```
386
+
387
+ ### Using as an MCP Server
388
+
389
+ ```bash
390
+ # Start Vibeo as an MCP (Model Context Protocol) server
391
+ bunx @vibeo/cli --mcp
392
+
393
+ # Register as a persistent MCP server for your agent
394
+ bunx @vibeo/cli mcp add
395
+ ```
396
+
397
+ This lets LLMs call `create`, `render`, `preview`, and `list` as structured tool calls through the MCP protocol.
398
+
399
+ ### Generating Skill Files
400
+
401
+ ```bash
402
+ # Sync skill files to your agent's skill directory
403
+ bunx @vibeo/cli skills add
404
+ ```
405
+
406
+ This generates markdown skill files that agents like Claude Code can discover and use to write Vibeo code without reading source.
407
+
408
+ ### Agent-Friendly Output
409
+
410
+ ```bash
411
+ # Output as JSON for programmatic consumption
412
+ bunx @vibeo/cli list --entry src/index.tsx --format json
413
+
414
+ # Output as YAML
415
+ bunx @vibeo/cli list --entry src/index.tsx --format yaml
416
+
417
+ # Filter output to specific keys
418
+ bunx @vibeo/cli list --entry src/index.tsx --filter-output compositions[0].id
419
+
420
+ # Count tokens in output (useful for context window planning)
421
+ bunx @vibeo/cli render --schema --token-count
422
+ ```
423
+
424
+ ### How LLMs Should Use Vibeo
425
+
426
+ 1. **Discover commands**: Run `bunx @vibeo/cli --llms` to get the command manifest
427
+ 2. **Create a project**: `bunx @vibeo/cli create my-video --template basic`
428
+ 3. **Edit `src/index.tsx`**: Write React components using `@vibeo/core` hooks and components
429
+ 4. **Preview**: `bunx @vibeo/cli preview --entry src/index.tsx`
430
+ 5. **Render**: `bunx @vibeo/cli render --entry src/index.tsx --composition MyComp`
431
+
432
+ All commands accept `--format json` for structured output that LLMs can parse reliably.