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,11 @@
1
+ import type { TextOverlay } from './types/editor.js';
2
+ export type TextPresetId = 'title' | 'subtitle' | 'cta' | 'price' | 'lower-third' | 'minimal';
3
+ export interface TextPreset {
4
+ id: TextPresetId;
5
+ name: string;
6
+ icon: string;
7
+ defaults: Partial<TextOverlay>;
8
+ }
9
+ export declare const TEXT_PRESETS: TextPreset[];
10
+ /** Default values for a new text overlay */
11
+ export declare function createTextOverlay(preset?: Partial<TextOverlay>): TextOverlay;
@@ -0,0 +1,203 @@
1
+ export const TEXT_PRESETS = [
2
+ {
3
+ id: 'title',
4
+ name: 'Title',
5
+ icon: 'T',
6
+ defaults: {
7
+ text: 'Your Title',
8
+ fontFamily: "'Archivo Black', sans-serif",
9
+ fontSize: 56,
10
+ fontWeight: 400,
11
+ color: '#FFFFFF',
12
+ backgroundColor: 'transparent',
13
+ backgroundOpacity: 0,
14
+ strokeColor: '#000000',
15
+ strokeWidth: 2,
16
+ shadowColor: '#000000',
17
+ shadowBlur: 8,
18
+ shadowOffsetX: 0,
19
+ shadowOffsetY: 2,
20
+ textTransform: 'uppercase',
21
+ textAlign: 'center',
22
+ animation: 'pop',
23
+ animationDurationMs: 400,
24
+ padding: 8,
25
+ borderRadius: 0,
26
+ opacity: 1,
27
+ },
28
+ },
29
+ {
30
+ id: 'subtitle',
31
+ name: 'Subtitle',
32
+ icon: 'Aa',
33
+ defaults: {
34
+ text: 'Subtitle text',
35
+ fontFamily: "'Inter', sans-serif",
36
+ fontSize: 32,
37
+ fontWeight: 500,
38
+ color: '#FFFFFF',
39
+ backgroundColor: '#000000',
40
+ backgroundOpacity: 0.5,
41
+ strokeColor: 'transparent',
42
+ strokeWidth: 0,
43
+ shadowColor: 'transparent',
44
+ shadowBlur: 0,
45
+ shadowOffsetX: 0,
46
+ shadowOffsetY: 0,
47
+ textTransform: 'none',
48
+ textAlign: 'center',
49
+ animation: 'fade-in',
50
+ animationDurationMs: 300,
51
+ padding: 8,
52
+ borderRadius: 4,
53
+ opacity: 1,
54
+ },
55
+ },
56
+ {
57
+ id: 'cta',
58
+ name: 'CTA',
59
+ icon: '→',
60
+ defaults: {
61
+ text: 'SHOP NOW',
62
+ fontFamily: "'Bebas Neue', sans-serif",
63
+ fontSize: 40,
64
+ fontWeight: 400,
65
+ color: '#000000',
66
+ backgroundColor: '#FFD600',
67
+ backgroundOpacity: 1,
68
+ strokeColor: 'transparent',
69
+ strokeWidth: 0,
70
+ shadowColor: '#000000',
71
+ shadowBlur: 6,
72
+ shadowOffsetX: 0,
73
+ shadowOffsetY: 3,
74
+ textTransform: 'uppercase',
75
+ textAlign: 'center',
76
+ animation: 'slide-up',
77
+ animationDurationMs: 350,
78
+ padding: 12,
79
+ borderRadius: 8,
80
+ opacity: 1,
81
+ },
82
+ },
83
+ {
84
+ id: 'price',
85
+ name: 'Price',
86
+ icon: '€',
87
+ defaults: {
88
+ text: '€29.99',
89
+ fontFamily: "'JetBrains Mono', monospace",
90
+ fontSize: 48,
91
+ fontWeight: 700,
92
+ color: '#FFFFFF',
93
+ backgroundColor: '#EF4444',
94
+ backgroundOpacity: 1,
95
+ strokeColor: 'transparent',
96
+ strokeWidth: 0,
97
+ shadowColor: '#000000',
98
+ shadowBlur: 4,
99
+ shadowOffsetX: 0,
100
+ shadowOffsetY: 2,
101
+ textTransform: 'none',
102
+ textAlign: 'center',
103
+ animation: 'pop',
104
+ animationDurationMs: 350,
105
+ padding: 10,
106
+ borderRadius: 6,
107
+ opacity: 1,
108
+ },
109
+ },
110
+ {
111
+ id: 'lower-third',
112
+ name: 'Lower Third',
113
+ icon: '▁',
114
+ defaults: {
115
+ text: 'Name | Title',
116
+ fontFamily: "'Space Grotesk', sans-serif",
117
+ fontSize: 28,
118
+ fontWeight: 600,
119
+ color: '#FFFFFF',
120
+ backgroundColor: '#000000',
121
+ backgroundOpacity: 0.7,
122
+ strokeColor: 'transparent',
123
+ strokeWidth: 0,
124
+ shadowColor: 'transparent',
125
+ shadowBlur: 0,
126
+ shadowOffsetX: 0,
127
+ shadowOffsetY: 0,
128
+ textTransform: 'none',
129
+ textAlign: 'left',
130
+ animation: 'slide-left',
131
+ animationDurationMs: 400,
132
+ padding: 10,
133
+ borderRadius: 4,
134
+ opacity: 1,
135
+ x: 0.15,
136
+ y: 0.85,
137
+ },
138
+ },
139
+ {
140
+ id: 'minimal',
141
+ name: 'Minimal',
142
+ icon: '·',
143
+ defaults: {
144
+ text: 'Your text here',
145
+ fontFamily: "'Newsreader', serif",
146
+ fontSize: 36,
147
+ fontWeight: 400,
148
+ color: '#FFFFFF',
149
+ backgroundColor: 'transparent',
150
+ backgroundOpacity: 0,
151
+ strokeColor: 'transparent',
152
+ strokeWidth: 0,
153
+ shadowColor: '#000000',
154
+ shadowBlur: 12,
155
+ shadowOffsetX: 0,
156
+ shadowOffsetY: 0,
157
+ textTransform: 'none',
158
+ textAlign: 'center',
159
+ animation: 'fade-in-out',
160
+ animationDurationMs: 500,
161
+ padding: 8,
162
+ borderRadius: 0,
163
+ opacity: 1,
164
+ },
165
+ },
166
+ ];
167
+ /** Default values for a new text overlay */
168
+ export function createTextOverlay(preset) {
169
+ return {
170
+ text: 'Text',
171
+ startMs: 0,
172
+ endMs: 5000,
173
+ x: 0.5,
174
+ y: 0.5,
175
+ scale: 1,
176
+ rotation: 0,
177
+ fontFamily: "'Inter', sans-serif",
178
+ fontSize: 36,
179
+ fontWeight: 600,
180
+ color: '#FFFFFF',
181
+ backgroundColor: 'transparent',
182
+ backgroundOpacity: 0,
183
+ strokeColor: '#000000',
184
+ strokeWidth: 1,
185
+ strokeOpacity: 100,
186
+ lineHeight: 1.2,
187
+ shadowColor: '#000000',
188
+ shadowBlur: 4,
189
+ shadowOffsetX: 0,
190
+ shadowOffsetY: 2,
191
+ textTransform: 'none',
192
+ textAlign: 'center',
193
+ padding: 8,
194
+ borderRadius: 0,
195
+ opacity: 1,
196
+ animation: 'fade-in',
197
+ animationDurationMs: 300,
198
+ trackIndex: 0,
199
+ ...preset,
200
+ // Always generate a fresh ID — prevents duplicates sharing the same identity
201
+ id: `text-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`,
202
+ };
203
+ }
@@ -0,0 +1,25 @@
1
+ /** Track gutter width (icon column) */
2
+ export declare const TRACK_GUTTER = 40;
3
+ /** Magnetic snap threshold in pixels on the timeline */
4
+ export declare const TIMELINE_SNAP_PX = 10;
5
+ export declare function formatTime(ms: number): string;
6
+ /** Detailed time format with sub-seconds for ruler labels */
7
+ export declare function formatTimeDetailed(ms: number): string;
8
+ export declare function clamp(v: number, min: number, max: number): number;
9
+ interface ClipLike {
10
+ id: string;
11
+ trackId: string;
12
+ startMs: number;
13
+ durationMs: number;
14
+ }
15
+ /**
16
+ * Find the nearest valid (non-overlapping) position for a clip on its track.
17
+ * Scans gaps between existing clips and returns the closest valid startMs.
18
+ */
19
+ export declare function findNearestValidPosition(clips: ClipLike[], clipId: string, trackId: string, proposedStartMs: number, durationMs: number, maxMs: number): number;
20
+ /**
21
+ * Find the first available gap on a track to place a new clip.
22
+ * Returns startMs or null if no space available.
23
+ */
24
+ export declare function findFirstGap(clips: ClipLike[], trackId: string, durationMs: number, maxMs: number): number | null;
25
+ export {};
@@ -0,0 +1,81 @@
1
+ /** Track gutter width (icon column) */
2
+ export const TRACK_GUTTER = 40;
3
+ /** Magnetic snap threshold in pixels on the timeline */
4
+ export const TIMELINE_SNAP_PX = 10;
5
+ export function formatTime(ms) {
6
+ const totalSec = Math.floor(ms / 1000);
7
+ const min = Math.floor(totalSec / 60);
8
+ const sec = totalSec % 60;
9
+ return `${String(min).padStart(2, '0')}:${String(sec).padStart(2, '0')}`;
10
+ }
11
+ /** Detailed time format with sub-seconds for ruler labels */
12
+ export function formatTimeDetailed(ms) {
13
+ const totalSec = Math.floor(ms / 1000);
14
+ const min = Math.floor(totalSec / 60);
15
+ const sec = totalSec % 60;
16
+ const frac = Math.floor((ms % 1000) / 100);
17
+ if (min > 0)
18
+ return `${min}:${String(sec).padStart(2, '0')}`;
19
+ if (frac > 0)
20
+ return `${sec}.${frac}s`;
21
+ return `${sec}s`;
22
+ }
23
+ export function clamp(v, min, max) {
24
+ return Math.max(min, Math.min(max, v));
25
+ }
26
+ /**
27
+ * Find the nearest valid (non-overlapping) position for a clip on its track.
28
+ * Scans gaps between existing clips and returns the closest valid startMs.
29
+ */
30
+ export function findNearestValidPosition(clips, clipId, trackId, proposedStartMs, durationMs, maxMs) {
31
+ const others = clips
32
+ .filter((c) => c.id !== clipId && c.trackId === trackId)
33
+ .sort((a, b) => a.startMs - b.startMs);
34
+ if (others.length === 0)
35
+ return clamp(proposedStartMs, 0, maxMs - durationMs);
36
+ // Build valid gap ranges
37
+ const gaps = [];
38
+ let prevEnd = 0;
39
+ for (const other of others) {
40
+ if (other.startMs > prevEnd) {
41
+ gaps.push({ start: prevEnd, end: other.startMs });
42
+ }
43
+ prevEnd = Math.max(prevEnd, other.startMs + other.durationMs);
44
+ }
45
+ if (prevEnd < maxMs) {
46
+ gaps.push({ start: prevEnd, end: maxMs });
47
+ }
48
+ // Find the gap where proposed position fits closest
49
+ let bestStart = clamp(proposedStartMs, 0, maxMs - durationMs);
50
+ let bestDist = Infinity;
51
+ for (const gap of gaps) {
52
+ if (gap.end - gap.start < durationMs)
53
+ continue; // gap too small
54
+ const clampedStart = clamp(proposedStartMs, gap.start, gap.end - durationMs);
55
+ const dist = Math.abs(clampedStart - proposedStartMs);
56
+ if (dist < bestDist) {
57
+ bestDist = dist;
58
+ bestStart = clampedStart;
59
+ }
60
+ }
61
+ return bestStart;
62
+ }
63
+ /**
64
+ * Find the first available gap on a track to place a new clip.
65
+ * Returns startMs or null if no space available.
66
+ */
67
+ export function findFirstGap(clips, trackId, durationMs, maxMs) {
68
+ const trackClips = clips
69
+ .filter((c) => c.trackId === trackId)
70
+ .sort((a, b) => a.startMs - b.startMs);
71
+ let prevEnd = 0;
72
+ for (const clip of trackClips) {
73
+ if (clip.startMs - prevEnd >= durationMs) {
74
+ return prevEnd;
75
+ }
76
+ prevEnd = Math.max(prevEnd, clip.startMs + clip.durationMs);
77
+ }
78
+ if (prevEnd + durationMs <= maxMs)
79
+ return prevEnd;
80
+ return null;
81
+ }
@@ -0,0 +1,13 @@
1
+ import type { Easing } from './easing.js';
2
+ export type AnimatableProp = 'x' | 'y' | 'scale' | 'rotation' | 'opacity' | 'blur' | 'brightness' | 'contrast' | 'saturation' | 'maskProgress';
3
+ export type Keyframe = {
4
+ t: number;
5
+ v: number;
6
+ easing?: Easing;
7
+ };
8
+ export type Animation = {
9
+ keyframes: Keyframe[];
10
+ };
11
+ export type Animations = Partial<Record<AnimatableProp, Animation>>;
12
+ export declare const ANIMATABLE_PROPS: readonly AnimatableProp[];
13
+ export declare const ANIMATABLE_PROP_DEFAULTS: Readonly<Record<AnimatableProp, number>>;
@@ -0,0 +1,16 @@
1
+ export const ANIMATABLE_PROPS = [
2
+ 'x', 'y', 'scale', 'rotation', 'opacity',
3
+ 'blur', 'brightness', 'contrast', 'saturation', 'maskProgress',
4
+ ];
5
+ export const ANIMATABLE_PROP_DEFAULTS = {
6
+ x: 0.5,
7
+ y: 0.5,
8
+ scale: 1,
9
+ rotation: 0,
10
+ opacity: 1,
11
+ blur: 0,
12
+ brightness: 1,
13
+ contrast: 1,
14
+ saturation: 1,
15
+ maskProgress: 1,
16
+ };
@@ -0,0 +1,11 @@
1
+ import type { AnimatableProp } from './animation.js';
2
+ export type AudioFeature = 'rms' | 'low' | 'mid' | 'high' | 'beat';
3
+ export type AudioReactiveModifier = {
4
+ property: AnimatableProp;
5
+ feature: AudioFeature;
6
+ assetId: string;
7
+ factor: number;
8
+ mode: 'add' | 'multiply';
9
+ smoothing?: number;
10
+ };
11
+ export declare const AUDIO_FEATURES: readonly AudioFeature[];
@@ -0,0 +1 @@
1
+ export const AUDIO_FEATURES = ['rms', 'low', 'mid', 'high', 'beat'];
@@ -0,0 +1,16 @@
1
+ /** Result of the audio beat analysis (computed in the browser by
2
+ * `lib/editor/beat-analyzer.ts`; stored on the editor store per asset). */
3
+ export interface BeatAnalysis {
4
+ /** Beats per minute (typically 60-200) */
5
+ bpm: number;
6
+ /** Offset of the first detected beat from start of audio, in ms */
7
+ firstBeatMs: number;
8
+ /** All beats from firstBeatMs to end of analyzed window, in ms */
9
+ beats: number[];
10
+ /** Downbeats — every 4th beat (1, 5, 9, ...) — in ms */
11
+ downbeats: number[];
12
+ /** Source of the BPM value */
13
+ source: 'detected' | 'filename-hint' | 'detected+hint-validated' | 'envelope-fallback';
14
+ /** Duration of the analyzed audio, in ms */
15
+ analyzedDurationMs: number;
16
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,14 @@
1
+ export type EasingPreset = 'linear' | 'easeInQuad' | 'easeOutQuad' | 'easeInOutQuad' | 'easeInCubic' | 'easeOutCubic' | 'easeInOutCubic' | 'easeInQuart' | 'easeOutQuart' | 'easeInOutQuart' | 'easeInExpo' | 'easeOutExpo' | 'easeInOutExpo' | 'easeInBack' | 'easeOutBack' | 'easeInOutBack' | 'easeInElastic' | 'easeOutElastic' | 'easeInOutElastic' | 'easeInBounce' | 'easeOutBounce' | 'easeInOutBounce';
2
+ export type BezierEasing = readonly [number, number, number, number];
3
+ export type SpringConfig = {
4
+ type: 'spring';
5
+ stiffness: number;
6
+ damping: number;
7
+ mass: number;
8
+ };
9
+ export type Easing = EasingPreset | BezierEasing | SpringConfig;
10
+ export declare const EASING_PRESETS: readonly EasingPreset[];
11
+ export declare const DEFAULT_SPRING: SpringConfig;
12
+ export declare function isBezierEasing(e: Easing): e is BezierEasing;
13
+ export declare function isSpringConfig(e: Easing): e is SpringConfig;
14
+ export declare function isEasingPreset(e: Easing): e is EasingPreset;
@@ -0,0 +1,25 @@
1
+ export const EASING_PRESETS = [
2
+ 'linear',
3
+ 'easeInQuad', 'easeOutQuad', 'easeInOutQuad',
4
+ 'easeInCubic', 'easeOutCubic', 'easeInOutCubic',
5
+ 'easeInQuart', 'easeOutQuart', 'easeInOutQuart',
6
+ 'easeInExpo', 'easeOutExpo', 'easeInOutExpo',
7
+ 'easeInBack', 'easeOutBack', 'easeInOutBack',
8
+ 'easeInElastic', 'easeOutElastic', 'easeInOutElastic',
9
+ 'easeInBounce', 'easeOutBounce', 'easeInOutBounce',
10
+ ];
11
+ export const DEFAULT_SPRING = {
12
+ type: 'spring',
13
+ stiffness: 170,
14
+ damping: 26,
15
+ mass: 1,
16
+ };
17
+ export function isBezierEasing(e) {
18
+ return Array.isArray(e) && e.length === 4;
19
+ }
20
+ export function isSpringConfig(e) {
21
+ return typeof e === 'object' && !Array.isArray(e) && e.type === 'spring';
22
+ }
23
+ export function isEasingPreset(e) {
24
+ return typeof e === 'string';
25
+ }
@@ -0,0 +1,123 @@
1
+ export interface TranscriptionWord {
2
+ word: string;
3
+ startMs: number;
4
+ endMs: number;
5
+ confidence: number;
6
+ }
7
+ export interface TranscriptionCue {
8
+ id: string;
9
+ text: string;
10
+ startMs: number;
11
+ endMs: number;
12
+ words: TranscriptionWord[];
13
+ }
14
+ export interface TranscriptionResult {
15
+ cues: TranscriptionCue[];
16
+ durationMs: number;
17
+ language: string;
18
+ }
19
+ export type SubtitleAnimation = 'fade' | 'pop' | 'slide' | 'typewriter' | 'word-highlight' | 'word-popup' | 'outline-reveal' | 'ugc-pop' | 'karaoke-marker' | 'cinematic-fade' | 'neon-cyberpunk' | 'playful-wiggle';
20
+ /** Vertical position as percentage from top (0 = top, 100 = bottom) */
21
+ export type SubtitlePosition = number;
22
+ export interface SubtitleStyleConfig {
23
+ fontFamily: string;
24
+ fontSize: number;
25
+ fontWeight: number;
26
+ color: string;
27
+ backgroundColor: string;
28
+ backgroundOpacity: number;
29
+ strokeColor: string;
30
+ strokeWidth: number;
31
+ textTransform: 'none' | 'uppercase';
32
+ padding: number;
33
+ borderRadius: number;
34
+ highlightColor: string;
35
+ shadowColor: string;
36
+ shadowBlur: number;
37
+ shadowOffsetX?: number;
38
+ shadowOffsetY?: number;
39
+ lineHeight?: number;
40
+ }
41
+ /** Canvas-space subtitle position and transform (normalized 0-1) */
42
+ export interface SubtitleLayout {
43
+ x: number;
44
+ y: number;
45
+ scale: number;
46
+ maxWidthPct?: number;
47
+ maxHeightPct?: number;
48
+ rotation: number;
49
+ }
50
+ export interface SubtitleTemplate {
51
+ id: string;
52
+ name: string;
53
+ format: '9:16' | '16:9' | '1:1';
54
+ animation: SubtitleAnimation;
55
+ position: SubtitlePosition;
56
+ style: SubtitleStyleConfig;
57
+ }
58
+ /** Text overlay animation types */
59
+ export type TextAnimation = 'none' | 'fade-in' | 'fade-out' | 'fade-in-out' | 'slide-up' | 'slide-down' | 'slide-left' | 'slide-right' | 'pop' | 'typewriter' | 'bounce' | 'blur-in' | 'scale-up';
60
+ import type { Animations } from './animation.js';
61
+ import type { Effect } from './effect.js';
62
+ import type { AudioReactiveModifier } from './audioReactive.js';
63
+ /** Text overlay on the canvas with timing, position, style, and animation */
64
+ export interface TextOverlay {
65
+ id: string;
66
+ text: string;
67
+ startMs: number;
68
+ endMs: number;
69
+ x: number;
70
+ y: number;
71
+ scale: number;
72
+ rotation: number;
73
+ fontFamily: string;
74
+ fontSize: number;
75
+ fontWeight: number;
76
+ fontStyle?: 'normal' | 'italic';
77
+ textDecoration?: 'none' | 'underline' | 'line-through' | 'overline';
78
+ color: string;
79
+ backgroundColor: string;
80
+ backgroundOpacity: number;
81
+ strokeColor: string;
82
+ strokeWidth: number;
83
+ strokeOpacity: number;
84
+ lineHeight: number;
85
+ shadowColor: string;
86
+ shadowBlur: number;
87
+ shadowOffsetX: number;
88
+ shadowOffsetY: number;
89
+ textTransform: 'none' | 'uppercase';
90
+ textAlign: 'left' | 'center' | 'right';
91
+ padding: number;
92
+ borderRadius: number;
93
+ opacity: number;
94
+ animation: TextAnimation;
95
+ animationDurationMs: number;
96
+ animationIn?: {
97
+ type: 'none' | 'fade' | 'slide-up' | 'slide-down' | 'zoom-in' | 'zoom-out' | 'zoom-pan' | 'spin' | 'blur-reveal';
98
+ durationMs: number;
99
+ };
100
+ animationOut?: {
101
+ type: 'none' | 'fade' | 'slide-up' | 'slide-down' | 'zoom-in' | 'zoom-out' | 'zoom-pan' | 'spin' | 'blur-reveal';
102
+ durationMs: number;
103
+ };
104
+ trackIndex?: number;
105
+ animations?: Animations;
106
+ effects?: Effect[];
107
+ audioReactive?: AudioReactiveModifier[];
108
+ }
109
+ /** Text overlay preset (one-click styled text) */
110
+ export interface TextPreset {
111
+ id: string;
112
+ name: string;
113
+ icon: string;
114
+ defaults: Partial<TextOverlay>;
115
+ }
116
+ export interface EditorProject {
117
+ cues: TranscriptionCue[];
118
+ templateId: string;
119
+ styleOverrides: Partial<SubtitleStyleConfig>;
120
+ videoUrl: string | null;
121
+ videoDurationMs: number;
122
+ textOverlays?: TextOverlay[];
123
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,32 @@
1
+ export type Effect = {
2
+ type: 'blur';
3
+ amount: number;
4
+ } | {
5
+ type: 'vignette';
6
+ intensity: number;
7
+ size: number;
8
+ } | {
9
+ type: 'colorGrade';
10
+ brightness?: number;
11
+ contrast?: number;
12
+ saturation?: number;
13
+ hue?: number;
14
+ } | {
15
+ type: 'grain';
16
+ intensity: number;
17
+ size: number;
18
+ } | {
19
+ type: 'glitch';
20
+ intensity: number;
21
+ blockSize: number;
22
+ } | {
23
+ type: 'chromaticAberration';
24
+ offset: number;
25
+ } | {
26
+ type: 'pixelate';
27
+ blockSize: number;
28
+ };
29
+ export type EffectType = Effect['type'];
30
+ export declare const GPU_EFFECTS: ReadonlySet<EffectType>;
31
+ export declare function isGpuEffect(effect: Effect): boolean;
32
+ export declare function hasGpuEffect(effects: readonly Effect[] | undefined): boolean;
@@ -0,0 +1,17 @@
1
+ export const GPU_EFFECTS = new Set([
2
+ 'grain',
3
+ 'glitch',
4
+ 'chromaticAberration',
5
+ 'pixelate',
6
+ ]);
7
+ export function isGpuEffect(effect) {
8
+ return GPU_EFFECTS.has(effect.type);
9
+ }
10
+ export function hasGpuEffect(effects) {
11
+ if (!effects)
12
+ return false;
13
+ for (const e of effects)
14
+ if (isGpuEffect(e))
15
+ return true;
16
+ return false;
17
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * V2 mask schema. All coordinates in 0-1 fractions of the object's bounds.
3
+ *
4
+ * NOTE: this is distinct from the V1 `MaskConfig` (src/store/editor.ts) which
5
+ * uses `{ type: 'rounded' | 'circle' | 'star' | 'hexagon', cropX/Y/W/H }`.
6
+ * V1 and V2 masks coexist during the v1→v2 migration window.
7
+ */
8
+ export type Mask = {
9
+ type: 'none';
10
+ } | {
11
+ type: 'rect';
12
+ x: number;
13
+ y: number;
14
+ w: number;
15
+ h: number;
16
+ feather?: number;
17
+ } | {
18
+ type: 'circle';
19
+ cx: number;
20
+ cy: number;
21
+ r: number;
22
+ feather?: number;
23
+ } | {
24
+ type: 'reveal';
25
+ direction: 'ltr' | 'rtl' | 'ttb' | 'btt';
26
+ progress: number;
27
+ } | {
28
+ type: 'clipPath';
29
+ svgPath: string;
30
+ };
31
+ export type MaskType = Mask['type'];
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,36 @@
1
+ import type { AssetClip, CanvasObject, CanvasPreset, TimelineClip, TrackDefinition } from '../store.js';
2
+ import type { TextOverlay } from './editor.js';
3
+ /**
4
+ * Editor project file — v2 schema (spec v2.2).
5
+ *
6
+ * Note on versioning: the existing V1 project files already contain a
7
+ * `version: 2` field (ad-hoc, predates the v2 spec). To avoid colliding with
8
+ * it, the v2 migration uses a separate `schemaVersion: 2` marker. A project
9
+ * is considered v1 if `schemaVersion` is missing or < 2.
10
+ */
11
+ export type ProjectFile = {
12
+ /** Hard version gate for the v2 spec schema. */
13
+ schemaVersion: 2;
14
+ /** Legacy `version` field kept as-is for backward compat (ignored by spec). */
15
+ version?: number;
16
+ projectName: string;
17
+ videoDurationMs: number;
18
+ canvasPreset: CanvasPreset;
19
+ tracks: TrackDefinition[];
20
+ timelineClips: Array<Omit<TimelineClip, 'url'> & {
21
+ url?: string;
22
+ }>;
23
+ canvasObjects: CanvasObject[];
24
+ textOverlays: TextOverlay[];
25
+ assets: Array<Partial<AssetClip> & {
26
+ id: string;
27
+ }>;
28
+ cues?: unknown[];
29
+ templateId?: string;
30
+ styleOverrides?: Record<string, unknown>;
31
+ animation?: unknown;
32
+ position?: unknown;
33
+ subtitleLayout?: unknown;
34
+ };
35
+ export declare function isV1Project(p: unknown): boolean;
36
+ export declare function isV2Project(p: unknown): p is ProjectFile;