vivid-editor-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.
@@ -0,0 +1,266 @@
1
+ /**
2
+ * Zod schema for ProjectFile v2 (load/save path).
3
+ *
4
+ * Scope (spec M0 task 9): validate ProjectFile when loading from R2 after
5
+ * migration. The schema is permissive on unknown fields (passthrough) to
6
+ * avoid breaking on forward-compat additions, but strict on the v2 schema
7
+ * markers and type shapes.
8
+ *
9
+ * NOTE: this is NOT the LLM-output validator. That one is
10
+ * `validateGeneratedTimeline` in src/lib/editor/prompt-builder.ts and will
11
+ * be replaced by a separate Zod schema in M6.
12
+ */
13
+ import { z } from 'zod';
14
+ export declare const projectSchemaV2: z.ZodObject<{
15
+ schemaVersion: z.ZodLiteral<2>;
16
+ version: z.ZodOptional<z.ZodNumber>;
17
+ projectName: z.ZodOptional<z.ZodString>;
18
+ videoDurationMs: z.ZodOptional<z.ZodNumber>;
19
+ canvasPreset: z.ZodOptional<z.ZodObject<{
20
+ id: z.ZodString;
21
+ label: z.ZodOptional<z.ZodString>;
22
+ width: z.ZodNumber;
23
+ height: z.ZodNumber;
24
+ }, z.core.$loose>>;
25
+ tracks: z.ZodOptional<z.ZodArray<z.ZodObject<{}, z.core.$loose>>>;
26
+ timelineClips: z.ZodOptional<z.ZodArray<z.ZodObject<{}, z.core.$loose>>>;
27
+ canvasObjects: z.ZodOptional<z.ZodArray<z.ZodObject<{
28
+ id: z.ZodString;
29
+ clipId: z.ZodString;
30
+ x: z.ZodNumber;
31
+ y: z.ZodNumber;
32
+ w: z.ZodNumber;
33
+ h: z.ZodNumber;
34
+ rotation: z.ZodNumber;
35
+ animations: z.ZodOptional<z.ZodRecord<z.ZodEnum<{
36
+ x: "x";
37
+ y: "y";
38
+ scale: "scale";
39
+ rotation: "rotation";
40
+ opacity: "opacity";
41
+ blur: "blur";
42
+ brightness: "brightness";
43
+ contrast: "contrast";
44
+ saturation: "saturation";
45
+ maskProgress: "maskProgress";
46
+ }>, z.ZodObject<{
47
+ keyframes: z.ZodArray<z.ZodObject<{
48
+ t: z.ZodNumber;
49
+ v: z.ZodNumber;
50
+ easing: z.ZodOptional<z.ZodUnion<readonly [z.ZodEnum<{
51
+ linear: "linear";
52
+ easeInQuad: "easeInQuad";
53
+ easeOutQuad: "easeOutQuad";
54
+ easeInOutQuad: "easeInOutQuad";
55
+ easeInCubic: "easeInCubic";
56
+ easeOutCubic: "easeOutCubic";
57
+ easeInOutCubic: "easeInOutCubic";
58
+ easeInBack: "easeInBack";
59
+ easeOutBack: "easeOutBack";
60
+ easeInOutBack: "easeInOutBack";
61
+ easeInQuart: "easeInQuart";
62
+ easeOutQuart: "easeOutQuart";
63
+ easeInOutQuart: "easeInOutQuart";
64
+ easeInExpo: "easeInExpo";
65
+ easeOutExpo: "easeOutExpo";
66
+ easeInOutExpo: "easeInOutExpo";
67
+ }>, z.ZodTuple<[z.ZodNumber, z.ZodNumber, z.ZodNumber, z.ZodNumber], null>, z.ZodObject<{
68
+ type: z.ZodLiteral<"spring">;
69
+ stiffness: z.ZodNumber;
70
+ damping: z.ZodNumber;
71
+ mass: z.ZodNumber;
72
+ }, z.core.$strip>]>>;
73
+ }, z.core.$strip>>;
74
+ }, z.core.$strip>>>;
75
+ effects: z.ZodOptional<z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
76
+ type: z.ZodLiteral<"blur">;
77
+ amount: z.ZodNumber;
78
+ }, z.core.$strip>, z.ZodObject<{
79
+ type: z.ZodLiteral<"vignette">;
80
+ intensity: z.ZodNumber;
81
+ size: z.ZodNumber;
82
+ }, z.core.$strip>, z.ZodObject<{
83
+ type: z.ZodLiteral<"colorGrade">;
84
+ brightness: z.ZodOptional<z.ZodNumber>;
85
+ contrast: z.ZodOptional<z.ZodNumber>;
86
+ saturation: z.ZodOptional<z.ZodNumber>;
87
+ hue: z.ZodOptional<z.ZodNumber>;
88
+ }, z.core.$strip>, z.ZodObject<{
89
+ type: z.ZodLiteral<"grain">;
90
+ intensity: z.ZodNumber;
91
+ size: z.ZodNumber;
92
+ }, z.core.$strip>, z.ZodObject<{
93
+ type: z.ZodLiteral<"glitch">;
94
+ intensity: z.ZodNumber;
95
+ blockSize: z.ZodNumber;
96
+ }, z.core.$strip>, z.ZodObject<{
97
+ type: z.ZodLiteral<"chromaticAberration">;
98
+ offset: z.ZodNumber;
99
+ }, z.core.$strip>, z.ZodObject<{
100
+ type: z.ZodLiteral<"pixelate">;
101
+ blockSize: z.ZodNumber;
102
+ }, z.core.$strip>], "type">>>;
103
+ maskV2: z.ZodOptional<z.ZodDiscriminatedUnion<[z.ZodObject<{
104
+ type: z.ZodLiteral<"none">;
105
+ }, z.core.$strip>, z.ZodObject<{
106
+ type: z.ZodLiteral<"rect">;
107
+ x: z.ZodNumber;
108
+ y: z.ZodNumber;
109
+ w: z.ZodNumber;
110
+ h: z.ZodNumber;
111
+ feather: z.ZodOptional<z.ZodNumber>;
112
+ }, z.core.$strip>, z.ZodObject<{
113
+ type: z.ZodLiteral<"circle">;
114
+ cx: z.ZodNumber;
115
+ cy: z.ZodNumber;
116
+ r: z.ZodNumber;
117
+ feather: z.ZodOptional<z.ZodNumber>;
118
+ }, z.core.$strip>, z.ZodObject<{
119
+ type: z.ZodLiteral<"reveal">;
120
+ direction: z.ZodEnum<{
121
+ ltr: "ltr";
122
+ rtl: "rtl";
123
+ ttb: "ttb";
124
+ btt: "btt";
125
+ }>;
126
+ progress: z.ZodNumber;
127
+ }, z.core.$strip>, z.ZodObject<{
128
+ type: z.ZodLiteral<"clipPath">;
129
+ svgPath: z.ZodString;
130
+ }, z.core.$strip>], "type">>;
131
+ audioReactive: z.ZodOptional<z.ZodArray<z.ZodObject<{
132
+ property: z.ZodEnum<{
133
+ x: "x";
134
+ y: "y";
135
+ scale: "scale";
136
+ rotation: "rotation";
137
+ opacity: "opacity";
138
+ blur: "blur";
139
+ brightness: "brightness";
140
+ contrast: "contrast";
141
+ saturation: "saturation";
142
+ maskProgress: "maskProgress";
143
+ }>;
144
+ feature: z.ZodEnum<{
145
+ rms: "rms";
146
+ low: "low";
147
+ mid: "mid";
148
+ high: "high";
149
+ beat: "beat";
150
+ }>;
151
+ assetId: z.ZodString;
152
+ factor: z.ZodNumber;
153
+ mode: z.ZodEnum<{
154
+ add: "add";
155
+ multiply: "multiply";
156
+ }>;
157
+ smoothing: z.ZodOptional<z.ZodNumber>;
158
+ }, z.core.$strip>>>;
159
+ }, z.core.$loose>>>;
160
+ textOverlays: z.ZodOptional<z.ZodArray<z.ZodObject<{
161
+ id: z.ZodString;
162
+ text: z.ZodString;
163
+ startMs: z.ZodNumber;
164
+ endMs: z.ZodNumber;
165
+ x: z.ZodNumber;
166
+ y: z.ZodNumber;
167
+ animations: z.ZodOptional<z.ZodRecord<z.ZodEnum<{
168
+ x: "x";
169
+ y: "y";
170
+ scale: "scale";
171
+ rotation: "rotation";
172
+ opacity: "opacity";
173
+ blur: "blur";
174
+ brightness: "brightness";
175
+ contrast: "contrast";
176
+ saturation: "saturation";
177
+ maskProgress: "maskProgress";
178
+ }>, z.ZodObject<{
179
+ keyframes: z.ZodArray<z.ZodObject<{
180
+ t: z.ZodNumber;
181
+ v: z.ZodNumber;
182
+ easing: z.ZodOptional<z.ZodUnion<readonly [z.ZodEnum<{
183
+ linear: "linear";
184
+ easeInQuad: "easeInQuad";
185
+ easeOutQuad: "easeOutQuad";
186
+ easeInOutQuad: "easeInOutQuad";
187
+ easeInCubic: "easeInCubic";
188
+ easeOutCubic: "easeOutCubic";
189
+ easeInOutCubic: "easeInOutCubic";
190
+ easeInBack: "easeInBack";
191
+ easeOutBack: "easeOutBack";
192
+ easeInOutBack: "easeInOutBack";
193
+ easeInQuart: "easeInQuart";
194
+ easeOutQuart: "easeOutQuart";
195
+ easeInOutQuart: "easeInOutQuart";
196
+ easeInExpo: "easeInExpo";
197
+ easeOutExpo: "easeOutExpo";
198
+ easeInOutExpo: "easeInOutExpo";
199
+ }>, z.ZodTuple<[z.ZodNumber, z.ZodNumber, z.ZodNumber, z.ZodNumber], null>, z.ZodObject<{
200
+ type: z.ZodLiteral<"spring">;
201
+ stiffness: z.ZodNumber;
202
+ damping: z.ZodNumber;
203
+ mass: z.ZodNumber;
204
+ }, z.core.$strip>]>>;
205
+ }, z.core.$strip>>;
206
+ }, z.core.$strip>>>;
207
+ effects: z.ZodOptional<z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
208
+ type: z.ZodLiteral<"blur">;
209
+ amount: z.ZodNumber;
210
+ }, z.core.$strip>, z.ZodObject<{
211
+ type: z.ZodLiteral<"vignette">;
212
+ intensity: z.ZodNumber;
213
+ size: z.ZodNumber;
214
+ }, z.core.$strip>, z.ZodObject<{
215
+ type: z.ZodLiteral<"colorGrade">;
216
+ brightness: z.ZodOptional<z.ZodNumber>;
217
+ contrast: z.ZodOptional<z.ZodNumber>;
218
+ saturation: z.ZodOptional<z.ZodNumber>;
219
+ hue: z.ZodOptional<z.ZodNumber>;
220
+ }, z.core.$strip>, z.ZodObject<{
221
+ type: z.ZodLiteral<"grain">;
222
+ intensity: z.ZodNumber;
223
+ size: z.ZodNumber;
224
+ }, z.core.$strip>, z.ZodObject<{
225
+ type: z.ZodLiteral<"glitch">;
226
+ intensity: z.ZodNumber;
227
+ blockSize: z.ZodNumber;
228
+ }, z.core.$strip>, z.ZodObject<{
229
+ type: z.ZodLiteral<"chromaticAberration">;
230
+ offset: z.ZodNumber;
231
+ }, z.core.$strip>, z.ZodObject<{
232
+ type: z.ZodLiteral<"pixelate">;
233
+ blockSize: z.ZodNumber;
234
+ }, z.core.$strip>], "type">>>;
235
+ audioReactive: z.ZodOptional<z.ZodArray<z.ZodObject<{
236
+ property: z.ZodEnum<{
237
+ x: "x";
238
+ y: "y";
239
+ scale: "scale";
240
+ rotation: "rotation";
241
+ opacity: "opacity";
242
+ blur: "blur";
243
+ brightness: "brightness";
244
+ contrast: "contrast";
245
+ saturation: "saturation";
246
+ maskProgress: "maskProgress";
247
+ }>;
248
+ feature: z.ZodEnum<{
249
+ rms: "rms";
250
+ low: "low";
251
+ mid: "mid";
252
+ high: "high";
253
+ beat: "beat";
254
+ }>;
255
+ assetId: z.ZodString;
256
+ factor: z.ZodNumber;
257
+ mode: z.ZodEnum<{
258
+ add: "add";
259
+ multiply: "multiply";
260
+ }>;
261
+ smoothing: z.ZodOptional<z.ZodNumber>;
262
+ }, z.core.$strip>>>;
263
+ }, z.core.$loose>>>;
264
+ assets: z.ZodOptional<z.ZodArray<z.ZodObject<{}, z.core.$loose>>>;
265
+ }, z.core.$loose>;
266
+ export type ProjectSchemaV2 = z.infer<typeof projectSchemaV2>;
@@ -0,0 +1,143 @@
1
+ /**
2
+ * Zod schema for ProjectFile v2 (load/save path).
3
+ *
4
+ * Scope (spec M0 task 9): validate ProjectFile when loading from R2 after
5
+ * migration. The schema is permissive on unknown fields (passthrough) to
6
+ * avoid breaking on forward-compat additions, but strict on the v2 schema
7
+ * markers and type shapes.
8
+ *
9
+ * NOTE: this is NOT the LLM-output validator. That one is
10
+ * `validateGeneratedTimeline` in src/lib/editor/prompt-builder.ts and will
11
+ * be replaced by a separate Zod schema in M6.
12
+ */
13
+ import { z } from 'zod';
14
+ // ─── Easing ───────────────────────────────────────────────────
15
+ const EASING_PRESET = z.enum([
16
+ 'linear',
17
+ 'easeInQuad', 'easeOutQuad', 'easeInOutQuad',
18
+ 'easeInCubic', 'easeOutCubic', 'easeInOutCubic',
19
+ 'easeInQuart', 'easeOutQuart', 'easeInOutQuart',
20
+ 'easeInExpo', 'easeOutExpo', 'easeInOutExpo',
21
+ 'easeInBack', 'easeOutBack', 'easeInOutBack',
22
+ ]);
23
+ const BEZIER_EASING = z.tuple([z.number(), z.number(), z.number(), z.number()]);
24
+ const SPRING_CONFIG = z.object({
25
+ type: z.literal('spring'),
26
+ stiffness: z.number().positive(),
27
+ damping: z.number().min(0),
28
+ mass: z.number().positive(),
29
+ });
30
+ const EASING = z.union([EASING_PRESET, BEZIER_EASING, SPRING_CONFIG]);
31
+ // ─── Animation ────────────────────────────────────────────────
32
+ const KEYFRAME = z.object({
33
+ t: z.number(),
34
+ v: z.number(),
35
+ easing: EASING.optional(),
36
+ });
37
+ const ANIMATION = z.object({
38
+ keyframes: z.array(KEYFRAME).min(1),
39
+ });
40
+ const ANIMATABLE_PROP = z.enum([
41
+ 'x', 'y', 'scale', 'rotation', 'opacity',
42
+ 'blur', 'brightness', 'contrast', 'saturation', 'maskProgress',
43
+ ]);
44
+ const ANIMATIONS = z.record(ANIMATABLE_PROP, ANIMATION);
45
+ // ─── Effects ──────────────────────────────────────────────────
46
+ const EFFECT = z.discriminatedUnion('type', [
47
+ z.object({ type: z.literal('blur'), amount: z.number() }),
48
+ z.object({ type: z.literal('vignette'), intensity: z.number(), size: z.number() }),
49
+ z.object({
50
+ type: z.literal('colorGrade'),
51
+ brightness: z.number().optional(),
52
+ contrast: z.number().optional(),
53
+ saturation: z.number().optional(),
54
+ hue: z.number().optional(),
55
+ }),
56
+ z.object({ type: z.literal('grain'), intensity: z.number(), size: z.number() }),
57
+ z.object({ type: z.literal('glitch'), intensity: z.number(), blockSize: z.number() }),
58
+ z.object({ type: z.literal('chromaticAberration'), offset: z.number() }),
59
+ z.object({ type: z.literal('pixelate'), blockSize: z.number() }),
60
+ ]);
61
+ // ─── Mask (v2) ────────────────────────────────────────────────
62
+ const MASK_V2 = z.discriminatedUnion('type', [
63
+ z.object({ type: z.literal('none') }),
64
+ z.object({
65
+ type: z.literal('rect'),
66
+ x: z.number(), y: z.number(), w: z.number(), h: z.number(),
67
+ feather: z.number().optional(),
68
+ }),
69
+ z.object({
70
+ type: z.literal('circle'),
71
+ cx: z.number(), cy: z.number(), r: z.number(),
72
+ feather: z.number().optional(),
73
+ }),
74
+ z.object({
75
+ type: z.literal('reveal'),
76
+ direction: z.enum(['ltr', 'rtl', 'ttb', 'btt']),
77
+ progress: z.number(),
78
+ }),
79
+ z.object({ type: z.literal('clipPath'), svgPath: z.string() }),
80
+ ]);
81
+ // ─── Audio reactivity ─────────────────────────────────────────
82
+ const AUDIO_REACTIVE = z.object({
83
+ property: ANIMATABLE_PROP,
84
+ feature: z.enum(['rms', 'low', 'mid', 'high', 'beat']),
85
+ assetId: z.string(),
86
+ factor: z.number(),
87
+ mode: z.enum(['add', 'multiply']),
88
+ smoothing: z.number().optional(),
89
+ });
90
+ // ─── Visual objects ───────────────────────────────────────────
91
+ /**
92
+ * Permissive CanvasObject schema — V1 objects have many legacy fields
93
+ * (blendMode enum, mask v1 config, constrainProportions, etc.). We validate
94
+ * only the v2-additions and pass the rest via passthrough.
95
+ */
96
+ const CANVAS_OBJECT = z.object({
97
+ id: z.string(),
98
+ clipId: z.string(),
99
+ x: z.number(),
100
+ y: z.number(),
101
+ w: z.number(),
102
+ h: z.number(),
103
+ rotation: z.number(),
104
+ animations: ANIMATIONS.optional(),
105
+ effects: z.array(EFFECT).optional(),
106
+ maskV2: MASK_V2.optional(),
107
+ audioReactive: z.array(AUDIO_REACTIVE).optional(),
108
+ }).passthrough();
109
+ /**
110
+ * Permissive TextOverlay schema — the V1 type has 20+ style fields. We
111
+ * validate only the v2-relevant additions and the handful of base fields
112
+ * the loader/renderer relies on; rest passes through.
113
+ */
114
+ const TEXT_OVERLAY = z.object({
115
+ id: z.string(),
116
+ text: z.string(),
117
+ startMs: z.number(),
118
+ endMs: z.number(),
119
+ x: z.number(),
120
+ y: z.number(),
121
+ animations: ANIMATIONS.optional(),
122
+ effects: z.array(EFFECT).optional(),
123
+ audioReactive: z.array(AUDIO_REACTIVE).optional(),
124
+ }).passthrough();
125
+ // ─── ProjectFile v2 ───────────────────────────────────────────
126
+ export const projectSchemaV2 = z.object({
127
+ schemaVersion: z.literal(2),
128
+ // Legacy field from V1 files — coexists with schemaVersion.
129
+ version: z.number().optional(),
130
+ projectName: z.string().optional(),
131
+ videoDurationMs: z.number().optional(),
132
+ canvasPreset: z.object({
133
+ id: z.string(),
134
+ label: z.string().optional(),
135
+ width: z.number(),
136
+ height: z.number(),
137
+ }).passthrough().optional(),
138
+ tracks: z.array(z.object({}).passthrough()).optional(),
139
+ timelineClips: z.array(z.object({}).passthrough()).optional(),
140
+ canvasObjects: z.array(CANVAS_OBJECT).optional(),
141
+ textOverlays: z.array(TEXT_OVERLAY).optional(),
142
+ assets: z.array(z.object({}).passthrough()).optional(),
143
+ }).passthrough();
@@ -0,0 +1,256 @@
1
+ /**
2
+ * Editor store — the single source of truth for timeline / canvas / text
3
+ * state and every editing operation (add, split, trim, transitions,
4
+ * canvas presets, undo…). Runs anywhere: the browser wraps it in a React
5
+ * hook (vivid-app `src/store/editor.ts`), vivid-mcp drives it headless in
6
+ * Node. Everything that touches the browser (audio engine, asset manager,
7
+ * localStorage, rAF, vision analysis) is injected through `EditorStoreDeps`
8
+ * and defaults to a no-op.
9
+ */
10
+ import { type StoreApi } from 'zustand/vanilla';
11
+ import type { TextOverlay } from './types/editor.js';
12
+ import type { Animations } from './types/animation.js';
13
+ import type { Effect } from './types/effect.js';
14
+ import type { Mask as MaskV2 } from './types/mask.js';
15
+ import type { AudioReactiveModifier } from './types/audioReactive.js';
16
+ import type { BeatAnalysis } from './types/beat.js';
17
+ /** Browser-side hooks. Every member is optional — headless use passes `{}`. */
18
+ export interface EditorStoreDeps {
19
+ /** Playhead moved by a seek (already clamped). */
20
+ onSeek?: (ms: number) => void;
21
+ /** Play / pause toggled. */
22
+ onPlayingChange?: (playing: boolean) => void;
23
+ /** Asset library changes (browser: AssetManager, beat analysis). */
24
+ onAssetAdded?: (clip: AssetClip) => void;
25
+ onAssetRemoved?: (id: string) => void;
26
+ onAssetUpdated?: (id: string, patch: Partial<AssetClip>) => void;
27
+ /** Fire-and-forget vision description for a visual asset. */
28
+ analyzeAsset?: (clip: AssetClip) => Promise<{
29
+ description?: string | null;
30
+ } | undefined>;
31
+ /** Tiny key/value persistence (browser: localStorage). */
32
+ storage?: {
33
+ get: (key: string) => string | null;
34
+ set: (key: string, value: string) => void;
35
+ };
36
+ /** Schedule a callback on the next frame (browser: requestAnimationFrame). */
37
+ nextFrame?: (cb: () => void) => void;
38
+ }
39
+ export type MediaType = 'video' | 'audio' | 'image' | 'color';
40
+ export type TrackType = 'visual' | 'audio';
41
+ export interface TrackDefinition {
42
+ id: string;
43
+ label: string;
44
+ type: TrackType;
45
+ color: string;
46
+ }
47
+ /** @deprecated Use store `tracks` state instead. Kept for backward compatibility. */
48
+ export declare const TIMELINE_TRACKS: TrackDefinition[];
49
+ export interface AssetClip {
50
+ id: string;
51
+ name: string;
52
+ url: string;
53
+ durationMs: number;
54
+ file: File | null;
55
+ mediaType: MediaType;
56
+ /** Intrinsic pixel width of the media (video.videoWidth / img.naturalWidth) */
57
+ intrinsicWidth?: number;
58
+ /** Intrinsic pixel height of the media (video.videoHeight / img.naturalHeight) */
59
+ intrinsicHeight?: number;
60
+ /** Server-side asset ID (set after uploading to R2 or when imported from library) */
61
+ serverAssetId?: string;
62
+ /** Origin of this asset — used to filter gallery and prevent duplicate uploads */
63
+ source?: 'upload' | 'library' | 'pexels' | 'stock';
64
+ /** Beats per minute — set for audio tracks to enable beat-synced editing */
65
+ bpm?: number;
66
+ /** AI-generated description of visual content (from vision analysis) */
67
+ aiDescription?: string;
68
+ }
69
+ export type TransitionType = 'dissolve' | 'fade-black';
70
+ export type AudioTransitionType = 'crossfade' | 'fade-in' | 'fade-out';
71
+ export interface TransitionConfig {
72
+ type: TransitionType | AudioTransitionType;
73
+ durationMs: number;
74
+ }
75
+ /** Clip-level animation types for entrance/exit */
76
+ export type ClipAnimationType = 'none' | 'fade' | 'slide-up' | 'slide-down' | 'zoom-in' | 'zoom-out' | 'zoom-pan' | 'spin' | 'blur-reveal';
77
+ export interface ClipAnimationConfig {
78
+ type: ClipAnimationType;
79
+ durationMs: number;
80
+ }
81
+ export interface TimelineClip {
82
+ id: string;
83
+ assetId: string;
84
+ name: string;
85
+ url: string;
86
+ startMs: number;
87
+ durationMs: number;
88
+ originalDurationMs: number;
89
+ sourceOffsetMs: number;
90
+ mediaType: MediaType;
91
+ trackId: string;
92
+ /** When true, audio is detached from this video clip (video plays muted, separate audio clip exists) */
93
+ isAudioDetached?: boolean;
94
+ /** Transition applied at the start of this clip */
95
+ transitionIn?: TransitionConfig;
96
+ /** Transition applied at the end of this clip */
97
+ transitionOut?: TransitionConfig;
98
+ /** Entrance animation */
99
+ animationIn?: ClipAnimationConfig;
100
+ /** Exit animation */
101
+ animationOut?: ClipAnimationConfig;
102
+ /** Fill color for 'color' type clips */
103
+ color?: string;
104
+ /** Clip volume 0-1 (default 1) */
105
+ volume?: number;
106
+ /** Clip opacity 0-1 (default 1, fully opaque) */
107
+ opacity?: number;
108
+ /** Playback speed multiplier (default 1.0). Changes visual duration on timeline. */
109
+ playbackRate?: number;
110
+ }
111
+ export type MaskType = 'none' | 'rounded' | 'circle' | 'star' | 'hexagon';
112
+ export type MaskAspectPreset = 'free' | '1:1' | '4:3' | '3:4' | '16:9' | '9:16';
113
+ export interface MaskConfig {
114
+ type: MaskType;
115
+ borderRadius?: number;
116
+ /** Relative mask crop region (0-1 fractions of the media element). Set via MaskEditorModal. */
117
+ cropX?: number;
118
+ cropY?: number;
119
+ cropW?: number;
120
+ cropH?: number;
121
+ /** Aspect lock used by the MaskEditorModal resize handles. 'free' (default)
122
+ * resizes width/height independently; a ratio preset keeps the crop locked
123
+ * to it. Circle masks ignore this — they always render as a true circle. */
124
+ aspectPreset?: MaskAspectPreset;
125
+ }
126
+ export type BlendMode = 'normal' | 'multiply' | 'screen' | 'overlay' | 'darken' | 'lighten' | 'color-dodge' | 'color-burn' | 'hard-light' | 'soft-light' | 'difference' | 'exclusion';
127
+ export interface CanvasObject {
128
+ id: string;
129
+ clipId: string;
130
+ x: number;
131
+ y: number;
132
+ w: number;
133
+ h: number;
134
+ rotation: number;
135
+ mask?: MaskConfig;
136
+ blendMode?: BlendMode;
137
+ constrainProportions?: boolean;
138
+ animations?: Animations;
139
+ effects?: Effect[];
140
+ maskV2?: MaskV2;
141
+ audioReactive?: AudioReactiveModifier[];
142
+ }
143
+ export interface CanvasPreset {
144
+ id: string;
145
+ label: string;
146
+ width: number;
147
+ height: number;
148
+ }
149
+ export interface SnapGuide {
150
+ axis: 'x' | 'y';
151
+ position: number;
152
+ }
153
+ export declare const CANVAS_PRESETS: CanvasPreset[];
154
+ export type CinematicLook = 'none' | 'kodak-portra' | 'noir' | 'cyberpunk' | 'kodak-portra-400' | 'fuji-provia' | 'ilford-hp5' | 'cinestill-800t' | 'bleach-bypass' | 'polaroid-vintage' | 'technicolor' | 'cross-process' | 'muted-cinematic' | 'cyberpunk-neon';
155
+ export declare const CINEMATIC_LOOKS: {
156
+ value: CinematicLook;
157
+ label: string;
158
+ }[];
159
+ /** CSS filter string for live DOM preview */
160
+ export declare function getCinematicFilter(look: CinematicLook): string;
161
+ export declare const MAX_TIMELINE_MS = 300000;
162
+ export declare const MAX_CROSSFADE_MS = 3000;
163
+ export declare const ZOOM_MIN_PX_PER_SEC = 10;
164
+ export declare const ZOOM_MAX_PX_PER_SEC = 3000;
165
+ export declare const ZOOM_DEFAULT_PX_PER_SEC = 100;
166
+ export declare function maxPxPerSecForDuration(durationMs: number): number;
167
+ export interface UndoableState {
168
+ timelineClips: TimelineClip[];
169
+ canvasObjects: CanvasObject[];
170
+ tracks: TrackDefinition[];
171
+ textOverlays: TextOverlay[];
172
+ }
173
+ export interface EditorState {
174
+ playheadMs: number;
175
+ isPlaying: boolean;
176
+ loopPlayback: boolean;
177
+ assetLibrary: AssetClip[];
178
+ selectedAssetIds: Set<string>;
179
+ timelineClips: TimelineClip[];
180
+ selectedClipId: string | null;
181
+ tracks: TrackDefinition[];
182
+ nextVisualNum: number;
183
+ nextAudioNum: number;
184
+ canvasPreset: CanvasPreset;
185
+ canvasObjects: CanvasObject[];
186
+ selectedObjectId: string | null;
187
+ snapGuides: SnapGuide[];
188
+ textOverlays: TextOverlay[];
189
+ selectedTextId: string | null;
190
+ cinematicLook: CinematicLook;
191
+ projectName: string;
192
+ pendingReframe: CanvasPreset | null;
193
+ maskEditorClipId: string | null;
194
+ pxPerSec: number;
195
+ audioBeatsByAssetId: Record<string, BeatAnalysis | 'analyzing' | 'failed'>;
196
+ undoStack: UndoableState[];
197
+ redoStack: UndoableState[];
198
+ setProjectName: (name: string) => void;
199
+ setPlayheadMs: (ms: number) => void;
200
+ setIsPlaying: (playing: boolean) => void;
201
+ toggleLoopPlayback: () => void;
202
+ addAsset: (clip: AssetClip) => void;
203
+ removeAsset: (id: string) => void;
204
+ updateAsset: (id: string, patch: Partial<AssetClip>) => void;
205
+ toggleAssetSelected: (id: string) => void;
206
+ clearAssetSelection: () => void;
207
+ addClipToTimeline: (asset: AssetClip) => void;
208
+ updateClipBounds: (id: string, updates: Partial<Pick<TimelineClip, 'startMs' | 'durationMs' | 'sourceOffsetMs'>>) => void;
209
+ moveClip: (id: string, newStartMs: number, newTrackId?: string, allowOverlap?: boolean) => void;
210
+ trimClipStart: (id: string, deltaMs: number) => void;
211
+ trimClipEnd: (id: string, deltaMs: number) => void;
212
+ splitClip: (splitAtMs: number) => void;
213
+ duplicateClip: (id: string) => void;
214
+ deleteSelectedClip: () => void;
215
+ deleteClipById: (id: string) => void;
216
+ setSelectedClipId: (id: string | null) => void;
217
+ addColorClip: (color: string, durationMs: number) => void;
218
+ setClipTransition: (clipId: string, edge: 'in' | 'out', config: TransitionConfig | undefined) => void;
219
+ setClipVolume: (clipId: string, volume: number) => void;
220
+ detachAudio: (clipId: string) => void;
221
+ setClipOpacity: (clipId: string, opacity: number) => void;
222
+ setClipSpeed: (clipId: string, rate: number) => void;
223
+ setClipAnimation: (clipId: string, phase: 'in' | 'out', config: ClipAnimationConfig | undefined) => void;
224
+ addTrack: (type: TrackType) => TrackDefinition;
225
+ removeEmptyTracks: () => void;
226
+ ensureAudioTrack: (visualTrackId: string) => void;
227
+ addTextOverlay: (overlay: TextOverlay) => void;
228
+ updateTextOverlay: (id: string, patch: Partial<TextOverlay>) => void;
229
+ removeTextOverlay: (id: string) => void;
230
+ setSelectedTextId: (id: string | null) => void;
231
+ setCanvasPreset: (preset: CanvasPreset) => void;
232
+ updateCanvasObject: (id: string, patch: Partial<CanvasObject>) => void;
233
+ setSelectedObjectId: (id: string | null) => void;
234
+ setSnapGuides: (guides: SnapGuide[]) => void;
235
+ setCinematicLook: (look: CinematicLook) => void;
236
+ dismissReframe: () => void;
237
+ openMaskEditor: (clipId: string) => void;
238
+ closeMaskEditor: () => void;
239
+ setPxPerSec: (pxPerSec: number) => void;
240
+ zoomIn: () => void;
241
+ zoomOut: () => void;
242
+ undo: () => void;
243
+ redo: () => void;
244
+ pushUndo: () => void;
245
+ setAudioBeatsStatus: (assetId: string, value: BeatAnalysis | 'analyzing' | 'failed') => void;
246
+ replaceTimelineClips: (clips: TimelineClip[], canvasObjects?: CanvasObject[]) => void;
247
+ hydrateTimeline: (data: {
248
+ tracks: TrackDefinition[];
249
+ timelineClips: TimelineClip[];
250
+ canvasObjects: CanvasObject[];
251
+ textOverlays: import('./types/editor.js').TextOverlay[];
252
+ }) => void;
253
+ reset: () => void;
254
+ }
255
+ export type EditorStore = StoreApi<EditorState>;
256
+ export declare function createEditorStore(deps?: EditorStoreDeps): EditorStore;