@aicut/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/LICENSE +21 -0
- package/README.md +201 -0
- package/dist/index.cjs +3344 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +640 -0
- package/dist/index.d.ts +640 -0
- package/dist/index.js +3331 -0
- package/dist/index.js.map +1 -0
- package/package.json +38 -0
- package/styles/theme.css +414 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,640 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UI strings the editor paints into the DOM (toolbar tooltips, the
|
|
3
|
+
* fullscreen exit button) and onto the timeline canvas (phantom new-
|
|
4
|
+
* track label, track header labels). Every user-visible literal in
|
|
5
|
+
* `@aicut/core` flows through this interface — there are no hidden
|
|
6
|
+
* hard-coded translations elsewhere in the library.
|
|
7
|
+
*
|
|
8
|
+
* Defaults to English. Hosts that want Chinese (or any other locale)
|
|
9
|
+
* pass `locale: localeZh` to `Editor.create` / `Timeline.create`, or
|
|
10
|
+
* override individual keys with `locale: { undo: "撤销" }`.
|
|
11
|
+
*/
|
|
12
|
+
interface Locale {
|
|
13
|
+
undo: string;
|
|
14
|
+
redo: string;
|
|
15
|
+
split: string;
|
|
16
|
+
trimLeft: string;
|
|
17
|
+
trimRight: string;
|
|
18
|
+
speedComingSoon: string;
|
|
19
|
+
playPause: string;
|
|
20
|
+
fullscreen: string;
|
|
21
|
+
snap: string;
|
|
22
|
+
/** Title shown on the snap button when snap is ON (clicking turns OFF). */
|
|
23
|
+
snapOnTitle: string;
|
|
24
|
+
/** Title shown when snap is OFF (clicking turns ON). */
|
|
25
|
+
snapOffTitle: string;
|
|
26
|
+
zoomOut: string;
|
|
27
|
+
zoomIn: string;
|
|
28
|
+
reset: string;
|
|
29
|
+
exitFullscreen: string;
|
|
30
|
+
exitFullscreenTitle: string;
|
|
31
|
+
/** Phantom row that appears under the last track during a drag. */
|
|
32
|
+
newTrack: string;
|
|
33
|
+
/** Track header — `{n}` is replaced with the 1-based track index. */
|
|
34
|
+
videoTrackLabel: string;
|
|
35
|
+
/** Same template format as videoTrackLabel. */
|
|
36
|
+
audioTrackLabel: string;
|
|
37
|
+
}
|
|
38
|
+
/** English. The library default — chosen over Chinese as the OSS norm. */
|
|
39
|
+
declare const localeEn: Locale;
|
|
40
|
+
/** Simplified Chinese. */
|
|
41
|
+
declare const localeZh: Locale;
|
|
42
|
+
/** Spread defaults under host overrides — host can supply a partial. */
|
|
43
|
+
declare function mergeLocale(partial: Partial<Locale> | undefined): Locale;
|
|
44
|
+
/**
|
|
45
|
+
* Replace `{key}` placeholders in a template. We only need `{n}`
|
|
46
|
+
* substitution today; the implementation is generic so additional
|
|
47
|
+
* keys (e.g. `{name}`) won't need a second pass.
|
|
48
|
+
*/
|
|
49
|
+
declare function formatLabel(template: string, vars: Record<string, string | number>): string;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Milliseconds. All timing in the project is expressed as integer ms to
|
|
53
|
+
* keep JSON serialization unambiguous (no frame-rate coupling in the
|
|
54
|
+
* data model — the renderer can present time as frames if it wants).
|
|
55
|
+
*/
|
|
56
|
+
type Ms = number;
|
|
57
|
+
interface MediaSource {
|
|
58
|
+
id: string;
|
|
59
|
+
url: string;
|
|
60
|
+
kind: "video" | "audio";
|
|
61
|
+
/** Optional — probed lazily from the <video> element if absent. */
|
|
62
|
+
duration?: Ms;
|
|
63
|
+
name?: string;
|
|
64
|
+
}
|
|
65
|
+
interface Clip {
|
|
66
|
+
id: string;
|
|
67
|
+
sourceId: string;
|
|
68
|
+
/** Window into the source — `in` inclusive, `out` exclusive. */
|
|
69
|
+
in: Ms;
|
|
70
|
+
out: Ms;
|
|
71
|
+
/** Position on the timeline. */
|
|
72
|
+
start: Ms;
|
|
73
|
+
/**
|
|
74
|
+
* Playback rate. 1 = normal, 2 = 2× speed. Default 1.
|
|
75
|
+
* Persisted in the project JSON so a host can restore exactly.
|
|
76
|
+
*/
|
|
77
|
+
speed?: number;
|
|
78
|
+
}
|
|
79
|
+
interface Track {
|
|
80
|
+
id: string;
|
|
81
|
+
kind: "video" | "audio";
|
|
82
|
+
/** Clips on this track. Must be kept sorted by `start` and non-overlapping. */
|
|
83
|
+
clips: Clip[];
|
|
84
|
+
}
|
|
85
|
+
interface Project {
|
|
86
|
+
/** Schema version — bump when breaking the JSON shape. */
|
|
87
|
+
version: 1;
|
|
88
|
+
sources: MediaSource[];
|
|
89
|
+
tracks: Track[];
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Subset of CSS variables the editor honors. Pass any custom values
|
|
93
|
+
* via `Editor` options; everything is forwarded as `--aicut-*` on the
|
|
94
|
+
* editor's root container, so a host can also override via plain CSS.
|
|
95
|
+
*/
|
|
96
|
+
interface Theme {
|
|
97
|
+
brand?: string;
|
|
98
|
+
secondary?: string;
|
|
99
|
+
surface?: string;
|
|
100
|
+
dark?: string;
|
|
101
|
+
muted?: string;
|
|
102
|
+
card?: string;
|
|
103
|
+
success?: string;
|
|
104
|
+
warning?: string;
|
|
105
|
+
info?: string;
|
|
106
|
+
error?: string;
|
|
107
|
+
/** Toolbar / ruler chrome. Background of the editor frame. */
|
|
108
|
+
controlsBg?: string;
|
|
109
|
+
controlsBorder?: string;
|
|
110
|
+
controlsText?: string;
|
|
111
|
+
controlsHover?: string;
|
|
112
|
+
controlsActive?: string;
|
|
113
|
+
/** Letterbox color around the preview video. Defaults to black. */
|
|
114
|
+
previewBg?: string;
|
|
115
|
+
radiusSm?: string;
|
|
116
|
+
radiusMd?: string;
|
|
117
|
+
radiusLg?: string;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
interface EditorOptions {
|
|
121
|
+
/** Host element to mount the editor into. Will be wiped on init. */
|
|
122
|
+
container: HTMLElement;
|
|
123
|
+
/** Initial project state. Falls back to an empty single-track project. */
|
|
124
|
+
project?: Project;
|
|
125
|
+
/** CSS variable overrides. */
|
|
126
|
+
theme?: Theme;
|
|
127
|
+
/** Initial playhead position (ms). */
|
|
128
|
+
initialTime?: Ms;
|
|
129
|
+
/** Initial timeline zoom (pixels per second). Defaults to 80. */
|
|
130
|
+
initialScale?: number;
|
|
131
|
+
/** Initial snap toggle. Defaults to true. */
|
|
132
|
+
initialSnap?: boolean;
|
|
133
|
+
/**
|
|
134
|
+
* UI string overrides. Falls back to English (`localeEn`) for any
|
|
135
|
+
* keys not provided. Use `localeZh` as the value for full Chinese.
|
|
136
|
+
* Call `editor.setLocale(...)` to switch at runtime.
|
|
137
|
+
*/
|
|
138
|
+
locale?: Partial<Locale>;
|
|
139
|
+
}
|
|
140
|
+
interface EditorEventMap {
|
|
141
|
+
/** Emitted whenever the project mutates. */
|
|
142
|
+
change: {
|
|
143
|
+
project: Project;
|
|
144
|
+
};
|
|
145
|
+
/** Playhead position update — driven by the playback tick loop. */
|
|
146
|
+
time: {
|
|
147
|
+
timeMs: Ms;
|
|
148
|
+
};
|
|
149
|
+
play: void;
|
|
150
|
+
pause: void;
|
|
151
|
+
/** A source's metadata finished loading (duration etc). */
|
|
152
|
+
ready: {
|
|
153
|
+
sourceId: string | null;
|
|
154
|
+
};
|
|
155
|
+
/** User clicked the Export button. Host decides what to do with the JSON. */
|
|
156
|
+
export: {
|
|
157
|
+
project: Project;
|
|
158
|
+
};
|
|
159
|
+
error: {
|
|
160
|
+
error: Error;
|
|
161
|
+
};
|
|
162
|
+
/** Currently selected clip id, or null. */
|
|
163
|
+
selectionChange: {
|
|
164
|
+
clipId: string | null;
|
|
165
|
+
};
|
|
166
|
+
/** Zoom (px/sec) changed. */
|
|
167
|
+
scaleChange: {
|
|
168
|
+
pxPerSec: number;
|
|
169
|
+
};
|
|
170
|
+
/** Snap toggle changed. */
|
|
171
|
+
snapChange: {
|
|
172
|
+
snap: boolean;
|
|
173
|
+
};
|
|
174
|
+
/** Undo/redo stack states changed (button enablement). */
|
|
175
|
+
historyChange: {
|
|
176
|
+
canUndo: boolean;
|
|
177
|
+
canRedo: boolean;
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
type EditorEventName = keyof EditorEventMap;
|
|
181
|
+
interface EditorApi {
|
|
182
|
+
play(): void;
|
|
183
|
+
pause(): void;
|
|
184
|
+
togglePlay(): void;
|
|
185
|
+
seek(timeMs: Ms): void;
|
|
186
|
+
getTime(): Ms;
|
|
187
|
+
getDuration(): Ms;
|
|
188
|
+
isPlaying(): boolean;
|
|
189
|
+
enterFullscreen(): Promise<void>;
|
|
190
|
+
exitFullscreen(): Promise<void>;
|
|
191
|
+
isFullscreen(): boolean;
|
|
192
|
+
split(timeMs?: Ms): string[] | null;
|
|
193
|
+
/** Alias of split, kept for back-compat. */
|
|
194
|
+
cut(timeMs?: Ms): string[] | null;
|
|
195
|
+
/** Trim the selected clip's left edge to the given time (or playhead). */
|
|
196
|
+
trimLeft(timeMs?: Ms): boolean;
|
|
197
|
+
/** Trim the selected clip's right edge to the given time (or playhead). */
|
|
198
|
+
trimRight(timeMs?: Ms): boolean;
|
|
199
|
+
removeClip(clipId: string): boolean;
|
|
200
|
+
setClipSpeed(clipId: string, speed: number): boolean;
|
|
201
|
+
previewMoveTarget(clipId: string, start: Ms, intendedTrackId?: string): {
|
|
202
|
+
trackIndex: number;
|
|
203
|
+
trackId: string;
|
|
204
|
+
wouldCreateNew: boolean;
|
|
205
|
+
} | null;
|
|
206
|
+
addTrack(kind: Track["kind"]): Track;
|
|
207
|
+
removeTrack(trackId: string): boolean;
|
|
208
|
+
moveClip(clipId: string, opts: {
|
|
209
|
+
start?: Ms;
|
|
210
|
+
trackId?: string;
|
|
211
|
+
newTrack?: boolean;
|
|
212
|
+
}): boolean;
|
|
213
|
+
resizeClip(clipId: string, edits: Partial<Pick<Clip, "in" | "out" | "start">>): boolean;
|
|
214
|
+
addSource(source: MediaSource, opts?: {
|
|
215
|
+
appendClip?: boolean;
|
|
216
|
+
}): MediaSource;
|
|
217
|
+
setProject(project: Project): void;
|
|
218
|
+
getProject(): Project;
|
|
219
|
+
/** Replace the project with a brand-new empty one. */
|
|
220
|
+
reset(): void;
|
|
221
|
+
setTheme(theme: Theme): void;
|
|
222
|
+
/**
|
|
223
|
+
* Swap the UI locale at runtime. Partial overrides merge with the
|
|
224
|
+
* English default. Triggers a re-render so the toolbar tooltips
|
|
225
|
+
* and timeline canvas labels pick up the new strings immediately.
|
|
226
|
+
*/
|
|
227
|
+
setLocale(locale: Partial<Locale>): void;
|
|
228
|
+
/**
|
|
229
|
+
* Fire the `export` event with the current project JSON. Hosts call
|
|
230
|
+
* this from their own export button (built into their toolbarRight
|
|
231
|
+
* slot, a keyboard shortcut, a menu item, etc.) to surface project
|
|
232
|
+
* data to whatever pipeline they own. The library never invokes
|
|
233
|
+
* this on its own — it has no UI for export.
|
|
234
|
+
*/
|
|
235
|
+
requestExport(): void;
|
|
236
|
+
getScale(): number;
|
|
237
|
+
setScale(pxPerSec: number): void;
|
|
238
|
+
getSnap(): boolean;
|
|
239
|
+
setSnap(snap: boolean): void;
|
|
240
|
+
getSelection(): string | null;
|
|
241
|
+
setSelection(clipId: string | null): void;
|
|
242
|
+
canUndo(): boolean;
|
|
243
|
+
canRedo(): boolean;
|
|
244
|
+
undo(): boolean;
|
|
245
|
+
redo(): boolean;
|
|
246
|
+
/**
|
|
247
|
+
* Bookend slot at the very left of the top toolbar — host appends
|
|
248
|
+
* its own controls (e.g. an aspect-ratio dropdown). Empty by default
|
|
249
|
+
* and renders no separator until populated.
|
|
250
|
+
*/
|
|
251
|
+
readonly toolbarLeft: HTMLElement;
|
|
252
|
+
/** Right-side bookend slot — conventionally export / save / share. */
|
|
253
|
+
readonly toolbarRight: HTMLElement;
|
|
254
|
+
on<K extends EditorEventName>(event: K, handler: (payload: EditorEventMap[K]) => void): () => void;
|
|
255
|
+
off<K extends EditorEventName>(event: K, handler: (payload: EditorEventMap[K]) => void): void;
|
|
256
|
+
destroy(): void;
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Top-level editor instance — the only stateful object a host app
|
|
260
|
+
* interacts with. Owns the project, the playback engine, the vanilla
|
|
261
|
+
* DOM UI, plus viewport (zoom/snap), selection, and history state.
|
|
262
|
+
*
|
|
263
|
+
* Framework wrappers (`@aicut/react`, `@aicut/vue`) should mount a
|
|
264
|
+
* container, instantiate this once, mirror prop changes (`theme`)
|
|
265
|
+
* into it, and forward events as framework-native callbacks.
|
|
266
|
+
*/
|
|
267
|
+
declare class Editor implements EditorApi {
|
|
268
|
+
private container;
|
|
269
|
+
private project;
|
|
270
|
+
private engine;
|
|
271
|
+
private ui;
|
|
272
|
+
private bus;
|
|
273
|
+
private history;
|
|
274
|
+
private selectedClipId;
|
|
275
|
+
private pxPerSec;
|
|
276
|
+
private snap;
|
|
277
|
+
private locale;
|
|
278
|
+
private destroyed;
|
|
279
|
+
constructor(opts: EditorOptions);
|
|
280
|
+
static create(opts: EditorOptions): Editor;
|
|
281
|
+
get toolbarLeft(): HTMLElement;
|
|
282
|
+
get toolbarRight(): HTMLElement;
|
|
283
|
+
play(): void;
|
|
284
|
+
pause(): void;
|
|
285
|
+
togglePlay(): void;
|
|
286
|
+
seek(timeMs: Ms): void;
|
|
287
|
+
getTime(): Ms;
|
|
288
|
+
getDuration(): Ms;
|
|
289
|
+
isPlaying(): boolean;
|
|
290
|
+
/**
|
|
291
|
+
* In-tab "fullscreen" — covers the browser viewport via fixed
|
|
292
|
+
* positioning, NOT the OS Fullscreen API. This is what the reference
|
|
293
|
+
* UI calls "全屏预览": the user stays in their tab, no browser
|
|
294
|
+
* permission prompt, ESC exits. Browser fullscreen would also work
|
|
295
|
+
* but is heavier UX and gets blocked in iframes.
|
|
296
|
+
*/
|
|
297
|
+
enterFullscreen(): Promise<void>;
|
|
298
|
+
exitFullscreen(): Promise<void>;
|
|
299
|
+
isFullscreen(): boolean;
|
|
300
|
+
/**
|
|
301
|
+
* Split the clip at `timeMs` (or playhead). Returns the two new clip ids
|
|
302
|
+
* or null if there's no clip to split at that time.
|
|
303
|
+
*/
|
|
304
|
+
split(timeMs?: Ms): string[] | null;
|
|
305
|
+
cut(timeMs?: Ms): string[] | null;
|
|
306
|
+
trimLeft(timeMs?: Ms): boolean;
|
|
307
|
+
trimRight(timeMs?: Ms): boolean;
|
|
308
|
+
removeClip(clipId: string): boolean;
|
|
309
|
+
setClipSpeed(clipId: string, speed: number): boolean;
|
|
310
|
+
addTrack(kind: Track["kind"]): Track;
|
|
311
|
+
removeTrack(trackId: string): boolean;
|
|
312
|
+
/**
|
|
313
|
+
* Pure prediction of where a `moveClip(...)` would land — same smart
|
|
314
|
+
* routing as the real move (intended → source → other → new track),
|
|
315
|
+
* just no mutation, no history. Lets the Timeline preview the
|
|
316
|
+
* ACTUAL outcome of a drop so the ghost stops lying about new
|
|
317
|
+
* tracks that won't get created.
|
|
318
|
+
*/
|
|
319
|
+
previewMoveTarget(clipId: string, start: Ms, intendedTrackId?: string): {
|
|
320
|
+
trackIndex: number;
|
|
321
|
+
trackId: string;
|
|
322
|
+
wouldCreateNew: boolean;
|
|
323
|
+
} | null;
|
|
324
|
+
moveClip(clipId: string, opts: {
|
|
325
|
+
start?: Ms;
|
|
326
|
+
trackId?: string;
|
|
327
|
+
newTrack?: boolean;
|
|
328
|
+
}): boolean;
|
|
329
|
+
resizeClip(clipId: string, edits: Partial<Pick<Clip, "in" | "out" | "start">>): boolean;
|
|
330
|
+
addSource(source: MediaSource, opts?: {
|
|
331
|
+
appendClip?: boolean;
|
|
332
|
+
}): MediaSource;
|
|
333
|
+
setProject(project: Project): void;
|
|
334
|
+
getProject(): Project;
|
|
335
|
+
/**
|
|
336
|
+
* Restore the "fresh import" state: same media library, single
|
|
337
|
+
* default video track, one full-length clip per video source laid
|
|
338
|
+
* end-to-end. This mirrors the initial layout a host would get
|
|
339
|
+
* after dropping their videos in, so "reset" feels like "start
|
|
340
|
+
* over without re-importing" rather than "wipe everything".
|
|
341
|
+
*
|
|
342
|
+
* Goes through the regular history stack — ⌘Z brings the previous
|
|
343
|
+
* edit back. Sources without a known duration are skipped (they'd
|
|
344
|
+
* render as zero-width clips, which is worse than absent).
|
|
345
|
+
*/
|
|
346
|
+
reset(): void;
|
|
347
|
+
setTheme(theme: Theme): void;
|
|
348
|
+
setLocale(locale: Partial<Locale>): void;
|
|
349
|
+
/** Internal — UI reads the resolved locale here on each render. */
|
|
350
|
+
getLocale(): Locale;
|
|
351
|
+
requestExport(): void;
|
|
352
|
+
getScale(): number;
|
|
353
|
+
setScale(pxPerSec: number): void;
|
|
354
|
+
getSnap(): boolean;
|
|
355
|
+
setSnap(snap: boolean): void;
|
|
356
|
+
/** Snap a candidate ms to the nearest snappable surface within SNAP_PX. */
|
|
357
|
+
snapMs(timeMs: Ms, ignoreClipId?: string | null): Ms;
|
|
358
|
+
getSelection(): string | null;
|
|
359
|
+
setSelection(clipId: string | null): void;
|
|
360
|
+
canUndo(): boolean;
|
|
361
|
+
canRedo(): boolean;
|
|
362
|
+
undo(): boolean;
|
|
363
|
+
redo(): boolean;
|
|
364
|
+
on<K extends EditorEventName>(event: K, handler: (payload: EditorEventMap[K]) => void): () => void;
|
|
365
|
+
off<K extends EditorEventName>(event: K, handler: (payload: EditorEventMap[K]) => void): void;
|
|
366
|
+
destroy(): void;
|
|
367
|
+
private appendTrack;
|
|
368
|
+
private resolveTrimTarget;
|
|
369
|
+
private pushHistory;
|
|
370
|
+
private emitHistory;
|
|
371
|
+
private afterMutation;
|
|
372
|
+
private handleSourceMetadata;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
declare function createEmptyProject(): Project;
|
|
376
|
+
/**
|
|
377
|
+
* Defensive normalization — ensures clips on each track are sorted by
|
|
378
|
+
* `start`, IDs exist, and trivially-empty clips (out <= in) are dropped.
|
|
379
|
+
* Called from `Editor.setProject` so consumers can hand us loosely-formed
|
|
380
|
+
* JSON without risking inconsistent internal state.
|
|
381
|
+
*/
|
|
382
|
+
declare function normalizeProject(project: Project): Project;
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Short, stable, URL-safe ID. Not cryptographic — fine for client IDs
|
|
386
|
+
* that get persisted in the project JSON.
|
|
387
|
+
*/
|
|
388
|
+
declare function createId(prefix?: string): string;
|
|
389
|
+
|
|
390
|
+
/** Visual constants — kept here so draw + hit-test share one source of truth. */
|
|
391
|
+
declare const TRACK_HEIGHT = 56;
|
|
392
|
+
declare const RULER_HEIGHT = 24;
|
|
393
|
+
declare const HEADER_WIDTH = 96;
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Public options for the standalone `Timeline` component. The class
|
|
397
|
+
* is framework-agnostic — `@aicut/react` and `@aicut/vue` wrap it,
|
|
398
|
+
* and the built-in `Editor` composes one internally for its timeline
|
|
399
|
+
* panel. Reuse the same instance for a "frame-picker" use case by
|
|
400
|
+
* loading a project with a single video clip and `readOnly: true`.
|
|
401
|
+
*/
|
|
402
|
+
interface TimelineOptions {
|
|
403
|
+
/** Host element. Will be wiped on init. */
|
|
404
|
+
container: HTMLElement;
|
|
405
|
+
project: Project;
|
|
406
|
+
/** Pixels per second. Defaults to 80; auto-fits on mount when possible. */
|
|
407
|
+
pxPerSec?: number;
|
|
408
|
+
/** Initial playhead position. */
|
|
409
|
+
time?: Ms;
|
|
410
|
+
/** Initially selected clip. */
|
|
411
|
+
selectedClipId?: string | null;
|
|
412
|
+
/** Show the track-name header column (left). Default true. */
|
|
413
|
+
showHeader?: boolean;
|
|
414
|
+
/** Disable interactions — useful for read-only preview / frame picker. */
|
|
415
|
+
readOnly?: boolean;
|
|
416
|
+
/** Snap to clip edges + playhead when dragging. Default true. */
|
|
417
|
+
snap?: boolean;
|
|
418
|
+
/** Compute and apply fit-to-window on first project change. Default true. */
|
|
419
|
+
autoFit?: boolean;
|
|
420
|
+
/** UI string overrides (English defaults). Use `localeZh` for Chinese. */
|
|
421
|
+
locale?: Partial<Locale>;
|
|
422
|
+
/**
|
|
423
|
+
* Render an empty 36px toolbar strip at the top of the host element
|
|
424
|
+
* with `toolbarLeft` / `toolbarRight` flex slots. The library paints
|
|
425
|
+
* NOTHING into either slot — hosts append their own controls (an
|
|
426
|
+
* export button, a size/aspect dropdown, etc.). Default false; the
|
|
427
|
+
* canvas takes the full host height when the toolbar is off, so
|
|
428
|
+
* adopting the slot later is a non-breaking opt-in.
|
|
429
|
+
*/
|
|
430
|
+
toolbar?: boolean;
|
|
431
|
+
onSeek?: (timeMs: Ms) => void;
|
|
432
|
+
onSelectClip?: (clipId: string | null) => void;
|
|
433
|
+
onScaleChange?: (pxPerSec: number) => void;
|
|
434
|
+
onDeleteTrack?: (trackId: string) => void;
|
|
435
|
+
onMoveClip?: (clipId: string, opts: {
|
|
436
|
+
start?: Ms;
|
|
437
|
+
trackId?: string;
|
|
438
|
+
newTrack?: boolean;
|
|
439
|
+
}) => void;
|
|
440
|
+
onResizeClip?: (clipId: string, edits: Partial<Pick<Clip, "in" | "out" | "start">>) => void;
|
|
441
|
+
onChange?: (project: Project) => void;
|
|
442
|
+
/**
|
|
443
|
+
* Lets the host predict where a drop will actually land — used to
|
|
444
|
+
* keep the drag-ghost visual honest. The Editor wires this to its
|
|
445
|
+
* smart routing (intended → source → other → new track), so the
|
|
446
|
+
* ghost shows the real outcome rather than just the user's hover.
|
|
447
|
+
*
|
|
448
|
+
* Return `{ trackIndex }` for an existing track, or
|
|
449
|
+
* `{ wouldCreateNew: true }` for the auto-split case.
|
|
450
|
+
*/
|
|
451
|
+
resolveDrop?: (clipId: string, intent: {
|
|
452
|
+
start: Ms;
|
|
453
|
+
intendedTrackIndex: number;
|
|
454
|
+
}) => {
|
|
455
|
+
trackIndex: number;
|
|
456
|
+
wouldCreateNew: boolean;
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* Canvas-rendered, framework-free timeline. Owns ruler, multi-track
|
|
461
|
+
* layout, headers, frame-thumbnails, playhead, snap, and all pointer
|
|
462
|
+
* gestures. No DOM children for clips/ticks/etc — every pixel is
|
|
463
|
+
* painted via 2D canvas, so even hundreds of clips render in <2ms.
|
|
464
|
+
*/
|
|
465
|
+
declare class Timeline {
|
|
466
|
+
private root;
|
|
467
|
+
private opts;
|
|
468
|
+
private canvas;
|
|
469
|
+
private ctx;
|
|
470
|
+
private thumbs;
|
|
471
|
+
private hiddenHost;
|
|
472
|
+
private toolbarEl;
|
|
473
|
+
/**
|
|
474
|
+
* Public flex slot at the left of the top toolbar. `null` when
|
|
475
|
+
* `toolbar` is disabled. Hosts append their own elements (e.g. a
|
|
476
|
+
* size/aspect dropdown). React/Vue wrappers portal children here.
|
|
477
|
+
*/
|
|
478
|
+
readonly toolbarLeft: HTMLDivElement | null;
|
|
479
|
+
/** Right-side counterpart — conventionally used for export/save. */
|
|
480
|
+
readonly toolbarRight: HTMLDivElement | null;
|
|
481
|
+
private project;
|
|
482
|
+
private pxPerSec;
|
|
483
|
+
private timeMs;
|
|
484
|
+
private selectedClipId;
|
|
485
|
+
private snapEnabled;
|
|
486
|
+
private showHeader;
|
|
487
|
+
private readOnly;
|
|
488
|
+
private autoFitEnabled;
|
|
489
|
+
private locale;
|
|
490
|
+
private scrollLeft;
|
|
491
|
+
private scrollTop;
|
|
492
|
+
private viewportWidth;
|
|
493
|
+
private viewportHeight;
|
|
494
|
+
/**
|
|
495
|
+
* `Date.now()` of the last interaction with each scrollbar (scroll
|
|
496
|
+
* change OR hover OR drag). Drives the macOS-style fade — bars are
|
|
497
|
+
* fully opaque for SCROLLBAR_FADE_HOLD_MS after activity, then ease
|
|
498
|
+
* out over the next SCROLLBAR_FADE_OUT_MS.
|
|
499
|
+
*/
|
|
500
|
+
private lastScrollInteractY;
|
|
501
|
+
private lastScrollInteractX;
|
|
502
|
+
private hoverScrollbarY;
|
|
503
|
+
private hoverScrollbarX;
|
|
504
|
+
/** When set, pointer is dragging a scrollbar thumb. */
|
|
505
|
+
private scrollbarDrag;
|
|
506
|
+
private hoveredClipId;
|
|
507
|
+
private hoveredTrackIndex;
|
|
508
|
+
private hoverCursor;
|
|
509
|
+
private dropTargetTrackIndex;
|
|
510
|
+
private snapX;
|
|
511
|
+
private drag;
|
|
512
|
+
/**
|
|
513
|
+
* In-flight ghost of the clip being dragged. Decoupled from the
|
|
514
|
+
* project data so the data stays clean and undo-able only commits
|
|
515
|
+
* on release. Has both the proposed `start` (X) and `trackIndex`
|
|
516
|
+
* (Y), so the rendered ghost follows the cursor across tracks.
|
|
517
|
+
*/
|
|
518
|
+
private dragGhost;
|
|
519
|
+
/**
|
|
520
|
+
* Most recent local pointer coords during a move drag — used by the
|
|
521
|
+
* edge-autoscroll loop to re-run drop-target resolution between
|
|
522
|
+
* pointermove events while scrollTop ticks under a stationary cursor.
|
|
523
|
+
*/
|
|
524
|
+
private lastDragPointerX;
|
|
525
|
+
private lastDragPointerY;
|
|
526
|
+
private dragScrollRafPending;
|
|
527
|
+
private rafPending;
|
|
528
|
+
private hasAutoFitted;
|
|
529
|
+
private resizeObs;
|
|
530
|
+
private destroyed;
|
|
531
|
+
static create(opts: TimelineOptions): Timeline;
|
|
532
|
+
constructor(opts: TimelineOptions);
|
|
533
|
+
/**
|
|
534
|
+
* Sync the project data. Does NOT reset the auto-fit latch — that's
|
|
535
|
+
* what caused the editor-side zoom feedback loop: every Editor
|
|
536
|
+
* mutation called `ui.render() → timeline.setProject()` which used
|
|
537
|
+
* to reset auto-fit, refit on the next frame, emit a new scale,
|
|
538
|
+
* which re-rendered… and round we went. Callers that genuinely
|
|
539
|
+
* want a re-fit (e.g. when the host swaps to a brand-new project)
|
|
540
|
+
* should call `refit()` explicitly.
|
|
541
|
+
*/
|
|
542
|
+
setProject(p: Project): void;
|
|
543
|
+
/** Force a re-fit on the next render. */
|
|
544
|
+
refit(): void;
|
|
545
|
+
getProject(): Project;
|
|
546
|
+
setTime(timeMs: Ms): void;
|
|
547
|
+
getTime(): Ms;
|
|
548
|
+
setScale(pxPerSec: number): void;
|
|
549
|
+
getScale(): number;
|
|
550
|
+
setSelection(id: string | null): void;
|
|
551
|
+
getSelection(): string | null;
|
|
552
|
+
setSnap(snap: boolean): void;
|
|
553
|
+
getSnap(): boolean;
|
|
554
|
+
setLocale(locale: Partial<Locale>): void;
|
|
555
|
+
/** Fit the project's full duration into the current viewport width. */
|
|
556
|
+
fitToWindow(): void;
|
|
557
|
+
/**
|
|
558
|
+
* Test/debug introspection — pixel coordinates of every visible clip,
|
|
559
|
+
* the playhead, and the headers. Because clips are canvas-painted
|
|
560
|
+
* there are no DOM nodes to query in e2e; tests use this instead.
|
|
561
|
+
* Exposed publicly so React/Vue wrappers can forward it to a ref.
|
|
562
|
+
*/
|
|
563
|
+
getDebugInfo(): {
|
|
564
|
+
pxPerSec: number;
|
|
565
|
+
scrollLeft: number;
|
|
566
|
+
viewportWidth: number;
|
|
567
|
+
viewportHeight: number;
|
|
568
|
+
playheadX: number;
|
|
569
|
+
clips: Array<{
|
|
570
|
+
id: string;
|
|
571
|
+
trackIndex: number;
|
|
572
|
+
x: number;
|
|
573
|
+
width: number;
|
|
574
|
+
y: number;
|
|
575
|
+
height: number;
|
|
576
|
+
}>;
|
|
577
|
+
};
|
|
578
|
+
destroy(): void;
|
|
579
|
+
private resizeCanvas;
|
|
580
|
+
private computeFitScale;
|
|
581
|
+
private maxScrollLeft;
|
|
582
|
+
private maxScrollTop;
|
|
583
|
+
private clampScroll;
|
|
584
|
+
/**
|
|
585
|
+
* Scrollbar opacity = full for SCROLLBAR_FADE_HOLD_MS after last
|
|
586
|
+
* interaction, then linearly fades to 0 over SCROLLBAR_FADE_OUT_MS.
|
|
587
|
+
* Hovering or actively dragging the bar pins opacity at 1. Returns
|
|
588
|
+
* 0 if the bar isn't needed (content fits).
|
|
589
|
+
*/
|
|
590
|
+
private scrollbarOpacity;
|
|
591
|
+
/** Mark a scrollbar axis as just-touched so its fade timer restarts. */
|
|
592
|
+
private touchScrollbar;
|
|
593
|
+
private scheduleRender;
|
|
594
|
+
/**
|
|
595
|
+
* Keep the raf loop alive while a scrollbar is still in its HOLD or
|
|
596
|
+
* fade-out window. Without this, opacity is sampled once and the
|
|
597
|
+
* bar would freeze at whatever value it had at the last input event
|
|
598
|
+
* instead of smoothly fading out. Skipped when a bar is pinned
|
|
599
|
+
* (hover or active drag) since opacity is constant there.
|
|
600
|
+
*/
|
|
601
|
+
private maybeContinueFade;
|
|
602
|
+
private maybeAutoFit;
|
|
603
|
+
private buildDrawState;
|
|
604
|
+
private readStyle;
|
|
605
|
+
private attachPointer;
|
|
606
|
+
private onPointerDown;
|
|
607
|
+
private onPointerMove;
|
|
608
|
+
/**
|
|
609
|
+
* Update dragGhost + dropTargetTrackIndex for the in-flight move
|
|
610
|
+
* drag, given the current viewport pointer position. Pulled out of
|
|
611
|
+
* onPointerMove so the edge-autoscroll loop can re-run it on each
|
|
612
|
+
* tick — autoscroll moves scrollTop under a stationary cursor, and
|
|
613
|
+
* the ghost must follow the new row under that cursor.
|
|
614
|
+
*/
|
|
615
|
+
private processMoveDrag;
|
|
616
|
+
/**
|
|
617
|
+
* Px-per-frame scroll speed when the pointer is in a vertical edge
|
|
618
|
+
* zone of the track region. Returns 0 outside the zone. Speed ramps
|
|
619
|
+
* linearly from 0 at the zone's inner edge to ~16 px/frame at the
|
|
620
|
+
* outer edge, so brushing the edge gives a gentle nudge and parking
|
|
621
|
+
* deep at it gives a brisk auto-scroll.
|
|
622
|
+
*/
|
|
623
|
+
private dragScrollSpeedY;
|
|
624
|
+
/**
|
|
625
|
+
* Drive vertical auto-scroll while the user holds a clip near the
|
|
626
|
+
* top/bottom edge of the track area. Self-stopping — exits the loop
|
|
627
|
+
* once the pointer leaves the zone, the drag ends, or scroll bottoms
|
|
628
|
+
* out at the clamp.
|
|
629
|
+
*/
|
|
630
|
+
private maybeStartDragAutoScroll;
|
|
631
|
+
private onPointerUp;
|
|
632
|
+
private attachWheel;
|
|
633
|
+
private attachResize;
|
|
634
|
+
private localCoords;
|
|
635
|
+
private hitTarget;
|
|
636
|
+
private trackIndexAtY;
|
|
637
|
+
private applySnap;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
export { type Clip, Editor, type EditorApi, type EditorEventMap, type EditorEventName, type EditorOptions, HEADER_WIDTH, type Locale, type MediaSource, type Ms, type Project, RULER_HEIGHT, TRACK_HEIGHT, type Theme, Timeline, type TimelineOptions, type Track, createEmptyProject, createId, formatLabel, localeEn, localeZh, mergeLocale, normalizeProject };
|