@twick/timeline 0.14.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/README.md +175 -0
- package/dist/context/timeline-context.d.ts +33 -0
- package/dist/context/undo-redo-context.d.ts +21 -0
- package/dist/core/addOns/animation.d.ts +24 -0
- package/dist/core/addOns/frame-effect.d.ts +14 -0
- package/dist/core/addOns/text-effect.d.ts +19 -0
- package/dist/core/editor/timeline.editor.d.ts +94 -0
- package/dist/core/elements/audio.element.d.ts +20 -0
- package/dist/core/elements/base.element.d.ts +35 -0
- package/dist/core/elements/caption.element.d.ts +10 -0
- package/dist/core/elements/circle.element.d.ts +13 -0
- package/dist/core/elements/icon.element.d.ts +9 -0
- package/dist/core/elements/image.element.d.ts +32 -0
- package/dist/core/elements/rect.element.d.ts +11 -0
- package/dist/core/elements/text.element.d.ts +26 -0
- package/dist/core/elements/video.element.d.ts +41 -0
- package/dist/core/track/track.d.ts +77 -0
- package/dist/core/track/track.friend.d.ts +34 -0
- package/dist/core/visitor/element-adder.d.ts +29 -0
- package/dist/core/visitor/element-cloner.d.ts +22 -0
- package/dist/core/visitor/element-deserializer.d.ts +23 -0
- package/dist/core/visitor/element-remover.d.ts +28 -0
- package/dist/core/visitor/element-serializer.d.ts +23 -0
- package/dist/core/visitor/element-splitter.d.ts +28 -0
- package/dist/core/visitor/element-updater.d.ts +28 -0
- package/dist/core/visitor/element-validator.d.ts +34 -0
- package/dist/core/visitor/element-visitor.d.ts +19 -0
- package/dist/index.d.ts +36 -0
- package/dist/index.js +2630 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +2628 -0
- package/dist/index.mjs.map +1 -0
- package/dist/services/data.service.d.ts +25 -0
- package/dist/types/index.d.ts +169 -0
- package/dist/utils/constants.d.ts +55 -0
- package/dist/utils/register-editor.d.ts +8 -0
- package/dist/utils/timeline.utils.d.ts +11 -0
- package/package.json +40 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,2628 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
3
|
+
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
4
|
+
import { jsx } from "react/jsx-runtime";
|
|
5
|
+
import { useState, createContext, useContext, useRef, useMemo, useEffect } from "react";
|
|
6
|
+
const PLAYER_STATE = {
|
|
7
|
+
REFRESH: "Refresh",
|
|
8
|
+
PLAYING: "Playing",
|
|
9
|
+
PAUSED: "Paused"
|
|
10
|
+
};
|
|
11
|
+
const CAPTION_STYLE = {
|
|
12
|
+
WORD_BG_HIGHLIGHT: "highlight_bg",
|
|
13
|
+
WORD_BY_WORD: "word_by_word",
|
|
14
|
+
WORD_BY_WORD_WITH_BG: "word_by_word_with_bg"
|
|
15
|
+
};
|
|
16
|
+
const CAPTION_STYLE_OPTIONS = {
|
|
17
|
+
[CAPTION_STYLE.WORD_BG_HIGHLIGHT]: {
|
|
18
|
+
label: "Highlight Background",
|
|
19
|
+
value: CAPTION_STYLE.WORD_BG_HIGHLIGHT
|
|
20
|
+
},
|
|
21
|
+
[CAPTION_STYLE.WORD_BY_WORD]: {
|
|
22
|
+
label: "Word by Word",
|
|
23
|
+
value: CAPTION_STYLE.WORD_BY_WORD
|
|
24
|
+
},
|
|
25
|
+
[CAPTION_STYLE.WORD_BY_WORD_WITH_BG]: {
|
|
26
|
+
label: "Word with Background",
|
|
27
|
+
value: CAPTION_STYLE.WORD_BY_WORD_WITH_BG
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
const CAPTION_FONT = {
|
|
31
|
+
size: 40
|
|
32
|
+
};
|
|
33
|
+
const CAPTION_COLOR = {
|
|
34
|
+
text: "#ffffff",
|
|
35
|
+
highlight: "#ff4081",
|
|
36
|
+
bgColor: "#8C52FF"
|
|
37
|
+
};
|
|
38
|
+
const WORDS_PER_PHRASE = 4;
|
|
39
|
+
const TIMELINE_ACTION = {
|
|
40
|
+
NONE: "none",
|
|
41
|
+
SET_PLAYER_STATE: "setPlayerState",
|
|
42
|
+
UPDATE_PLAYER_DATA: "updatePlayerData",
|
|
43
|
+
ON_PLAYER_UPDATED: "onPlayerUpdated"
|
|
44
|
+
};
|
|
45
|
+
const TIMELINE_ELEMENT_TYPE = {
|
|
46
|
+
VIDEO: "video",
|
|
47
|
+
CAPTION: "caption",
|
|
48
|
+
IMAGE: "image",
|
|
49
|
+
AUDIO: "audio",
|
|
50
|
+
TEXT: "text",
|
|
51
|
+
RECT: "rect",
|
|
52
|
+
CIRCLE: "circle",
|
|
53
|
+
ICON: "icon"
|
|
54
|
+
};
|
|
55
|
+
const PROCESS_STATE = {
|
|
56
|
+
IDLE: "Idle",
|
|
57
|
+
PROCESSING: "Processing",
|
|
58
|
+
COMPLETED: "Completed",
|
|
59
|
+
FAILED: "Failed"
|
|
60
|
+
};
|
|
61
|
+
const getDecimalNumber = (num, precision = 3) => {
|
|
62
|
+
return Number(num.toFixed(precision));
|
|
63
|
+
};
|
|
64
|
+
const getTotalDuration = (tracks) => {
|
|
65
|
+
return (tracks || []).reduce(
|
|
66
|
+
(maxDuration, timeline) => Math.max(
|
|
67
|
+
maxDuration,
|
|
68
|
+
((timeline == null ? void 0 : timeline.elements) || []).reduce(
|
|
69
|
+
(timelineDuration, element) => Math.max(timelineDuration, element.e),
|
|
70
|
+
0
|
|
71
|
+
)
|
|
72
|
+
),
|
|
73
|
+
0
|
|
74
|
+
);
|
|
75
|
+
};
|
|
76
|
+
const generateShortUuid = () => {
|
|
77
|
+
return "xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
|
|
78
|
+
const r = Math.random() * 16 | 0, v = c === "x" ? r : r & 3 | 8;
|
|
79
|
+
return v.toString(16);
|
|
80
|
+
});
|
|
81
|
+
};
|
|
82
|
+
const getCurrentElements = (currentTime, tracks) => {
|
|
83
|
+
const currentElements = [];
|
|
84
|
+
if (tracks == null ? void 0 : tracks.length) {
|
|
85
|
+
for (let i = 0; i < tracks.length; i++) {
|
|
86
|
+
if (tracks[i]) {
|
|
87
|
+
const elements = tracks[i].getElements();
|
|
88
|
+
for (let j = 0; j < elements.length; j++) {
|
|
89
|
+
const element = elements[j];
|
|
90
|
+
if (element.getStart() <= currentTime && element.getEnd() >= currentTime) {
|
|
91
|
+
currentElements.push(element);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return currentElements;
|
|
98
|
+
};
|
|
99
|
+
const canSplitElement = (element, currentTime) => {
|
|
100
|
+
return element.getStart() <= currentTime && element.getEnd() >= currentTime;
|
|
101
|
+
};
|
|
102
|
+
const isElementId = (id) => id.startsWith("e-");
|
|
103
|
+
const isTrackId = (id) => id.startsWith("t-");
|
|
104
|
+
const imageDimensionsCache = {};
|
|
105
|
+
const videoMetaCache = {};
|
|
106
|
+
const audioDurationCache = {};
|
|
107
|
+
const getAudioDuration = (audioSrc) => {
|
|
108
|
+
if (audioDurationCache[audioSrc]) {
|
|
109
|
+
return Promise.resolve(audioDurationCache[audioSrc]);
|
|
110
|
+
}
|
|
111
|
+
return new Promise((resolve, reject) => {
|
|
112
|
+
const audio = document.createElement("audio");
|
|
113
|
+
audio.preload = "metadata";
|
|
114
|
+
const isSafeUrl = /^(https?:|blob:|data:audio\/)/i.test(audioSrc);
|
|
115
|
+
if (!isSafeUrl) {
|
|
116
|
+
throw new Error("Unsafe audio source URL");
|
|
117
|
+
}
|
|
118
|
+
audio.src = audioSrc;
|
|
119
|
+
audio.onloadedmetadata = () => {
|
|
120
|
+
const duration = audio.duration;
|
|
121
|
+
audioDurationCache[audioSrc] = duration;
|
|
122
|
+
resolve(duration);
|
|
123
|
+
};
|
|
124
|
+
audio.onerror = () => {
|
|
125
|
+
reject(new Error("Failed to load audio metadata"));
|
|
126
|
+
};
|
|
127
|
+
});
|
|
128
|
+
};
|
|
129
|
+
const concurrencyLimit = 5;
|
|
130
|
+
let activeCount = 0;
|
|
131
|
+
const queue = [];
|
|
132
|
+
function runNext() {
|
|
133
|
+
if (queue.length === 0 || activeCount >= concurrencyLimit) return;
|
|
134
|
+
const next = queue.shift();
|
|
135
|
+
if (next) {
|
|
136
|
+
activeCount++;
|
|
137
|
+
next();
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
function limit(fn) {
|
|
141
|
+
return new Promise((resolve, reject) => {
|
|
142
|
+
const task = () => {
|
|
143
|
+
fn().then(resolve).catch(reject).finally(() => {
|
|
144
|
+
activeCount--;
|
|
145
|
+
runNext();
|
|
146
|
+
});
|
|
147
|
+
};
|
|
148
|
+
if (activeCount < concurrencyLimit) {
|
|
149
|
+
activeCount++;
|
|
150
|
+
task();
|
|
151
|
+
} else {
|
|
152
|
+
queue.push(task);
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
const loadImageDimensions = (url) => {
|
|
157
|
+
return new Promise((resolve, reject) => {
|
|
158
|
+
if (typeof document === "undefined") {
|
|
159
|
+
reject(new Error("getImageDimensions() is only available in the browser."));
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
const img = new Image();
|
|
163
|
+
img.onload = () => {
|
|
164
|
+
resolve({ width: img.naturalWidth, height: img.naturalHeight });
|
|
165
|
+
};
|
|
166
|
+
img.onerror = reject;
|
|
167
|
+
img.src = url;
|
|
168
|
+
});
|
|
169
|
+
};
|
|
170
|
+
const getImageDimensions = (url) => {
|
|
171
|
+
if (imageDimensionsCache[url]) {
|
|
172
|
+
return Promise.resolve(imageDimensionsCache[url]);
|
|
173
|
+
}
|
|
174
|
+
return limit(() => loadImageDimensions(url)).then((dimensions) => {
|
|
175
|
+
imageDimensionsCache[url] = dimensions;
|
|
176
|
+
return dimensions;
|
|
177
|
+
});
|
|
178
|
+
};
|
|
179
|
+
const getVideoMeta = (videoSrc) => {
|
|
180
|
+
if (videoMetaCache[videoSrc]) {
|
|
181
|
+
return Promise.resolve(videoMetaCache[videoSrc]);
|
|
182
|
+
}
|
|
183
|
+
return new Promise((resolve, reject) => {
|
|
184
|
+
const video = document.createElement("video");
|
|
185
|
+
video.preload = "metadata";
|
|
186
|
+
const isSafeUrl = /^(https?:|blob:|data:video\/)/i.test(videoSrc);
|
|
187
|
+
if (!isSafeUrl) {
|
|
188
|
+
reject(new Error("Unsafe video source URL"));
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
video.src = videoSrc;
|
|
192
|
+
video.onloadedmetadata = () => {
|
|
193
|
+
const meta = {
|
|
194
|
+
width: video.videoWidth,
|
|
195
|
+
height: video.videoHeight,
|
|
196
|
+
duration: video.duration
|
|
197
|
+
};
|
|
198
|
+
videoMetaCache[videoSrc] = meta;
|
|
199
|
+
resolve(meta);
|
|
200
|
+
};
|
|
201
|
+
video.onerror = () => reject(new Error("Failed to load video metadata"));
|
|
202
|
+
});
|
|
203
|
+
};
|
|
204
|
+
const getObjectFitSize = (objectFit, elementSize, containerSize) => {
|
|
205
|
+
const elementAspectRatio = elementSize.width / elementSize.height;
|
|
206
|
+
const containerAspectRatio = containerSize.width / containerSize.height;
|
|
207
|
+
switch (objectFit) {
|
|
208
|
+
case "contain":
|
|
209
|
+
if (elementAspectRatio > containerAspectRatio) {
|
|
210
|
+
return {
|
|
211
|
+
width: containerSize.width,
|
|
212
|
+
height: containerSize.width / elementAspectRatio
|
|
213
|
+
};
|
|
214
|
+
} else {
|
|
215
|
+
return {
|
|
216
|
+
width: containerSize.height * elementAspectRatio,
|
|
217
|
+
height: containerSize.height
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
case "cover":
|
|
221
|
+
if (elementAspectRatio > containerAspectRatio) {
|
|
222
|
+
return {
|
|
223
|
+
width: containerSize.height * elementAspectRatio,
|
|
224
|
+
height: containerSize.height
|
|
225
|
+
};
|
|
226
|
+
} else {
|
|
227
|
+
return {
|
|
228
|
+
width: containerSize.width,
|
|
229
|
+
height: containerSize.width / elementAspectRatio
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
case "fill":
|
|
233
|
+
return {
|
|
234
|
+
width: containerSize.width,
|
|
235
|
+
height: containerSize.height
|
|
236
|
+
};
|
|
237
|
+
default:
|
|
238
|
+
return {
|
|
239
|
+
width: elementSize.width,
|
|
240
|
+
height: elementSize.height
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
class TrackElement {
|
|
245
|
+
constructor(type, id) {
|
|
246
|
+
__publicField(this, "id");
|
|
247
|
+
__publicField(this, "type");
|
|
248
|
+
__publicField(this, "s");
|
|
249
|
+
__publicField(this, "e");
|
|
250
|
+
__publicField(this, "trackId");
|
|
251
|
+
__publicField(this, "name");
|
|
252
|
+
__publicField(this, "animation");
|
|
253
|
+
__publicField(this, "props");
|
|
254
|
+
this.id = id ?? `e-${generateShortUuid()}`;
|
|
255
|
+
this.type = type;
|
|
256
|
+
this.props = {
|
|
257
|
+
x: 0,
|
|
258
|
+
y: 0
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
getId() {
|
|
262
|
+
return this.id;
|
|
263
|
+
}
|
|
264
|
+
getType() {
|
|
265
|
+
return this.type;
|
|
266
|
+
}
|
|
267
|
+
getStart() {
|
|
268
|
+
return this.s;
|
|
269
|
+
}
|
|
270
|
+
getEnd() {
|
|
271
|
+
return this.e;
|
|
272
|
+
}
|
|
273
|
+
getDuration() {
|
|
274
|
+
return this.e - this.s;
|
|
275
|
+
}
|
|
276
|
+
getTrackId() {
|
|
277
|
+
return this.trackId;
|
|
278
|
+
}
|
|
279
|
+
getProps() {
|
|
280
|
+
return this.props;
|
|
281
|
+
}
|
|
282
|
+
getName() {
|
|
283
|
+
return this.name;
|
|
284
|
+
}
|
|
285
|
+
getAnimation() {
|
|
286
|
+
return this.animation;
|
|
287
|
+
}
|
|
288
|
+
getPosition() {
|
|
289
|
+
var _a, _b;
|
|
290
|
+
return {
|
|
291
|
+
x: ((_a = this.props) == null ? void 0 : _a.x) ?? 0,
|
|
292
|
+
y: ((_b = this.props) == null ? void 0 : _b.y) ?? 0
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
setId(id) {
|
|
296
|
+
this.id = id;
|
|
297
|
+
return this;
|
|
298
|
+
}
|
|
299
|
+
setType(type) {
|
|
300
|
+
this.type = type;
|
|
301
|
+
return this;
|
|
302
|
+
}
|
|
303
|
+
setStart(s) {
|
|
304
|
+
this.s = Math.max(0, s);
|
|
305
|
+
return this;
|
|
306
|
+
}
|
|
307
|
+
setEnd(e) {
|
|
308
|
+
this.e = Math.max(this.s ?? 0, e);
|
|
309
|
+
return this;
|
|
310
|
+
}
|
|
311
|
+
setTrackId(trackId) {
|
|
312
|
+
this.trackId = trackId;
|
|
313
|
+
return this;
|
|
314
|
+
}
|
|
315
|
+
setName(name) {
|
|
316
|
+
this.name = name;
|
|
317
|
+
return this;
|
|
318
|
+
}
|
|
319
|
+
setAnimation(animation) {
|
|
320
|
+
this.animation = animation;
|
|
321
|
+
return this;
|
|
322
|
+
}
|
|
323
|
+
setPosition(position) {
|
|
324
|
+
this.props.x = position.x;
|
|
325
|
+
this.props.y = position.y;
|
|
326
|
+
return this;
|
|
327
|
+
}
|
|
328
|
+
setProps(props) {
|
|
329
|
+
this.props = structuredClone(props);
|
|
330
|
+
return this;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
class VideoElement extends TrackElement {
|
|
334
|
+
constructor(src, parentSize) {
|
|
335
|
+
super(TIMELINE_ELEMENT_TYPE.VIDEO);
|
|
336
|
+
__publicField(this, "baseSize");
|
|
337
|
+
__publicField(this, "mediaDuration");
|
|
338
|
+
__publicField(this, "parentSize");
|
|
339
|
+
__publicField(this, "backgroundColor");
|
|
340
|
+
__publicField(this, "objectFit");
|
|
341
|
+
__publicField(this, "frameEffects");
|
|
342
|
+
__publicField(this, "frame");
|
|
343
|
+
this.objectFit = "cover";
|
|
344
|
+
this.frameEffects = [];
|
|
345
|
+
this.parentSize = parentSize;
|
|
346
|
+
this.props = {
|
|
347
|
+
src,
|
|
348
|
+
play: true,
|
|
349
|
+
playbackRate: 1,
|
|
350
|
+
time: 0,
|
|
351
|
+
mediaFilter: "none",
|
|
352
|
+
volume: 1
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
getParentSize() {
|
|
356
|
+
return this.parentSize;
|
|
357
|
+
}
|
|
358
|
+
getFrame() {
|
|
359
|
+
return this.frame;
|
|
360
|
+
}
|
|
361
|
+
getFrameEffects() {
|
|
362
|
+
return this.frameEffects;
|
|
363
|
+
}
|
|
364
|
+
getBackgroundColor() {
|
|
365
|
+
return this.backgroundColor;
|
|
366
|
+
}
|
|
367
|
+
getObjectFit() {
|
|
368
|
+
return this.objectFit;
|
|
369
|
+
}
|
|
370
|
+
getMediaDuration() {
|
|
371
|
+
return this.mediaDuration;
|
|
372
|
+
}
|
|
373
|
+
getStartAt() {
|
|
374
|
+
return this.props.time || 0;
|
|
375
|
+
}
|
|
376
|
+
getPosition() {
|
|
377
|
+
return {
|
|
378
|
+
x: this.frame.x ?? 0,
|
|
379
|
+
y: this.frame.y ?? 0
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
async updateVideoMeta(updateFrame = true) {
|
|
383
|
+
const meta = await getVideoMeta(this.props.src);
|
|
384
|
+
if (updateFrame) {
|
|
385
|
+
const baseSize = getObjectFitSize(
|
|
386
|
+
"contain",
|
|
387
|
+
{ width: meta.width, height: meta.height },
|
|
388
|
+
this.parentSize
|
|
389
|
+
);
|
|
390
|
+
this.frame = {
|
|
391
|
+
...this.frame,
|
|
392
|
+
size: [baseSize.width, baseSize.height]
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
this.mediaDuration = meta.duration;
|
|
396
|
+
}
|
|
397
|
+
setPosition(position) {
|
|
398
|
+
this.frame.x = position.x;
|
|
399
|
+
this.frame.y = position.y;
|
|
400
|
+
return this;
|
|
401
|
+
}
|
|
402
|
+
async setSrc(src) {
|
|
403
|
+
this.props.src = src;
|
|
404
|
+
await this.updateVideoMeta();
|
|
405
|
+
return this;
|
|
406
|
+
}
|
|
407
|
+
setMediaDuration(mediaDuration) {
|
|
408
|
+
this.mediaDuration = mediaDuration;
|
|
409
|
+
return this;
|
|
410
|
+
}
|
|
411
|
+
setParentSize(parentSize) {
|
|
412
|
+
this.parentSize = structuredClone(parentSize);
|
|
413
|
+
return this;
|
|
414
|
+
}
|
|
415
|
+
setObjectFit(objectFit) {
|
|
416
|
+
this.objectFit = objectFit;
|
|
417
|
+
return this;
|
|
418
|
+
}
|
|
419
|
+
setFrame(frame) {
|
|
420
|
+
this.frame = structuredClone(frame);
|
|
421
|
+
return this;
|
|
422
|
+
}
|
|
423
|
+
setPlay(play) {
|
|
424
|
+
this.props.play = play;
|
|
425
|
+
return this;
|
|
426
|
+
}
|
|
427
|
+
setPlaybackRate(playbackRate) {
|
|
428
|
+
this.props.playbackRate = playbackRate;
|
|
429
|
+
return this;
|
|
430
|
+
}
|
|
431
|
+
setStartAt(time) {
|
|
432
|
+
this.props.time = Math.max(0, time);
|
|
433
|
+
return this;
|
|
434
|
+
}
|
|
435
|
+
setMediaFilter(mediaFilter) {
|
|
436
|
+
this.props.mediaFilter = mediaFilter;
|
|
437
|
+
return this;
|
|
438
|
+
}
|
|
439
|
+
setVolume(volume) {
|
|
440
|
+
this.props.volume = volume;
|
|
441
|
+
return this;
|
|
442
|
+
}
|
|
443
|
+
setBackgroundColor(backgroundColor) {
|
|
444
|
+
this.backgroundColor = backgroundColor;
|
|
445
|
+
return this;
|
|
446
|
+
}
|
|
447
|
+
setProps(props) {
|
|
448
|
+
this.props = {
|
|
449
|
+
play: this.props.play,
|
|
450
|
+
...structuredClone(props),
|
|
451
|
+
src: this.props.src
|
|
452
|
+
};
|
|
453
|
+
return this;
|
|
454
|
+
}
|
|
455
|
+
setFrameEffects(frameEffects) {
|
|
456
|
+
this.frameEffects = frameEffects;
|
|
457
|
+
return this;
|
|
458
|
+
}
|
|
459
|
+
addFrameEffect(frameEffect) {
|
|
460
|
+
var _a;
|
|
461
|
+
(_a = this.frameEffects) == null ? void 0 : _a.push(frameEffect);
|
|
462
|
+
return this;
|
|
463
|
+
}
|
|
464
|
+
accept(visitor) {
|
|
465
|
+
return visitor.visitVideoElement(this);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
class AudioElement extends TrackElement {
|
|
469
|
+
constructor(src) {
|
|
470
|
+
super(TIMELINE_ELEMENT_TYPE.AUDIO);
|
|
471
|
+
__publicField(this, "mediaDuration");
|
|
472
|
+
this.props = {
|
|
473
|
+
src,
|
|
474
|
+
time: 0,
|
|
475
|
+
play: true,
|
|
476
|
+
playbackRate: 1,
|
|
477
|
+
volume: 1,
|
|
478
|
+
loop: false
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
getMediaDuration() {
|
|
482
|
+
return this.mediaDuration;
|
|
483
|
+
}
|
|
484
|
+
getStartAt() {
|
|
485
|
+
return this.props.time || 0;
|
|
486
|
+
}
|
|
487
|
+
async updateAudioMeta() {
|
|
488
|
+
this.mediaDuration = await getAudioDuration(this.props.src);
|
|
489
|
+
}
|
|
490
|
+
async setSrc(src) {
|
|
491
|
+
this.props.src = src;
|
|
492
|
+
await this.updateAudioMeta();
|
|
493
|
+
return this;
|
|
494
|
+
}
|
|
495
|
+
setMediaDuration(mediaDuration) {
|
|
496
|
+
this.mediaDuration = mediaDuration;
|
|
497
|
+
return this;
|
|
498
|
+
}
|
|
499
|
+
setVolume(volume) {
|
|
500
|
+
this.props.volume = volume;
|
|
501
|
+
return this;
|
|
502
|
+
}
|
|
503
|
+
setLoop(loop) {
|
|
504
|
+
this.props.loop = loop;
|
|
505
|
+
return this;
|
|
506
|
+
}
|
|
507
|
+
setStartAt(time) {
|
|
508
|
+
this.props.time = Math.max(0, time);
|
|
509
|
+
return this;
|
|
510
|
+
}
|
|
511
|
+
setPlaybackRate(playbackRate) {
|
|
512
|
+
this.props.playbackRate = playbackRate;
|
|
513
|
+
return this;
|
|
514
|
+
}
|
|
515
|
+
setProps(props) {
|
|
516
|
+
this.props = {
|
|
517
|
+
play: this.props.play,
|
|
518
|
+
...structuredClone(props),
|
|
519
|
+
src: this.props.src
|
|
520
|
+
};
|
|
521
|
+
return this;
|
|
522
|
+
}
|
|
523
|
+
accept(visitor) {
|
|
524
|
+
return visitor.visitAudioElement(this);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
class ImageElement extends TrackElement {
|
|
528
|
+
constructor(src, parentSize) {
|
|
529
|
+
super(TIMELINE_ELEMENT_TYPE.IMAGE);
|
|
530
|
+
__publicField(this, "backgroundColor");
|
|
531
|
+
__publicField(this, "parentSize");
|
|
532
|
+
__publicField(this, "objectFit");
|
|
533
|
+
__publicField(this, "frameEffects");
|
|
534
|
+
__publicField(this, "frame");
|
|
535
|
+
this.parentSize = parentSize;
|
|
536
|
+
this.objectFit = "cover";
|
|
537
|
+
this.frameEffects = [];
|
|
538
|
+
this.props = {
|
|
539
|
+
src,
|
|
540
|
+
mediaFilter: "none"
|
|
541
|
+
};
|
|
542
|
+
this.frame = {
|
|
543
|
+
x: 0,
|
|
544
|
+
y: 0
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
getParentSize() {
|
|
548
|
+
return this.parentSize;
|
|
549
|
+
}
|
|
550
|
+
getFrame() {
|
|
551
|
+
return this.frame;
|
|
552
|
+
}
|
|
553
|
+
getFrameEffects() {
|
|
554
|
+
return this.frameEffects;
|
|
555
|
+
}
|
|
556
|
+
getBackgroundColor() {
|
|
557
|
+
return this.backgroundColor;
|
|
558
|
+
}
|
|
559
|
+
getObjectFit() {
|
|
560
|
+
return this.objectFit;
|
|
561
|
+
}
|
|
562
|
+
getPosition() {
|
|
563
|
+
return {
|
|
564
|
+
x: this.frame.x ?? 0,
|
|
565
|
+
y: this.frame.y ?? 0
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
async updateImageMeta(updateFrame = true) {
|
|
569
|
+
const meta = await getImageDimensions(this.props.src);
|
|
570
|
+
if (updateFrame) {
|
|
571
|
+
const baseSize = getObjectFitSize(
|
|
572
|
+
"contain",
|
|
573
|
+
{ width: meta.width, height: meta.height },
|
|
574
|
+
this.parentSize
|
|
575
|
+
);
|
|
576
|
+
this.frame = {
|
|
577
|
+
size: [baseSize.width, baseSize.height],
|
|
578
|
+
...this.frame
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
setPosition(position) {
|
|
583
|
+
this.frame.x = position.x;
|
|
584
|
+
this.frame.y = position.y;
|
|
585
|
+
return this;
|
|
586
|
+
}
|
|
587
|
+
async setSrc(src) {
|
|
588
|
+
this.props.src = src;
|
|
589
|
+
await this.updateImageMeta();
|
|
590
|
+
return this;
|
|
591
|
+
}
|
|
592
|
+
setObjectFit(objectFit) {
|
|
593
|
+
this.objectFit = objectFit;
|
|
594
|
+
return this;
|
|
595
|
+
}
|
|
596
|
+
setFrame(frame) {
|
|
597
|
+
this.frame = structuredClone(frame);
|
|
598
|
+
return this;
|
|
599
|
+
}
|
|
600
|
+
setParentSize(parentSize) {
|
|
601
|
+
this.parentSize = structuredClone(parentSize);
|
|
602
|
+
return this;
|
|
603
|
+
}
|
|
604
|
+
setMediaFilter(mediaFilter) {
|
|
605
|
+
this.props.mediaFilter = mediaFilter;
|
|
606
|
+
return this;
|
|
607
|
+
}
|
|
608
|
+
setBackgroundColor(backgroundColor) {
|
|
609
|
+
this.backgroundColor = backgroundColor;
|
|
610
|
+
return this;
|
|
611
|
+
}
|
|
612
|
+
setProps(props) {
|
|
613
|
+
this.props = { ...structuredClone(props), src: this.props.src };
|
|
614
|
+
return this;
|
|
615
|
+
}
|
|
616
|
+
setFrameEffects(frameEffects) {
|
|
617
|
+
this.frameEffects = frameEffects;
|
|
618
|
+
return this;
|
|
619
|
+
}
|
|
620
|
+
addFrameEffect(frameEffect) {
|
|
621
|
+
var _a;
|
|
622
|
+
(_a = this.frameEffects) == null ? void 0 : _a.push(frameEffect);
|
|
623
|
+
return this;
|
|
624
|
+
}
|
|
625
|
+
accept(visitor) {
|
|
626
|
+
return visitor.visitImageElement(this);
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
class TextElement extends TrackElement {
|
|
630
|
+
constructor(text) {
|
|
631
|
+
super(TIMELINE_ELEMENT_TYPE.TEXT);
|
|
632
|
+
__publicField(this, "textEffect");
|
|
633
|
+
this.props = {
|
|
634
|
+
text,
|
|
635
|
+
fill: "#888888"
|
|
636
|
+
//default-grey
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
getTextEffect() {
|
|
640
|
+
return this.textEffect;
|
|
641
|
+
}
|
|
642
|
+
getText() {
|
|
643
|
+
return this.props.text;
|
|
644
|
+
}
|
|
645
|
+
getStrokeColor() {
|
|
646
|
+
return this.props.stroke;
|
|
647
|
+
}
|
|
648
|
+
getLineWidth() {
|
|
649
|
+
return this.props.lineWidth;
|
|
650
|
+
}
|
|
651
|
+
setText(text) {
|
|
652
|
+
this.props.text = text;
|
|
653
|
+
return this;
|
|
654
|
+
}
|
|
655
|
+
setFill(fill) {
|
|
656
|
+
this.props.fill = fill;
|
|
657
|
+
return this;
|
|
658
|
+
}
|
|
659
|
+
setRotation(rotation) {
|
|
660
|
+
this.props.rotation = rotation;
|
|
661
|
+
return this;
|
|
662
|
+
}
|
|
663
|
+
setFontSize(fontSize) {
|
|
664
|
+
this.props.fontSize = fontSize;
|
|
665
|
+
return this;
|
|
666
|
+
}
|
|
667
|
+
setFontFamily(fontFamily) {
|
|
668
|
+
this.props.fontFamily = fontFamily;
|
|
669
|
+
return this;
|
|
670
|
+
}
|
|
671
|
+
setFontWeight(fontWeight) {
|
|
672
|
+
this.props.fontWeight = fontWeight;
|
|
673
|
+
return this;
|
|
674
|
+
}
|
|
675
|
+
setFontStyle(fontStyle) {
|
|
676
|
+
this.props.fontStyle = fontStyle;
|
|
677
|
+
return this;
|
|
678
|
+
}
|
|
679
|
+
setTextEffect(textEffect) {
|
|
680
|
+
this.textEffect = textEffect;
|
|
681
|
+
return this;
|
|
682
|
+
}
|
|
683
|
+
setTextAlign(textAlign) {
|
|
684
|
+
this.props.textAlign = textAlign;
|
|
685
|
+
return this;
|
|
686
|
+
}
|
|
687
|
+
setStrokeColor(stroke) {
|
|
688
|
+
this.props.stroke = stroke;
|
|
689
|
+
return this;
|
|
690
|
+
}
|
|
691
|
+
setLineWidth(lineWidth) {
|
|
692
|
+
this.props.lineWidth = lineWidth;
|
|
693
|
+
return this;
|
|
694
|
+
}
|
|
695
|
+
accept(visitor) {
|
|
696
|
+
return visitor.visitTextElement(this);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
class CaptionElement extends TrackElement {
|
|
700
|
+
constructor(t, start, end) {
|
|
701
|
+
super(TIMELINE_ELEMENT_TYPE.CAPTION);
|
|
702
|
+
__publicField(this, "t");
|
|
703
|
+
this.t = t;
|
|
704
|
+
this.s = start;
|
|
705
|
+
this.e = end;
|
|
706
|
+
}
|
|
707
|
+
getText() {
|
|
708
|
+
return this.t;
|
|
709
|
+
}
|
|
710
|
+
setText(t) {
|
|
711
|
+
this.t = t;
|
|
712
|
+
return this;
|
|
713
|
+
}
|
|
714
|
+
accept(visitor) {
|
|
715
|
+
return visitor.visitCaptionElement(this);
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
class IconElement extends TrackElement {
|
|
719
|
+
constructor(src, size) {
|
|
720
|
+
super(TIMELINE_ELEMENT_TYPE.ICON);
|
|
721
|
+
this.props = {
|
|
722
|
+
src,
|
|
723
|
+
size
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
accept(visitor) {
|
|
727
|
+
return visitor.visitIconElement(this);
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
class CircleElement extends TrackElement {
|
|
731
|
+
constructor(fill, radius) {
|
|
732
|
+
super(TIMELINE_ELEMENT_TYPE.CIRCLE);
|
|
733
|
+
this.props = {
|
|
734
|
+
radius,
|
|
735
|
+
fill
|
|
736
|
+
};
|
|
737
|
+
}
|
|
738
|
+
getFill() {
|
|
739
|
+
return this.props.fill;
|
|
740
|
+
}
|
|
741
|
+
getRadius() {
|
|
742
|
+
return this.props.radius;
|
|
743
|
+
}
|
|
744
|
+
setFill(fill) {
|
|
745
|
+
this.props.fill = fill;
|
|
746
|
+
return this;
|
|
747
|
+
}
|
|
748
|
+
setRadius(radius) {
|
|
749
|
+
this.props.radius = radius;
|
|
750
|
+
return this;
|
|
751
|
+
}
|
|
752
|
+
accept(visitor) {
|
|
753
|
+
return visitor.visitCircleElement(this);
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
class RectElement extends TrackElement {
|
|
757
|
+
constructor(fill, size) {
|
|
758
|
+
super(TIMELINE_ELEMENT_TYPE.RECT);
|
|
759
|
+
this.props = {
|
|
760
|
+
width: size.width,
|
|
761
|
+
height: size.height,
|
|
762
|
+
fill
|
|
763
|
+
};
|
|
764
|
+
}
|
|
765
|
+
setFill(fill) {
|
|
766
|
+
this.props.fill = fill;
|
|
767
|
+
return this;
|
|
768
|
+
}
|
|
769
|
+
setSize(size) {
|
|
770
|
+
this.props.width = size.width;
|
|
771
|
+
this.props.height = size.height;
|
|
772
|
+
return this;
|
|
773
|
+
}
|
|
774
|
+
accept(visitor) {
|
|
775
|
+
return visitor.visitRectElement(this);
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
class ElementAnimation {
|
|
779
|
+
constructor(name) {
|
|
780
|
+
__publicField(this, "name");
|
|
781
|
+
__publicField(this, "interval");
|
|
782
|
+
__publicField(this, "intensity");
|
|
783
|
+
__publicField(this, "animate");
|
|
784
|
+
__publicField(this, "mode");
|
|
785
|
+
__publicField(this, "direction");
|
|
786
|
+
this.name = name;
|
|
787
|
+
}
|
|
788
|
+
getName() {
|
|
789
|
+
return this.name;
|
|
790
|
+
}
|
|
791
|
+
getInterval() {
|
|
792
|
+
return this.interval;
|
|
793
|
+
}
|
|
794
|
+
getIntensity() {
|
|
795
|
+
return this.intensity;
|
|
796
|
+
}
|
|
797
|
+
getAnimate() {
|
|
798
|
+
return this.animate;
|
|
799
|
+
}
|
|
800
|
+
getMode() {
|
|
801
|
+
return this.mode;
|
|
802
|
+
}
|
|
803
|
+
getDirection() {
|
|
804
|
+
return this.direction;
|
|
805
|
+
}
|
|
806
|
+
setInterval(interval) {
|
|
807
|
+
this.interval = interval;
|
|
808
|
+
}
|
|
809
|
+
setIntensity(intensity) {
|
|
810
|
+
this.intensity = intensity;
|
|
811
|
+
}
|
|
812
|
+
setAnimate(animate) {
|
|
813
|
+
this.animate = animate;
|
|
814
|
+
}
|
|
815
|
+
setMode(mode) {
|
|
816
|
+
this.mode = mode;
|
|
817
|
+
}
|
|
818
|
+
setDirection(direction) {
|
|
819
|
+
this.direction = direction;
|
|
820
|
+
}
|
|
821
|
+
toJSON() {
|
|
822
|
+
return {
|
|
823
|
+
name: this.name,
|
|
824
|
+
interval: this.interval,
|
|
825
|
+
intensity: this.intensity,
|
|
826
|
+
animate: this.animate,
|
|
827
|
+
mode: this.mode,
|
|
828
|
+
direction: this.direction
|
|
829
|
+
};
|
|
830
|
+
}
|
|
831
|
+
static fromJSON(json) {
|
|
832
|
+
const animation = new ElementAnimation(json.name);
|
|
833
|
+
animation.setInterval(json.interval);
|
|
834
|
+
animation.setIntensity(json.intensity);
|
|
835
|
+
animation.setAnimate(json.animate);
|
|
836
|
+
animation.setMode(json.mode);
|
|
837
|
+
animation.setDirection(json.direction);
|
|
838
|
+
return animation;
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
class ElementFrameEffect {
|
|
842
|
+
constructor(start, end) {
|
|
843
|
+
__publicField(this, "s");
|
|
844
|
+
__publicField(this, "e");
|
|
845
|
+
__publicField(this, "props");
|
|
846
|
+
this.s = start;
|
|
847
|
+
this.e = end;
|
|
848
|
+
}
|
|
849
|
+
setProps(props) {
|
|
850
|
+
this.props = props;
|
|
851
|
+
}
|
|
852
|
+
getProps() {
|
|
853
|
+
return this.props;
|
|
854
|
+
}
|
|
855
|
+
getStart() {
|
|
856
|
+
return this.s;
|
|
857
|
+
}
|
|
858
|
+
getEnd() {
|
|
859
|
+
return this.e;
|
|
860
|
+
}
|
|
861
|
+
toJSON() {
|
|
862
|
+
return {
|
|
863
|
+
s: this.s,
|
|
864
|
+
e: this.e,
|
|
865
|
+
props: this.props
|
|
866
|
+
};
|
|
867
|
+
}
|
|
868
|
+
static fromJSON(json) {
|
|
869
|
+
const effect = new ElementFrameEffect(json.s, json.e);
|
|
870
|
+
effect.setProps(json.props);
|
|
871
|
+
return effect;
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
class ElementTextEffect {
|
|
875
|
+
constructor(name) {
|
|
876
|
+
__publicField(this, "name");
|
|
877
|
+
__publicField(this, "duration");
|
|
878
|
+
__publicField(this, "delay");
|
|
879
|
+
__publicField(this, "bufferTime");
|
|
880
|
+
this.name = name;
|
|
881
|
+
}
|
|
882
|
+
getName() {
|
|
883
|
+
return this.name;
|
|
884
|
+
}
|
|
885
|
+
getDuration() {
|
|
886
|
+
return this.duration;
|
|
887
|
+
}
|
|
888
|
+
getDelay() {
|
|
889
|
+
return this.delay;
|
|
890
|
+
}
|
|
891
|
+
getBufferTime() {
|
|
892
|
+
return this.bufferTime;
|
|
893
|
+
}
|
|
894
|
+
setName(name) {
|
|
895
|
+
this.name = name;
|
|
896
|
+
}
|
|
897
|
+
setDuration(duration) {
|
|
898
|
+
this.duration = duration;
|
|
899
|
+
}
|
|
900
|
+
setDelay(delay) {
|
|
901
|
+
this.delay = delay;
|
|
902
|
+
}
|
|
903
|
+
setBufferTime(bufferTime) {
|
|
904
|
+
this.bufferTime = bufferTime;
|
|
905
|
+
}
|
|
906
|
+
toJSON() {
|
|
907
|
+
return {
|
|
908
|
+
name: this.name,
|
|
909
|
+
delay: this.delay,
|
|
910
|
+
duration: this.duration,
|
|
911
|
+
bufferTime: this.bufferTime
|
|
912
|
+
};
|
|
913
|
+
}
|
|
914
|
+
static fromJSON(json) {
|
|
915
|
+
const effect = new ElementTextEffect(json.name);
|
|
916
|
+
effect.setDelay(json.delay);
|
|
917
|
+
effect.setDuration(json.duration);
|
|
918
|
+
effect.setBufferTime(json.bufferTime);
|
|
919
|
+
return effect;
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
class ElementDeserializer {
|
|
923
|
+
static deserializeBaseElement(element, json) {
|
|
924
|
+
if (json.id) element.setId(json.id);
|
|
925
|
+
if (json.trackId) element.setTrackId(json.trackId);
|
|
926
|
+
if (json.s !== void 0) element.setStart(json.s);
|
|
927
|
+
if (json.e !== void 0) element.setEnd(json.e);
|
|
928
|
+
if (json.props) element.setProps(json.props);
|
|
929
|
+
if (json.animation) element.setAnimation(ElementAnimation.fromJSON(json.animation));
|
|
930
|
+
}
|
|
931
|
+
static deserializeVideoElement(json) {
|
|
932
|
+
var _a;
|
|
933
|
+
const parentSize = json.frame && json.frame.size ? { width: json.frame.size[0], height: json.frame.size[1] } : { width: 0, height: 0 };
|
|
934
|
+
const videoElement = new VideoElement(((_a = json.props) == null ? void 0 : _a.src) || "", parentSize);
|
|
935
|
+
ElementDeserializer.deserializeBaseElement(videoElement, json);
|
|
936
|
+
if (json.mediaDuration !== void 0) videoElement.setMediaDuration(json.mediaDuration);
|
|
937
|
+
if (json.objectFit) videoElement.setObjectFit(json.objectFit);
|
|
938
|
+
if (json.frame) videoElement.setFrame(json.frame);
|
|
939
|
+
if (json.frameEffects) videoElement.setFrameEffects(json.frameEffects.map((frameEffect) => ElementFrameEffect.fromJSON(frameEffect)));
|
|
940
|
+
if (json.backgroundColor) videoElement.setBackgroundColor(json.backgroundColor);
|
|
941
|
+
return videoElement;
|
|
942
|
+
}
|
|
943
|
+
static deserializeAudioElement(json) {
|
|
944
|
+
var _a;
|
|
945
|
+
const audioElement = new AudioElement(((_a = json.props) == null ? void 0 : _a.src) || "");
|
|
946
|
+
ElementDeserializer.deserializeBaseElement(audioElement, json);
|
|
947
|
+
if (json.mediaDuration !== void 0) audioElement.setMediaDuration(json.mediaDuration);
|
|
948
|
+
return audioElement;
|
|
949
|
+
}
|
|
950
|
+
static deserializeImageElement(json) {
|
|
951
|
+
var _a;
|
|
952
|
+
const parentSize = json.frame && json.frame.size ? { width: json.frame.size[0], height: json.frame.size[1] } : { width: 0, height: 0 };
|
|
953
|
+
const imageElement = new ImageElement(((_a = json.props) == null ? void 0 : _a.src) || "", parentSize);
|
|
954
|
+
ElementDeserializer.deserializeBaseElement(imageElement, json);
|
|
955
|
+
if (json.objectFit) imageElement.setObjectFit(json.objectFit);
|
|
956
|
+
if (json.frame) imageElement.setFrame(json.frame);
|
|
957
|
+
if (json.frameEffects) imageElement.setFrameEffects(json.frameEffects.map((frameEffect) => ElementFrameEffect.fromJSON(frameEffect)));
|
|
958
|
+
if (json.backgroundColor) imageElement.setBackgroundColor(json.backgroundColor);
|
|
959
|
+
return imageElement;
|
|
960
|
+
}
|
|
961
|
+
static deserializeTextElement(json) {
|
|
962
|
+
var _a;
|
|
963
|
+
const textElement = new TextElement(((_a = json.props) == null ? void 0 : _a.text) || "");
|
|
964
|
+
ElementDeserializer.deserializeBaseElement(textElement, json);
|
|
965
|
+
if (json.textEffect) textElement.setTextEffect(ElementTextEffect.fromJSON(json.textEffect));
|
|
966
|
+
return textElement;
|
|
967
|
+
}
|
|
968
|
+
static deserializeCaptionElement(json) {
|
|
969
|
+
const captionElement = new CaptionElement(
|
|
970
|
+
json.t || "",
|
|
971
|
+
json.s || 0,
|
|
972
|
+
json.e || 0
|
|
973
|
+
);
|
|
974
|
+
ElementDeserializer.deserializeBaseElement(captionElement, json);
|
|
975
|
+
return captionElement;
|
|
976
|
+
}
|
|
977
|
+
static deserializeIconElement(json) {
|
|
978
|
+
var _a, _b;
|
|
979
|
+
const size = ((_a = json.props) == null ? void 0 : _a.size) ? { width: json.props.size[0], height: json.props.size[1] } : { width: 0, height: 0 };
|
|
980
|
+
const iconElement = new IconElement(
|
|
981
|
+
((_b = json.props) == null ? void 0 : _b.src) || "",
|
|
982
|
+
size
|
|
983
|
+
);
|
|
984
|
+
ElementDeserializer.deserializeBaseElement(iconElement, json);
|
|
985
|
+
return iconElement;
|
|
986
|
+
}
|
|
987
|
+
static deserializeCircleElement(json) {
|
|
988
|
+
var _a, _b;
|
|
989
|
+
const circleElement = new CircleElement(
|
|
990
|
+
((_a = json.props) == null ? void 0 : _a.fill) || "",
|
|
991
|
+
((_b = json.props) == null ? void 0 : _b.radius) || 0
|
|
992
|
+
);
|
|
993
|
+
ElementDeserializer.deserializeBaseElement(circleElement, json);
|
|
994
|
+
return circleElement;
|
|
995
|
+
}
|
|
996
|
+
static deserializeRectElement(json) {
|
|
997
|
+
var _a, _b, _c;
|
|
998
|
+
const rectElement = new RectElement(
|
|
999
|
+
((_a = json.props) == null ? void 0 : _a.fill) || "",
|
|
1000
|
+
{
|
|
1001
|
+
width: ((_b = json.props) == null ? void 0 : _b.width) || 0,
|
|
1002
|
+
height: ((_c = json.props) == null ? void 0 : _c.height) || 0
|
|
1003
|
+
}
|
|
1004
|
+
);
|
|
1005
|
+
ElementDeserializer.deserializeBaseElement(rectElement, json);
|
|
1006
|
+
return rectElement;
|
|
1007
|
+
}
|
|
1008
|
+
static fromJSON(json) {
|
|
1009
|
+
try {
|
|
1010
|
+
switch (json.type) {
|
|
1011
|
+
case "video":
|
|
1012
|
+
return ElementDeserializer.deserializeVideoElement(json);
|
|
1013
|
+
case "audio":
|
|
1014
|
+
return ElementDeserializer.deserializeAudioElement(json);
|
|
1015
|
+
case "image":
|
|
1016
|
+
return ElementDeserializer.deserializeImageElement(json);
|
|
1017
|
+
case "text":
|
|
1018
|
+
return ElementDeserializer.deserializeTextElement(json);
|
|
1019
|
+
case "caption":
|
|
1020
|
+
return ElementDeserializer.deserializeCaptionElement(json);
|
|
1021
|
+
case "icon":
|
|
1022
|
+
return ElementDeserializer.deserializeIconElement(json);
|
|
1023
|
+
case "circle":
|
|
1024
|
+
return ElementDeserializer.deserializeCircleElement(json);
|
|
1025
|
+
case "rect":
|
|
1026
|
+
return ElementDeserializer.deserializeRectElement(json);
|
|
1027
|
+
default:
|
|
1028
|
+
throw new Error(`Unknown element type: ${json.type}`);
|
|
1029
|
+
}
|
|
1030
|
+
} catch (error) {
|
|
1031
|
+
console.error("Error deserializing element:", error);
|
|
1032
|
+
return null;
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
class ElementSerializer {
|
|
1037
|
+
serializeElement(element) {
|
|
1038
|
+
var _a;
|
|
1039
|
+
return {
|
|
1040
|
+
id: element.getId(),
|
|
1041
|
+
trackId: element.getTrackId(),
|
|
1042
|
+
type: element.getType(),
|
|
1043
|
+
name: element.getName(),
|
|
1044
|
+
s: element.getStart(),
|
|
1045
|
+
e: element.getEnd(),
|
|
1046
|
+
props: structuredClone(element.getProps()),
|
|
1047
|
+
animation: (_a = element.getAnimation()) == null ? void 0 : _a.toJSON()
|
|
1048
|
+
};
|
|
1049
|
+
}
|
|
1050
|
+
visitVideoElement(element) {
|
|
1051
|
+
var _a;
|
|
1052
|
+
return {
|
|
1053
|
+
...this.serializeElement(element),
|
|
1054
|
+
frame: structuredClone(element.getFrame()),
|
|
1055
|
+
frameEffects: (_a = element.getFrameEffects()) == null ? void 0 : _a.map((frameEffect) => frameEffect.toJSON()),
|
|
1056
|
+
backgroundColor: element.getBackgroundColor(),
|
|
1057
|
+
objectFit: element.getObjectFit(),
|
|
1058
|
+
mediaDuration: element.getMediaDuration()
|
|
1059
|
+
};
|
|
1060
|
+
}
|
|
1061
|
+
visitAudioElement(element) {
|
|
1062
|
+
return {
|
|
1063
|
+
...this.serializeElement(element),
|
|
1064
|
+
mediaDuration: element.getMediaDuration()
|
|
1065
|
+
};
|
|
1066
|
+
}
|
|
1067
|
+
visitImageElement(element) {
|
|
1068
|
+
var _a;
|
|
1069
|
+
return {
|
|
1070
|
+
...this.serializeElement(element),
|
|
1071
|
+
frame: structuredClone(element.getFrame()),
|
|
1072
|
+
frameEffects: (_a = element.getFrameEffects()) == null ? void 0 : _a.map((frameEffect) => frameEffect.toJSON()),
|
|
1073
|
+
backgroundColor: element.getBackgroundColor(),
|
|
1074
|
+
objectFit: element.getObjectFit()
|
|
1075
|
+
};
|
|
1076
|
+
}
|
|
1077
|
+
visitTextElement(element) {
|
|
1078
|
+
var _a;
|
|
1079
|
+
return {
|
|
1080
|
+
...this.serializeElement(element),
|
|
1081
|
+
textEffect: (_a = element.getTextEffect()) == null ? void 0 : _a.toJSON()
|
|
1082
|
+
};
|
|
1083
|
+
}
|
|
1084
|
+
visitCaptionElement(element) {
|
|
1085
|
+
return {
|
|
1086
|
+
...this.serializeElement(element),
|
|
1087
|
+
t: structuredClone(element.getText())
|
|
1088
|
+
};
|
|
1089
|
+
}
|
|
1090
|
+
visitIconElement(element) {
|
|
1091
|
+
return {
|
|
1092
|
+
...this.serializeElement(element)
|
|
1093
|
+
};
|
|
1094
|
+
}
|
|
1095
|
+
visitCircleElement(element) {
|
|
1096
|
+
return {
|
|
1097
|
+
...this.serializeElement(element)
|
|
1098
|
+
};
|
|
1099
|
+
}
|
|
1100
|
+
visitRectElement(element) {
|
|
1101
|
+
return {
|
|
1102
|
+
...this.serializeElement(element)
|
|
1103
|
+
};
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
class ValidationError extends Error {
|
|
1107
|
+
constructor(message, errors, warnings = []) {
|
|
1108
|
+
super(message);
|
|
1109
|
+
this.errors = errors;
|
|
1110
|
+
this.warnings = warnings;
|
|
1111
|
+
this.name = "ValidationError";
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
class ElementValidator {
|
|
1115
|
+
validateBasicProperties(element) {
|
|
1116
|
+
const errors = [];
|
|
1117
|
+
const warnings = [];
|
|
1118
|
+
if (!element.getId()) {
|
|
1119
|
+
errors.push("Element must have an ID");
|
|
1120
|
+
}
|
|
1121
|
+
if (!element.getType()) {
|
|
1122
|
+
errors.push("Element must have a type");
|
|
1123
|
+
}
|
|
1124
|
+
if (element.getStart() === void 0 || element.getStart() === null) {
|
|
1125
|
+
errors.push("Element must have a start time (s)");
|
|
1126
|
+
}
|
|
1127
|
+
if (element.getEnd() === void 0 || element.getEnd() === null) {
|
|
1128
|
+
errors.push("Element must have an end time (e)");
|
|
1129
|
+
}
|
|
1130
|
+
if (element.getStart() !== void 0 && element.getEnd() !== void 0) {
|
|
1131
|
+
if (element.getStart() < 0) {
|
|
1132
|
+
errors.push("Start time cannot be negative");
|
|
1133
|
+
}
|
|
1134
|
+
if (element.getEnd() <= element.getStart()) {
|
|
1135
|
+
errors.push("End time must be greater than start time");
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
if (!element.getName()) {
|
|
1139
|
+
warnings.push("Element should have a name for better identification");
|
|
1140
|
+
}
|
|
1141
|
+
if (!element.getTrackId()) {
|
|
1142
|
+
warnings.push("Element should have a track Id");
|
|
1143
|
+
}
|
|
1144
|
+
return { errors, warnings };
|
|
1145
|
+
}
|
|
1146
|
+
validateTextElement(element) {
|
|
1147
|
+
const basicValidation = this.validateBasicProperties(element);
|
|
1148
|
+
const errors = [...basicValidation.errors];
|
|
1149
|
+
const warnings = [...basicValidation.warnings];
|
|
1150
|
+
const props = element.getProps();
|
|
1151
|
+
if (!(props == null ? void 0 : props.text)) {
|
|
1152
|
+
errors.push("Text element must have text content");
|
|
1153
|
+
}
|
|
1154
|
+
if ((props == null ? void 0 : props.fontSize) !== void 0 && props.fontSize <= 0) {
|
|
1155
|
+
errors.push("Font size must be greater than 0");
|
|
1156
|
+
}
|
|
1157
|
+
if ((props == null ? void 0 : props.fontWeight) !== void 0 && props.fontWeight < 0) {
|
|
1158
|
+
errors.push("Font weight cannot be negative");
|
|
1159
|
+
}
|
|
1160
|
+
return { errors, warnings };
|
|
1161
|
+
}
|
|
1162
|
+
validateVideoElement(element) {
|
|
1163
|
+
const basicValidation = this.validateBasicProperties(element);
|
|
1164
|
+
const errors = [...basicValidation.errors];
|
|
1165
|
+
const warnings = [...basicValidation.warnings];
|
|
1166
|
+
const props = element.getProps();
|
|
1167
|
+
if (!(props == null ? void 0 : props.src)) {
|
|
1168
|
+
errors.push("Video element must have a source URL");
|
|
1169
|
+
}
|
|
1170
|
+
if ((props == null ? void 0 : props.volume) !== void 0 && (props.volume < 0 || props.volume > 1)) {
|
|
1171
|
+
errors.push("Volume must be between 0 and 1");
|
|
1172
|
+
}
|
|
1173
|
+
if ((props == null ? void 0 : props.playbackRate) !== void 0 && props.playbackRate <= 0) {
|
|
1174
|
+
errors.push("Playback rate must be greater than 0");
|
|
1175
|
+
}
|
|
1176
|
+
return { errors, warnings };
|
|
1177
|
+
}
|
|
1178
|
+
validateAudioElement(element) {
|
|
1179
|
+
const basicValidation = this.validateBasicProperties(element);
|
|
1180
|
+
const errors = [...basicValidation.errors];
|
|
1181
|
+
const warnings = [...basicValidation.warnings];
|
|
1182
|
+
const props = element.getProps();
|
|
1183
|
+
if (!(props == null ? void 0 : props.src)) {
|
|
1184
|
+
errors.push("Audio element must have a source URL");
|
|
1185
|
+
}
|
|
1186
|
+
if ((props == null ? void 0 : props.volume) !== void 0 && (props.volume < 0 || props.volume > 1)) {
|
|
1187
|
+
errors.push("Volume must be between 0 and 1");
|
|
1188
|
+
}
|
|
1189
|
+
if ((props == null ? void 0 : props.playbackRate) !== void 0 && props.playbackRate <= 0) {
|
|
1190
|
+
errors.push("Playback rate must be greater than 0");
|
|
1191
|
+
}
|
|
1192
|
+
return { errors, warnings };
|
|
1193
|
+
}
|
|
1194
|
+
validateImageElement(element) {
|
|
1195
|
+
const basicValidation = this.validateBasicProperties(element);
|
|
1196
|
+
const errors = [...basicValidation.errors];
|
|
1197
|
+
const warnings = [...basicValidation.warnings];
|
|
1198
|
+
const props = element.getProps();
|
|
1199
|
+
if (!(props == null ? void 0 : props.src)) {
|
|
1200
|
+
errors.push("Image element must have a source URL");
|
|
1201
|
+
}
|
|
1202
|
+
return { errors, warnings };
|
|
1203
|
+
}
|
|
1204
|
+
validateCaptionElement(element) {
|
|
1205
|
+
const basicValidation = this.validateBasicProperties(element);
|
|
1206
|
+
const errors = [...basicValidation.errors];
|
|
1207
|
+
const warnings = [...basicValidation.warnings];
|
|
1208
|
+
const props = element.getProps();
|
|
1209
|
+
if (!(props == null ? void 0 : props.text)) {
|
|
1210
|
+
errors.push("Caption element must have text content");
|
|
1211
|
+
}
|
|
1212
|
+
return { errors, warnings };
|
|
1213
|
+
}
|
|
1214
|
+
validateIconElement(element) {
|
|
1215
|
+
const basicValidation = this.validateBasicProperties(element);
|
|
1216
|
+
const errors = [...basicValidation.errors];
|
|
1217
|
+
const warnings = [...basicValidation.warnings];
|
|
1218
|
+
const props = element.getProps();
|
|
1219
|
+
if (!(props == null ? void 0 : props.icon)) {
|
|
1220
|
+
errors.push("Icon element must have an icon name");
|
|
1221
|
+
}
|
|
1222
|
+
return { errors, warnings };
|
|
1223
|
+
}
|
|
1224
|
+
validateCircleElement(element) {
|
|
1225
|
+
const basicValidation = this.validateBasicProperties(element);
|
|
1226
|
+
const errors = [...basicValidation.errors];
|
|
1227
|
+
const warnings = [...basicValidation.warnings];
|
|
1228
|
+
const props = element.getProps();
|
|
1229
|
+
if ((props == null ? void 0 : props.radius) !== void 0 && props.radius <= 0) {
|
|
1230
|
+
errors.push("Circle radius must be greater than 0");
|
|
1231
|
+
}
|
|
1232
|
+
return { errors, warnings };
|
|
1233
|
+
}
|
|
1234
|
+
validateRectElement(element) {
|
|
1235
|
+
const basicValidation = this.validateBasicProperties(element);
|
|
1236
|
+
const errors = [...basicValidation.errors];
|
|
1237
|
+
const warnings = [...basicValidation.warnings];
|
|
1238
|
+
const props = element.getProps();
|
|
1239
|
+
if ((props == null ? void 0 : props.width) !== void 0 && props.width <= 0) {
|
|
1240
|
+
errors.push("Rectangle width must be greater than 0");
|
|
1241
|
+
}
|
|
1242
|
+
if ((props == null ? void 0 : props.height) !== void 0 && props.height <= 0) {
|
|
1243
|
+
errors.push("Rectangle height must be greater than 0");
|
|
1244
|
+
}
|
|
1245
|
+
return { errors, warnings };
|
|
1246
|
+
}
|
|
1247
|
+
visitVideoElement(element) {
|
|
1248
|
+
const validation = this.validateVideoElement(element);
|
|
1249
|
+
if (validation.errors.length > 0) {
|
|
1250
|
+
throw new ValidationError(
|
|
1251
|
+
`Video element validation failed: ${validation.errors.join(", ")}`,
|
|
1252
|
+
validation.errors,
|
|
1253
|
+
validation.warnings
|
|
1254
|
+
);
|
|
1255
|
+
}
|
|
1256
|
+
return true;
|
|
1257
|
+
}
|
|
1258
|
+
visitAudioElement(element) {
|
|
1259
|
+
const validation = this.validateAudioElement(element);
|
|
1260
|
+
if (validation.errors.length > 0) {
|
|
1261
|
+
throw new ValidationError(
|
|
1262
|
+
`Audio element validation failed: ${validation.errors.join(", ")}`,
|
|
1263
|
+
validation.errors,
|
|
1264
|
+
validation.warnings
|
|
1265
|
+
);
|
|
1266
|
+
}
|
|
1267
|
+
return true;
|
|
1268
|
+
}
|
|
1269
|
+
visitImageElement(element) {
|
|
1270
|
+
const validation = this.validateImageElement(element);
|
|
1271
|
+
if (validation.errors.length > 0) {
|
|
1272
|
+
throw new ValidationError(
|
|
1273
|
+
`Image element validation failed: ${validation.errors.join(", ")}`,
|
|
1274
|
+
validation.errors,
|
|
1275
|
+
validation.warnings
|
|
1276
|
+
);
|
|
1277
|
+
}
|
|
1278
|
+
return true;
|
|
1279
|
+
}
|
|
1280
|
+
visitTextElement(element) {
|
|
1281
|
+
const validation = this.validateTextElement(element);
|
|
1282
|
+
if (validation.errors.length > 0) {
|
|
1283
|
+
throw new ValidationError(
|
|
1284
|
+
`Text element validation failed: ${validation.errors.join(", ")}`,
|
|
1285
|
+
validation.errors,
|
|
1286
|
+
validation.warnings
|
|
1287
|
+
);
|
|
1288
|
+
}
|
|
1289
|
+
return true;
|
|
1290
|
+
}
|
|
1291
|
+
visitCaptionElement(element) {
|
|
1292
|
+
const validation = this.validateCaptionElement(element);
|
|
1293
|
+
if (validation.errors.length > 0) {
|
|
1294
|
+
throw new ValidationError(
|
|
1295
|
+
`Caption element validation failed: ${validation.errors.join(", ")}`,
|
|
1296
|
+
validation.errors,
|
|
1297
|
+
validation.warnings
|
|
1298
|
+
);
|
|
1299
|
+
}
|
|
1300
|
+
return true;
|
|
1301
|
+
}
|
|
1302
|
+
visitIconElement(element) {
|
|
1303
|
+
const validation = this.validateIconElement(element);
|
|
1304
|
+
if (validation.errors.length > 0) {
|
|
1305
|
+
throw new ValidationError(
|
|
1306
|
+
`Icon element validation failed: ${validation.errors.join(", ")}`,
|
|
1307
|
+
validation.errors,
|
|
1308
|
+
validation.warnings
|
|
1309
|
+
);
|
|
1310
|
+
}
|
|
1311
|
+
return true;
|
|
1312
|
+
}
|
|
1313
|
+
visitCircleElement(element) {
|
|
1314
|
+
const validation = this.validateCircleElement(element);
|
|
1315
|
+
if (validation.errors.length > 0) {
|
|
1316
|
+
throw new ValidationError(
|
|
1317
|
+
`Circle element validation failed: ${validation.errors.join(", ")}`,
|
|
1318
|
+
validation.errors,
|
|
1319
|
+
validation.warnings
|
|
1320
|
+
);
|
|
1321
|
+
}
|
|
1322
|
+
return true;
|
|
1323
|
+
}
|
|
1324
|
+
visitRectElement(element) {
|
|
1325
|
+
const validation = this.validateRectElement(element);
|
|
1326
|
+
if (validation.errors.length > 0) {
|
|
1327
|
+
throw new ValidationError(
|
|
1328
|
+
`Rectangle element validation failed: ${validation.errors.join(", ")}`,
|
|
1329
|
+
validation.errors,
|
|
1330
|
+
validation.warnings
|
|
1331
|
+
);
|
|
1332
|
+
}
|
|
1333
|
+
return true;
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
class TrackFriend {
|
|
1337
|
+
constructor(track) {
|
|
1338
|
+
this.track = track;
|
|
1339
|
+
}
|
|
1340
|
+
/**
|
|
1341
|
+
* Add an element to the track with validation
|
|
1342
|
+
* @param element The element to add
|
|
1343
|
+
* @param skipValidation If true, skips validation (use with caution)
|
|
1344
|
+
* @returns true if element was added successfully, throws ValidationError if validation fails
|
|
1345
|
+
*/
|
|
1346
|
+
addElement(element, skipValidation = false) {
|
|
1347
|
+
return this.track.addElementViaFriend(element, skipValidation);
|
|
1348
|
+
}
|
|
1349
|
+
/**
|
|
1350
|
+
* Remove an element from the track
|
|
1351
|
+
* @param element The element to remove
|
|
1352
|
+
*/
|
|
1353
|
+
removeElement(element) {
|
|
1354
|
+
this.track.removeElementViaFriend(element);
|
|
1355
|
+
}
|
|
1356
|
+
/**
|
|
1357
|
+
* Update an element in the track with validation
|
|
1358
|
+
* @param element The element to update
|
|
1359
|
+
* @returns true if element was updated successfully, throws ValidationError if validation fails
|
|
1360
|
+
*/
|
|
1361
|
+
updateElement(element) {
|
|
1362
|
+
return this.track.updateElementViaFriend(element);
|
|
1363
|
+
}
|
|
1364
|
+
/**
|
|
1365
|
+
* Get the track instance (for advanced operations)
|
|
1366
|
+
* @returns The track instance
|
|
1367
|
+
*/
|
|
1368
|
+
getTrack() {
|
|
1369
|
+
return this.track;
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
class Track {
|
|
1373
|
+
constructor(name, id) {
|
|
1374
|
+
__publicField(this, "id");
|
|
1375
|
+
__publicField(this, "name");
|
|
1376
|
+
__publicField(this, "type");
|
|
1377
|
+
__publicField(this, "elements");
|
|
1378
|
+
__publicField(this, "validator");
|
|
1379
|
+
this.name = name;
|
|
1380
|
+
this.id = id ?? `t-${generateShortUuid}`;
|
|
1381
|
+
this.type = "element";
|
|
1382
|
+
this.elements = [];
|
|
1383
|
+
this.validator = new ElementValidator();
|
|
1384
|
+
}
|
|
1385
|
+
/**
|
|
1386
|
+
* Create a friend instance for explicit access to protected methods
|
|
1387
|
+
* This implements the Friend Class Pattern
|
|
1388
|
+
* @returns TrackFriend instance
|
|
1389
|
+
*/
|
|
1390
|
+
createFriend() {
|
|
1391
|
+
return new TrackFriend(this);
|
|
1392
|
+
}
|
|
1393
|
+
/**
|
|
1394
|
+
* Friend method to add element (called by TrackFriend)
|
|
1395
|
+
* @param element The element to add
|
|
1396
|
+
* @param skipValidation If true, skips validation
|
|
1397
|
+
* @returns true if element was added successfully
|
|
1398
|
+
*/
|
|
1399
|
+
addElementViaFriend(element, skipValidation = false) {
|
|
1400
|
+
return this.addElement(element, skipValidation);
|
|
1401
|
+
}
|
|
1402
|
+
/**
|
|
1403
|
+
* Friend method to remove element (called by TrackFriend)
|
|
1404
|
+
* @param element The element to remove
|
|
1405
|
+
*/
|
|
1406
|
+
removeElementViaFriend(element) {
|
|
1407
|
+
this.removeElement(element);
|
|
1408
|
+
}
|
|
1409
|
+
/**
|
|
1410
|
+
* Friend method to update element (called by TrackFriend)
|
|
1411
|
+
* @param element The element to update
|
|
1412
|
+
* @returns true if element was updated successfully
|
|
1413
|
+
*/
|
|
1414
|
+
updateElementViaFriend(element) {
|
|
1415
|
+
return this.updateElement(element);
|
|
1416
|
+
}
|
|
1417
|
+
getId() {
|
|
1418
|
+
return this.id;
|
|
1419
|
+
}
|
|
1420
|
+
getName() {
|
|
1421
|
+
return this.name;
|
|
1422
|
+
}
|
|
1423
|
+
getType() {
|
|
1424
|
+
return this.type;
|
|
1425
|
+
}
|
|
1426
|
+
getElements() {
|
|
1427
|
+
return [...this.elements];
|
|
1428
|
+
}
|
|
1429
|
+
/**
|
|
1430
|
+
* Validates an element
|
|
1431
|
+
* @param element The element to validate
|
|
1432
|
+
* @returns true if valid, throws ValidationError if invalid
|
|
1433
|
+
*/
|
|
1434
|
+
validateElement(element) {
|
|
1435
|
+
return element.accept(this.validator);
|
|
1436
|
+
}
|
|
1437
|
+
getTrackDuration() {
|
|
1438
|
+
var _a;
|
|
1439
|
+
return ((_a = this.elements) == null ? void 0 : _a.length) ? this.elements[this.elements.length - 1].getEnd() : 0;
|
|
1440
|
+
}
|
|
1441
|
+
/**
|
|
1442
|
+
* Adds an element to the track with validation
|
|
1443
|
+
* @param element The element to add
|
|
1444
|
+
* @param skipValidation If true, skips validation (use with caution)
|
|
1445
|
+
* @returns true if element was added successfully, throws ValidationError if validation fails
|
|
1446
|
+
*/
|
|
1447
|
+
addElement(element, skipValidation = false) {
|
|
1448
|
+
element.setTrackId(this.id);
|
|
1449
|
+
if (skipValidation) {
|
|
1450
|
+
this.elements.push(element);
|
|
1451
|
+
return true;
|
|
1452
|
+
}
|
|
1453
|
+
try {
|
|
1454
|
+
const isValid = this.validateElement(element);
|
|
1455
|
+
if (isValid) {
|
|
1456
|
+
this.elements.push(element);
|
|
1457
|
+
return true;
|
|
1458
|
+
}
|
|
1459
|
+
} catch (error) {
|
|
1460
|
+
if (error instanceof ValidationError) {
|
|
1461
|
+
throw error;
|
|
1462
|
+
}
|
|
1463
|
+
throw error;
|
|
1464
|
+
}
|
|
1465
|
+
return false;
|
|
1466
|
+
}
|
|
1467
|
+
removeElement(element) {
|
|
1468
|
+
const index = this.elements.findIndex((e) => e.getId() === element.getId());
|
|
1469
|
+
if (index !== -1) {
|
|
1470
|
+
this.elements.splice(index, 1);
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
/**
|
|
1474
|
+
* Updates an element in the track with validation
|
|
1475
|
+
* @param element The element to update
|
|
1476
|
+
* @returns true if element was updated successfully, throws ValidationError if validation fails
|
|
1477
|
+
*/
|
|
1478
|
+
updateElement(element) {
|
|
1479
|
+
try {
|
|
1480
|
+
const isValid = this.validateElement(element);
|
|
1481
|
+
if (isValid) {
|
|
1482
|
+
const index = this.elements.findIndex(
|
|
1483
|
+
(e) => e.getId() === element.getId()
|
|
1484
|
+
);
|
|
1485
|
+
if (index !== -1) {
|
|
1486
|
+
this.elements[index] = element;
|
|
1487
|
+
return true;
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
} catch (error) {
|
|
1491
|
+
if (error instanceof ValidationError) {
|
|
1492
|
+
throw error;
|
|
1493
|
+
}
|
|
1494
|
+
throw error;
|
|
1495
|
+
}
|
|
1496
|
+
return false;
|
|
1497
|
+
}
|
|
1498
|
+
getElementById(id) {
|
|
1499
|
+
const element = this.elements.find((e) => e.getId() === id);
|
|
1500
|
+
if (!element) return void 0;
|
|
1501
|
+
return element;
|
|
1502
|
+
}
|
|
1503
|
+
/**
|
|
1504
|
+
* Validates all elements in the track and returns combined result and per-element status
|
|
1505
|
+
* @returns Object with overall isValid and array of per-element validation results
|
|
1506
|
+
*/
|
|
1507
|
+
validateAllElements() {
|
|
1508
|
+
let validResult = true;
|
|
1509
|
+
const results = this.elements.map((element) => {
|
|
1510
|
+
try {
|
|
1511
|
+
const isValid = this.validateElement(element);
|
|
1512
|
+
if (!isValid) {
|
|
1513
|
+
validResult = false;
|
|
1514
|
+
}
|
|
1515
|
+
return { element, isValid };
|
|
1516
|
+
} catch (error) {
|
|
1517
|
+
if (error instanceof ValidationError) {
|
|
1518
|
+
validResult = false;
|
|
1519
|
+
return {
|
|
1520
|
+
element,
|
|
1521
|
+
isValid: false,
|
|
1522
|
+
errors: error.errors,
|
|
1523
|
+
warnings: error.warnings
|
|
1524
|
+
};
|
|
1525
|
+
}
|
|
1526
|
+
return {
|
|
1527
|
+
element,
|
|
1528
|
+
isValid: false,
|
|
1529
|
+
errors: [error instanceof Error ? error.message : "Unknown error"]
|
|
1530
|
+
};
|
|
1531
|
+
}
|
|
1532
|
+
});
|
|
1533
|
+
return { isValid: validResult, results };
|
|
1534
|
+
}
|
|
1535
|
+
serialize() {
|
|
1536
|
+
const serializer = new ElementSerializer();
|
|
1537
|
+
return {
|
|
1538
|
+
id: this.id,
|
|
1539
|
+
name: this.name,
|
|
1540
|
+
type: this.type,
|
|
1541
|
+
elements: this.elements.map(
|
|
1542
|
+
(element) => element.accept(serializer)
|
|
1543
|
+
)
|
|
1544
|
+
};
|
|
1545
|
+
}
|
|
1546
|
+
static fromJSON(json) {
|
|
1547
|
+
const track = new Track(json.name, json.id);
|
|
1548
|
+
track.type = json.type;
|
|
1549
|
+
track.elements = (json.elements || []).map(ElementDeserializer.fromJSON);
|
|
1550
|
+
return track;
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
const _TimelineContextStore = class _TimelineContextStore {
|
|
1554
|
+
constructor() {
|
|
1555
|
+
__publicField(this, "storeMap");
|
|
1556
|
+
this.storeMap = /* @__PURE__ */ new Map();
|
|
1557
|
+
}
|
|
1558
|
+
static getInstance() {
|
|
1559
|
+
if (!_TimelineContextStore.instance) {
|
|
1560
|
+
_TimelineContextStore.instance = new _TimelineContextStore();
|
|
1561
|
+
}
|
|
1562
|
+
return _TimelineContextStore.instance;
|
|
1563
|
+
}
|
|
1564
|
+
initializeContext(contextId) {
|
|
1565
|
+
if (!this.storeMap.has(contextId)) {
|
|
1566
|
+
this.storeMap.set(contextId, {
|
|
1567
|
+
tracks: [],
|
|
1568
|
+
version: 0,
|
|
1569
|
+
elementMap: {},
|
|
1570
|
+
trackMap: {},
|
|
1571
|
+
captionProps: {}
|
|
1572
|
+
});
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
getTimelineData(contextId) {
|
|
1576
|
+
const timelineStore = this.storeMap.get(contextId);
|
|
1577
|
+
return timelineStore ? {
|
|
1578
|
+
tracks: timelineStore.tracks,
|
|
1579
|
+
version: timelineStore.version
|
|
1580
|
+
} : null;
|
|
1581
|
+
}
|
|
1582
|
+
setTimelineData(contextId, timelineData) {
|
|
1583
|
+
this.ensureContext(contextId);
|
|
1584
|
+
this.storeMap.get(contextId).tracks = timelineData.tracks;
|
|
1585
|
+
this.storeMap.get(contextId).version = timelineData.version;
|
|
1586
|
+
return timelineData;
|
|
1587
|
+
}
|
|
1588
|
+
getElementMap(contextId) {
|
|
1589
|
+
this.ensureContext(contextId);
|
|
1590
|
+
return this.storeMap.get(contextId).elementMap;
|
|
1591
|
+
}
|
|
1592
|
+
setElementMap(contextId, elementMap) {
|
|
1593
|
+
this.ensureContext(contextId);
|
|
1594
|
+
this.storeMap.get(contextId).elementMap = elementMap;
|
|
1595
|
+
}
|
|
1596
|
+
getTrackMap(contextId) {
|
|
1597
|
+
this.ensureContext(contextId);
|
|
1598
|
+
return this.storeMap.get(contextId).trackMap;
|
|
1599
|
+
}
|
|
1600
|
+
setTrackMap(contextId, trackMap) {
|
|
1601
|
+
this.ensureContext(contextId);
|
|
1602
|
+
this.storeMap.get(contextId).trackMap = trackMap;
|
|
1603
|
+
}
|
|
1604
|
+
getCaptionProps(contextId) {
|
|
1605
|
+
this.ensureContext(contextId);
|
|
1606
|
+
return this.storeMap.get(contextId).captionProps;
|
|
1607
|
+
}
|
|
1608
|
+
setCaptionProps(contextId, captionProps) {
|
|
1609
|
+
this.ensureContext(contextId);
|
|
1610
|
+
this.storeMap.get(contextId).captionProps = captionProps;
|
|
1611
|
+
}
|
|
1612
|
+
clearContext(contextId) {
|
|
1613
|
+
this.storeMap.delete(contextId);
|
|
1614
|
+
}
|
|
1615
|
+
ensureContext(contextId) {
|
|
1616
|
+
if (!this.storeMap.has(contextId)) {
|
|
1617
|
+
this.initializeContext(contextId);
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
};
|
|
1621
|
+
__publicField(_TimelineContextStore, "instance");
|
|
1622
|
+
let TimelineContextStore = _TimelineContextStore;
|
|
1623
|
+
const timelineContextStore = TimelineContextStore.getInstance();
|
|
1624
|
+
class ElementAdder {
|
|
1625
|
+
constructor(track) {
|
|
1626
|
+
__publicField(this, "track");
|
|
1627
|
+
__publicField(this, "trackFriend");
|
|
1628
|
+
this.track = track;
|
|
1629
|
+
this.trackFriend = track.createFriend();
|
|
1630
|
+
}
|
|
1631
|
+
async visitVideoElement(element) {
|
|
1632
|
+
await element.updateVideoMeta();
|
|
1633
|
+
const elements = this.track.getElements();
|
|
1634
|
+
const lastEndtime = (elements == null ? void 0 : elements.length) ? elements[elements.length - 1].getEnd() : 0;
|
|
1635
|
+
if (isNaN(element.getStart())) {
|
|
1636
|
+
element.setStart(lastEndtime);
|
|
1637
|
+
}
|
|
1638
|
+
if (isNaN(element.getEnd())) {
|
|
1639
|
+
element.setEnd(element.getStart() + element.getMediaDuration());
|
|
1640
|
+
}
|
|
1641
|
+
return this.trackFriend.addElement(element);
|
|
1642
|
+
}
|
|
1643
|
+
async visitAudioElement(element) {
|
|
1644
|
+
await element.updateAudioMeta();
|
|
1645
|
+
const elements = this.track.getElements();
|
|
1646
|
+
const lastEndtime = (elements == null ? void 0 : elements.length) ? elements[elements.length - 1].getEnd() : 0;
|
|
1647
|
+
if (isNaN(element.getStart())) {
|
|
1648
|
+
element.setStart(lastEndtime);
|
|
1649
|
+
}
|
|
1650
|
+
if (isNaN(element.getEnd())) {
|
|
1651
|
+
element.setEnd(element.getStart() + element.getMediaDuration());
|
|
1652
|
+
}
|
|
1653
|
+
return this.trackFriend.addElement(element);
|
|
1654
|
+
}
|
|
1655
|
+
async visitImageElement(element) {
|
|
1656
|
+
await element.updateImageMeta();
|
|
1657
|
+
const elements = this.track.getElements();
|
|
1658
|
+
const lastEndtime = (elements == null ? void 0 : elements.length) ? elements[elements.length - 1].getEnd() : 0;
|
|
1659
|
+
if (isNaN(element.getStart())) {
|
|
1660
|
+
element.setStart(lastEndtime);
|
|
1661
|
+
}
|
|
1662
|
+
if (isNaN(element.getEnd())) {
|
|
1663
|
+
element.setEnd(element.getStart() + 1);
|
|
1664
|
+
}
|
|
1665
|
+
return this.trackFriend.addElement(element);
|
|
1666
|
+
}
|
|
1667
|
+
async visitTextElement(element) {
|
|
1668
|
+
const elements = this.track.getElements();
|
|
1669
|
+
const lastEndtime = (elements == null ? void 0 : elements.length) ? elements[elements.length - 1].getEnd() : 0;
|
|
1670
|
+
if (isNaN(element.getStart())) {
|
|
1671
|
+
element.setStart(lastEndtime);
|
|
1672
|
+
}
|
|
1673
|
+
if (isNaN(element.getEnd())) {
|
|
1674
|
+
element.setEnd(element.getStart() + 1);
|
|
1675
|
+
}
|
|
1676
|
+
return this.trackFriend.addElement(element);
|
|
1677
|
+
}
|
|
1678
|
+
async visitCaptionElement(element) {
|
|
1679
|
+
const elements = this.track.getElements();
|
|
1680
|
+
const lastEndtime = (elements == null ? void 0 : elements.length) ? elements[elements.length - 1].getEnd() : 0;
|
|
1681
|
+
if (isNaN(element.getStart())) {
|
|
1682
|
+
element.setStart(lastEndtime);
|
|
1683
|
+
}
|
|
1684
|
+
if (isNaN(element.getEnd())) {
|
|
1685
|
+
element.setEnd(element.getStart() + 1);
|
|
1686
|
+
}
|
|
1687
|
+
return this.trackFriend.addElement(element);
|
|
1688
|
+
}
|
|
1689
|
+
async visitIconElement(element) {
|
|
1690
|
+
const elements = this.track.getElements();
|
|
1691
|
+
const lastEndtime = (elements == null ? void 0 : elements.length) ? elements[elements.length - 1].getEnd() : 0;
|
|
1692
|
+
if (isNaN(element.getStart())) {
|
|
1693
|
+
element.setStart(lastEndtime);
|
|
1694
|
+
}
|
|
1695
|
+
if (isNaN(element.getEnd())) {
|
|
1696
|
+
element.setEnd(element.getStart() + 1);
|
|
1697
|
+
}
|
|
1698
|
+
return this.trackFriend.addElement(element);
|
|
1699
|
+
}
|
|
1700
|
+
async visitCircleElement(element) {
|
|
1701
|
+
const elements = this.track.getElements();
|
|
1702
|
+
const lastEndtime = (elements == null ? void 0 : elements.length) ? elements[elements.length - 1].getEnd() : 0;
|
|
1703
|
+
if (isNaN(element.getStart())) {
|
|
1704
|
+
element.setStart(lastEndtime);
|
|
1705
|
+
}
|
|
1706
|
+
if (isNaN(element.getEnd())) {
|
|
1707
|
+
element.setEnd(element.getStart() + 1);
|
|
1708
|
+
}
|
|
1709
|
+
return this.trackFriend.addElement(element);
|
|
1710
|
+
}
|
|
1711
|
+
async visitRectElement(element) {
|
|
1712
|
+
const elements = this.track.getElements();
|
|
1713
|
+
const lastEndtime = (elements == null ? void 0 : elements.length) ? elements[elements.length - 1].getEnd() : 0;
|
|
1714
|
+
if (isNaN(element.getStart())) {
|
|
1715
|
+
element.setStart(lastEndtime);
|
|
1716
|
+
}
|
|
1717
|
+
if (isNaN(element.getEnd())) {
|
|
1718
|
+
element.setEnd(element.getStart() + 1);
|
|
1719
|
+
}
|
|
1720
|
+
return this.trackFriend.addElement(element);
|
|
1721
|
+
}
|
|
1722
|
+
}
|
|
1723
|
+
class ElementRemover {
|
|
1724
|
+
constructor(track) {
|
|
1725
|
+
__publicField(this, "trackFriend");
|
|
1726
|
+
this.trackFriend = track.createFriend();
|
|
1727
|
+
}
|
|
1728
|
+
visitVideoElement(element) {
|
|
1729
|
+
this.trackFriend.removeElement(element);
|
|
1730
|
+
return true;
|
|
1731
|
+
}
|
|
1732
|
+
visitAudioElement(element) {
|
|
1733
|
+
this.trackFriend.removeElement(element);
|
|
1734
|
+
return true;
|
|
1735
|
+
}
|
|
1736
|
+
visitImageElement(element) {
|
|
1737
|
+
this.trackFriend.removeElement(element);
|
|
1738
|
+
return true;
|
|
1739
|
+
}
|
|
1740
|
+
visitTextElement(element) {
|
|
1741
|
+
this.trackFriend.removeElement(element);
|
|
1742
|
+
return true;
|
|
1743
|
+
}
|
|
1744
|
+
visitCaptionElement(element) {
|
|
1745
|
+
this.trackFriend.removeElement(element);
|
|
1746
|
+
return true;
|
|
1747
|
+
}
|
|
1748
|
+
visitIconElement(element) {
|
|
1749
|
+
this.trackFriend.removeElement(element);
|
|
1750
|
+
return true;
|
|
1751
|
+
}
|
|
1752
|
+
visitCircleElement(element) {
|
|
1753
|
+
this.trackFriend.removeElement(element);
|
|
1754
|
+
return true;
|
|
1755
|
+
}
|
|
1756
|
+
visitRectElement(element) {
|
|
1757
|
+
this.trackFriend.removeElement(element);
|
|
1758
|
+
return true;
|
|
1759
|
+
}
|
|
1760
|
+
}
|
|
1761
|
+
class ElementUpdater {
|
|
1762
|
+
constructor(track) {
|
|
1763
|
+
__publicField(this, "trackFriend");
|
|
1764
|
+
this.trackFriend = track.createFriend();
|
|
1765
|
+
}
|
|
1766
|
+
visitVideoElement(element) {
|
|
1767
|
+
return this.trackFriend.updateElement(element);
|
|
1768
|
+
}
|
|
1769
|
+
visitAudioElement(element) {
|
|
1770
|
+
return this.trackFriend.updateElement(element);
|
|
1771
|
+
}
|
|
1772
|
+
visitImageElement(element) {
|
|
1773
|
+
return this.trackFriend.updateElement(element);
|
|
1774
|
+
}
|
|
1775
|
+
visitTextElement(element) {
|
|
1776
|
+
return this.trackFriend.updateElement(element);
|
|
1777
|
+
}
|
|
1778
|
+
visitCaptionElement(element) {
|
|
1779
|
+
return this.trackFriend.updateElement(element);
|
|
1780
|
+
}
|
|
1781
|
+
visitIconElement(element) {
|
|
1782
|
+
return this.trackFriend.updateElement(element);
|
|
1783
|
+
}
|
|
1784
|
+
visitCircleElement(element) {
|
|
1785
|
+
return this.trackFriend.updateElement(element);
|
|
1786
|
+
}
|
|
1787
|
+
visitRectElement(element) {
|
|
1788
|
+
return this.trackFriend.updateElement(element);
|
|
1789
|
+
}
|
|
1790
|
+
}
|
|
1791
|
+
class ElementCloner {
|
|
1792
|
+
cloneElementProperties(srcElement, destElement) {
|
|
1793
|
+
return destElement.setName(srcElement.getName()).setType(srcElement.getType()).setStart(srcElement.getStart()).setEnd(srcElement.getEnd()).setProps(srcElement.getProps()).setAnimation(srcElement.getAnimation());
|
|
1794
|
+
}
|
|
1795
|
+
visitVideoElement(element) {
|
|
1796
|
+
const props = element.getProps();
|
|
1797
|
+
const clonedElement = new VideoElement(props.src, element.getParentSize());
|
|
1798
|
+
this.cloneElementProperties(element, clonedElement);
|
|
1799
|
+
clonedElement.setParentSize(element.getParentSize()).setMediaDuration(element.getMediaDuration()).setFrame(element.getFrame()).setFrameEffects(element.getFrameEffects() ?? []).setBackgroundColor(element.getBackgroundColor()).setObjectFit(element.getObjectFit());
|
|
1800
|
+
return clonedElement;
|
|
1801
|
+
}
|
|
1802
|
+
visitAudioElement(element) {
|
|
1803
|
+
const clonedElement = new AudioElement(element.getProps().src);
|
|
1804
|
+
this.cloneElementProperties(element, clonedElement);
|
|
1805
|
+
clonedElement.setMediaDuration(element.getMediaDuration());
|
|
1806
|
+
return clonedElement;
|
|
1807
|
+
}
|
|
1808
|
+
visitImageElement(element) {
|
|
1809
|
+
const clonedElement = new ImageElement(
|
|
1810
|
+
element.getProps().src,
|
|
1811
|
+
element.getParentSize()
|
|
1812
|
+
);
|
|
1813
|
+
this.cloneElementProperties(element, clonedElement);
|
|
1814
|
+
clonedElement.setParentSize(element.getParentSize()).setFrame(element.getFrame()).setFrameEffects(element.getFrameEffects()).setBackgroundColor(element.getBackgroundColor()).setObjectFit(element.getObjectFit());
|
|
1815
|
+
return clonedElement;
|
|
1816
|
+
}
|
|
1817
|
+
visitTextElement(element) {
|
|
1818
|
+
const clonedElement = new TextElement(element.getProps().text);
|
|
1819
|
+
this.cloneElementProperties(element, clonedElement);
|
|
1820
|
+
clonedElement.setTextEffect(element.getTextEffect());
|
|
1821
|
+
return clonedElement;
|
|
1822
|
+
}
|
|
1823
|
+
visitCaptionElement(element) {
|
|
1824
|
+
const clonedElement = new CaptionElement(
|
|
1825
|
+
element.getProps().text,
|
|
1826
|
+
element.getStart(),
|
|
1827
|
+
element.getEnd()
|
|
1828
|
+
);
|
|
1829
|
+
this.cloneElementProperties(element, clonedElement);
|
|
1830
|
+
return clonedElement;
|
|
1831
|
+
}
|
|
1832
|
+
visitRectElement(element) {
|
|
1833
|
+
const clonedElement = new RectElement(
|
|
1834
|
+
element.getProps().fill,
|
|
1835
|
+
element.getProps().size
|
|
1836
|
+
);
|
|
1837
|
+
this.cloneElementProperties(element, clonedElement);
|
|
1838
|
+
return clonedElement;
|
|
1839
|
+
}
|
|
1840
|
+
visitCircleElement(element) {
|
|
1841
|
+
const clonedElement = new CircleElement(
|
|
1842
|
+
element.getProps().fill,
|
|
1843
|
+
element.getProps().radius
|
|
1844
|
+
);
|
|
1845
|
+
this.cloneElementProperties(element, clonedElement);
|
|
1846
|
+
return clonedElement;
|
|
1847
|
+
}
|
|
1848
|
+
visitIconElement(element) {
|
|
1849
|
+
const clonedElement = new IconElement(
|
|
1850
|
+
element.getProps().src,
|
|
1851
|
+
element.getProps().size
|
|
1852
|
+
);
|
|
1853
|
+
this.cloneElementProperties(element, clonedElement);
|
|
1854
|
+
return clonedElement;
|
|
1855
|
+
}
|
|
1856
|
+
}
|
|
1857
|
+
class ElementSplitter {
|
|
1858
|
+
constructor(splitTime) {
|
|
1859
|
+
__publicField(this, "splitTime");
|
|
1860
|
+
__publicField(this, "elementCloner");
|
|
1861
|
+
this.splitTime = splitTime;
|
|
1862
|
+
this.elementCloner = new ElementCloner();
|
|
1863
|
+
}
|
|
1864
|
+
visitVideoElement(element) {
|
|
1865
|
+
if (!canSplitElement(element, this.splitTime)) {
|
|
1866
|
+
return { firstElement: null, secondElement: null, success: false };
|
|
1867
|
+
}
|
|
1868
|
+
const firstElement = this.elementCloner.visitVideoElement(
|
|
1869
|
+
element
|
|
1870
|
+
);
|
|
1871
|
+
const secondElement = this.elementCloner.visitVideoElement(
|
|
1872
|
+
element
|
|
1873
|
+
);
|
|
1874
|
+
const props = element.getProps();
|
|
1875
|
+
const secondStartAt = (props.time ?? 0) + (this.splitTime - element.getStart()) * (props.playbackRate ?? 1);
|
|
1876
|
+
firstElement.setEnd(this.splitTime);
|
|
1877
|
+
secondElement.setStart(this.splitTime).setStartAt(secondStartAt);
|
|
1878
|
+
return { firstElement, secondElement, success: true };
|
|
1879
|
+
}
|
|
1880
|
+
visitAudioElement(element) {
|
|
1881
|
+
if (!canSplitElement(element, this.splitTime)) {
|
|
1882
|
+
return { firstElement: null, secondElement: null, success: false };
|
|
1883
|
+
}
|
|
1884
|
+
const firstElement = this.elementCloner.visitAudioElement(
|
|
1885
|
+
element
|
|
1886
|
+
);
|
|
1887
|
+
const secondElement = this.elementCloner.visitAudioElement(
|
|
1888
|
+
element
|
|
1889
|
+
);
|
|
1890
|
+
const props = element.getProps();
|
|
1891
|
+
const secondStartAt = (props.time ?? 0) + (this.splitTime - element.getStart()) * (props.playbackRate ?? 1);
|
|
1892
|
+
firstElement.setEnd(this.splitTime);
|
|
1893
|
+
secondElement.setStart(this.splitTime).setStartAt(secondStartAt);
|
|
1894
|
+
return { firstElement, secondElement, success: true };
|
|
1895
|
+
}
|
|
1896
|
+
visitImageElement(element) {
|
|
1897
|
+
if (!canSplitElement(element, this.splitTime)) {
|
|
1898
|
+
return { firstElement: null, secondElement: null, success: false };
|
|
1899
|
+
}
|
|
1900
|
+
const firstElement = this.elementCloner.visitImageElement(
|
|
1901
|
+
element
|
|
1902
|
+
);
|
|
1903
|
+
const secondElement = this.elementCloner.visitImageElement(
|
|
1904
|
+
element
|
|
1905
|
+
);
|
|
1906
|
+
firstElement.setEnd(this.splitTime);
|
|
1907
|
+
secondElement.setStart(this.splitTime);
|
|
1908
|
+
return { firstElement, secondElement, success: true };
|
|
1909
|
+
}
|
|
1910
|
+
visitTextElement(element) {
|
|
1911
|
+
if (!canSplitElement(element, this.splitTime)) {
|
|
1912
|
+
return { firstElement: null, secondElement: null, success: false };
|
|
1913
|
+
}
|
|
1914
|
+
const originalText = element.getText() || "";
|
|
1915
|
+
const originalTextArray = originalText.split(" ");
|
|
1916
|
+
const percentage = (this.splitTime - element.getStart()) / element.getDuration();
|
|
1917
|
+
const firstElement = this.elementCloner.visitTextElement(
|
|
1918
|
+
element
|
|
1919
|
+
);
|
|
1920
|
+
firstElement.setText(
|
|
1921
|
+
originalTextArray.slice(0, Math.floor(originalTextArray.length * percentage)).join(" ")
|
|
1922
|
+
);
|
|
1923
|
+
firstElement.setEnd(this.splitTime);
|
|
1924
|
+
const secondElement = this.elementCloner.visitTextElement(
|
|
1925
|
+
element
|
|
1926
|
+
);
|
|
1927
|
+
secondElement.setText(
|
|
1928
|
+
originalTextArray.slice(
|
|
1929
|
+
Math.floor(originalTextArray.length * percentage),
|
|
1930
|
+
originalTextArray.length
|
|
1931
|
+
).join(" ")
|
|
1932
|
+
);
|
|
1933
|
+
secondElement.setStart(this.splitTime);
|
|
1934
|
+
return { firstElement, secondElement, success: true };
|
|
1935
|
+
}
|
|
1936
|
+
visitCaptionElement(element) {
|
|
1937
|
+
if (!canSplitElement(element, this.splitTime)) {
|
|
1938
|
+
return { firstElement: null, secondElement: null, success: false };
|
|
1939
|
+
}
|
|
1940
|
+
const originalText = element.getText() || "";
|
|
1941
|
+
const originalTextArray = originalText.split(" ");
|
|
1942
|
+
const percentage = (this.splitTime - element.getStart()) / element.getDuration();
|
|
1943
|
+
const firstElement = this.elementCloner.visitCaptionElement(
|
|
1944
|
+
element
|
|
1945
|
+
);
|
|
1946
|
+
firstElement.setText(
|
|
1947
|
+
originalTextArray.slice(0, Math.floor(originalTextArray.length * percentage)).join(" ")
|
|
1948
|
+
);
|
|
1949
|
+
firstElement.setEnd(this.splitTime);
|
|
1950
|
+
const secondElement = this.elementCloner.visitCaptionElement(
|
|
1951
|
+
element
|
|
1952
|
+
);
|
|
1953
|
+
secondElement.setText(
|
|
1954
|
+
originalTextArray.slice(
|
|
1955
|
+
Math.floor(originalTextArray.length * percentage),
|
|
1956
|
+
originalTextArray.length
|
|
1957
|
+
).join(" ")
|
|
1958
|
+
);
|
|
1959
|
+
secondElement.setStart(this.splitTime);
|
|
1960
|
+
return { firstElement, secondElement, success: true };
|
|
1961
|
+
}
|
|
1962
|
+
visitRectElement(element) {
|
|
1963
|
+
if (!canSplitElement(element, this.splitTime)) {
|
|
1964
|
+
return { firstElement: null, secondElement: null, success: false };
|
|
1965
|
+
}
|
|
1966
|
+
const firstElement = this.elementCloner.visitRectElement(
|
|
1967
|
+
element
|
|
1968
|
+
);
|
|
1969
|
+
const secondElement = this.elementCloner.visitRectElement(
|
|
1970
|
+
element
|
|
1971
|
+
);
|
|
1972
|
+
firstElement.setEnd(this.splitTime);
|
|
1973
|
+
secondElement.setStart(this.splitTime);
|
|
1974
|
+
return { firstElement, secondElement, success: true };
|
|
1975
|
+
}
|
|
1976
|
+
visitCircleElement(element) {
|
|
1977
|
+
if (!canSplitElement(element, this.splitTime)) {
|
|
1978
|
+
return { firstElement: null, secondElement: null, success: false };
|
|
1979
|
+
}
|
|
1980
|
+
const firstElement = this.elementCloner.visitCircleElement(element);
|
|
1981
|
+
const secondElement = this.elementCloner.visitCircleElement(element);
|
|
1982
|
+
firstElement.setEnd(this.splitTime);
|
|
1983
|
+
secondElement.setStart(this.splitTime);
|
|
1984
|
+
return { firstElement, secondElement, success: true };
|
|
1985
|
+
}
|
|
1986
|
+
visitIconElement(element) {
|
|
1987
|
+
if (!canSplitElement(element, this.splitTime)) {
|
|
1988
|
+
return { firstElement: null, secondElement: null, success: false };
|
|
1989
|
+
}
|
|
1990
|
+
const firstElement = this.elementCloner.visitIconElement(element);
|
|
1991
|
+
const secondElement = this.elementCloner.visitIconElement(element);
|
|
1992
|
+
firstElement.setEnd(this.splitTime);
|
|
1993
|
+
secondElement.setStart(this.splitTime);
|
|
1994
|
+
return { firstElement, secondElement, success: true };
|
|
1995
|
+
}
|
|
1996
|
+
}
|
|
1997
|
+
class TimelineEditor {
|
|
1998
|
+
constructor(context) {
|
|
1999
|
+
__publicField(this, "context");
|
|
2000
|
+
this.context = context;
|
|
2001
|
+
timelineContextStore.initializeContext(this.context.contextId);
|
|
2002
|
+
}
|
|
2003
|
+
getContext() {
|
|
2004
|
+
return this.context;
|
|
2005
|
+
}
|
|
2006
|
+
pauseVideo() {
|
|
2007
|
+
var _a;
|
|
2008
|
+
if ((_a = this.context) == null ? void 0 : _a.setTimelineAction) {
|
|
2009
|
+
this.context.setTimelineAction(
|
|
2010
|
+
TIMELINE_ACTION.SET_PLAYER_STATE,
|
|
2011
|
+
PLAYER_STATE.PAUSED
|
|
2012
|
+
);
|
|
2013
|
+
}
|
|
2014
|
+
}
|
|
2015
|
+
getTimelineData() {
|
|
2016
|
+
const contextId = this.context.contextId;
|
|
2017
|
+
return timelineContextStore.getTimelineData(contextId);
|
|
2018
|
+
}
|
|
2019
|
+
getLatestVersion() {
|
|
2020
|
+
const contextId = this.context.contextId;
|
|
2021
|
+
const timelineData = timelineContextStore.getTimelineData(contextId);
|
|
2022
|
+
return (timelineData == null ? void 0 : timelineData.version) || 0;
|
|
2023
|
+
}
|
|
2024
|
+
setTimelineData(tracks, version) {
|
|
2025
|
+
const prevTimelineData = this.getTimelineData();
|
|
2026
|
+
const updatedVersion = version ?? ((prevTimelineData == null ? void 0 : prevTimelineData.version) || 0) + 1;
|
|
2027
|
+
const updatedTimelineData = {
|
|
2028
|
+
tracks,
|
|
2029
|
+
version: updatedVersion
|
|
2030
|
+
};
|
|
2031
|
+
timelineContextStore.setTimelineData(
|
|
2032
|
+
this.context.contextId,
|
|
2033
|
+
updatedTimelineData
|
|
2034
|
+
);
|
|
2035
|
+
this.updateHistory(updatedTimelineData);
|
|
2036
|
+
this.context.updateChangeLog();
|
|
2037
|
+
return updatedTimelineData;
|
|
2038
|
+
}
|
|
2039
|
+
addTrack(name) {
|
|
2040
|
+
const prevTimelineData = this.getTimelineData();
|
|
2041
|
+
const id = `t-${generateShortUuid()}`;
|
|
2042
|
+
const track = new Track(name, id);
|
|
2043
|
+
const updatedTimelines = [...(prevTimelineData == null ? void 0 : prevTimelineData.tracks) || [], track];
|
|
2044
|
+
this.setTimelineData(updatedTimelines);
|
|
2045
|
+
return track;
|
|
2046
|
+
}
|
|
2047
|
+
getTrackById(id) {
|
|
2048
|
+
const prevTimelineData = this.getTimelineData();
|
|
2049
|
+
const track = prevTimelineData == null ? void 0 : prevTimelineData.tracks.find((t) => t.getId() === id);
|
|
2050
|
+
return track;
|
|
2051
|
+
}
|
|
2052
|
+
getTrackByName(name) {
|
|
2053
|
+
const prevTimelineData = this.getTimelineData();
|
|
2054
|
+
const track = prevTimelineData == null ? void 0 : prevTimelineData.tracks.find((t) => t.getName() === name);
|
|
2055
|
+
return track;
|
|
2056
|
+
}
|
|
2057
|
+
removeTrackById(id) {
|
|
2058
|
+
var _a;
|
|
2059
|
+
const tracks = ((_a = this.getTimelineData()) == null ? void 0 : _a.tracks) || [];
|
|
2060
|
+
const updatedTracks = tracks.filter((t) => t.getId() !== id);
|
|
2061
|
+
this.setTimelineData(updatedTracks);
|
|
2062
|
+
}
|
|
2063
|
+
removeTrack(track) {
|
|
2064
|
+
var _a;
|
|
2065
|
+
const tracks = ((_a = this.getTimelineData()) == null ? void 0 : _a.tracks) || [];
|
|
2066
|
+
const updatedTracks = tracks.filter((t) => t.getId() !== track.getId());
|
|
2067
|
+
this.setTimelineData(updatedTracks);
|
|
2068
|
+
}
|
|
2069
|
+
/**
|
|
2070
|
+
* Refresh the timeline data
|
|
2071
|
+
*/
|
|
2072
|
+
refresh() {
|
|
2073
|
+
const currentData = this.getTimelineData();
|
|
2074
|
+
if (currentData) {
|
|
2075
|
+
this.setTimelineData(currentData.tracks);
|
|
2076
|
+
}
|
|
2077
|
+
}
|
|
2078
|
+
/**
|
|
2079
|
+
* Add an element to a specific track using the visitor pattern
|
|
2080
|
+
* @param track The track to add the element to
|
|
2081
|
+
* @param element The element to add
|
|
2082
|
+
* @returns Promise<boolean> true if element was added successfully
|
|
2083
|
+
*/
|
|
2084
|
+
async addElementToTrack(track, element) {
|
|
2085
|
+
if (!track) {
|
|
2086
|
+
return false;
|
|
2087
|
+
}
|
|
2088
|
+
try {
|
|
2089
|
+
const elementAdder = new ElementAdder(track);
|
|
2090
|
+
const result = await element.accept(elementAdder);
|
|
2091
|
+
if (result) {
|
|
2092
|
+
const currentData = this.getTimelineData();
|
|
2093
|
+
if (currentData) {
|
|
2094
|
+
this.setTimelineData(currentData.tracks);
|
|
2095
|
+
}
|
|
2096
|
+
}
|
|
2097
|
+
return result;
|
|
2098
|
+
} catch (error) {
|
|
2099
|
+
return false;
|
|
2100
|
+
}
|
|
2101
|
+
}
|
|
2102
|
+
/**
|
|
2103
|
+
* Remove an element from a specific track using the visitor pattern
|
|
2104
|
+
* @param element The element to remove
|
|
2105
|
+
* @returns boolean true if element was removed successfully
|
|
2106
|
+
*/
|
|
2107
|
+
removeElement(element) {
|
|
2108
|
+
const track = this.getTrackById(element.getTrackId());
|
|
2109
|
+
if (!track) {
|
|
2110
|
+
return false;
|
|
2111
|
+
}
|
|
2112
|
+
try {
|
|
2113
|
+
const elementRemover = new ElementRemover(track);
|
|
2114
|
+
const result = element.accept(elementRemover);
|
|
2115
|
+
if (result) {
|
|
2116
|
+
const currentData = this.getTimelineData();
|
|
2117
|
+
if (currentData) {
|
|
2118
|
+
this.setTimelineData(currentData.tracks);
|
|
2119
|
+
}
|
|
2120
|
+
}
|
|
2121
|
+
return result;
|
|
2122
|
+
} catch (error) {
|
|
2123
|
+
return false;
|
|
2124
|
+
}
|
|
2125
|
+
}
|
|
2126
|
+
/**
|
|
2127
|
+
* Update an element in a specific track using the visitor pattern
|
|
2128
|
+
* @param element The updated element
|
|
2129
|
+
* @returns boolean true if element was updated successfully
|
|
2130
|
+
*/
|
|
2131
|
+
updateElement(element) {
|
|
2132
|
+
const track = this.getTrackById(element.getTrackId());
|
|
2133
|
+
if (!track) {
|
|
2134
|
+
return false;
|
|
2135
|
+
}
|
|
2136
|
+
try {
|
|
2137
|
+
const elementUpdater = new ElementUpdater(track);
|
|
2138
|
+
const result = element.accept(elementUpdater);
|
|
2139
|
+
if (result) {
|
|
2140
|
+
const currentData = this.getTimelineData();
|
|
2141
|
+
if (currentData) {
|
|
2142
|
+
this.setTimelineData(currentData.tracks);
|
|
2143
|
+
}
|
|
2144
|
+
}
|
|
2145
|
+
return result;
|
|
2146
|
+
} catch (error) {
|
|
2147
|
+
return false;
|
|
2148
|
+
}
|
|
2149
|
+
}
|
|
2150
|
+
/**
|
|
2151
|
+
* Split an element at a specific time point using the visitor pattern
|
|
2152
|
+
* @param element The element to split
|
|
2153
|
+
* @param splitTime The time point to split at
|
|
2154
|
+
* @returns SplitResult with first element, second element, and success status
|
|
2155
|
+
*/
|
|
2156
|
+
async splitElement(element, splitTime) {
|
|
2157
|
+
const track = this.getTrackById(element.getTrackId());
|
|
2158
|
+
if (!track) {
|
|
2159
|
+
return { firstElement: element, secondElement: null, success: false };
|
|
2160
|
+
}
|
|
2161
|
+
try {
|
|
2162
|
+
const elementSplitter = new ElementSplitter(splitTime);
|
|
2163
|
+
const result = element.accept(elementSplitter);
|
|
2164
|
+
if (result.success) {
|
|
2165
|
+
const elementRemover = new ElementRemover(track);
|
|
2166
|
+
element.accept(elementRemover);
|
|
2167
|
+
const elementAdder = new ElementAdder(track);
|
|
2168
|
+
result.firstElement.accept(elementAdder);
|
|
2169
|
+
result.secondElement.accept(elementAdder);
|
|
2170
|
+
const currentData = this.getTimelineData();
|
|
2171
|
+
if (currentData) {
|
|
2172
|
+
this.setTimelineData(currentData.tracks);
|
|
2173
|
+
}
|
|
2174
|
+
}
|
|
2175
|
+
return result;
|
|
2176
|
+
} catch (error) {
|
|
2177
|
+
return { firstElement: element, secondElement: null, success: false };
|
|
2178
|
+
}
|
|
2179
|
+
}
|
|
2180
|
+
/**
|
|
2181
|
+
* Clone an element using the visitor pattern
|
|
2182
|
+
* @param element The element to clone
|
|
2183
|
+
* @returns TrackElement | null - the cloned element or null if cloning failed
|
|
2184
|
+
*/
|
|
2185
|
+
cloneElement(element) {
|
|
2186
|
+
try {
|
|
2187
|
+
const elementCloner = new ElementCloner();
|
|
2188
|
+
return element.accept(elementCloner);
|
|
2189
|
+
} catch (error) {
|
|
2190
|
+
return null;
|
|
2191
|
+
}
|
|
2192
|
+
}
|
|
2193
|
+
reorderTracks(tracks) {
|
|
2194
|
+
this.setTimelineData(tracks);
|
|
2195
|
+
}
|
|
2196
|
+
updateHistory(timelineTrackData) {
|
|
2197
|
+
const tracks = timelineTrackData.tracks.map((t) => t.serialize());
|
|
2198
|
+
this.context.setTotalDuration(getTotalDuration(tracks));
|
|
2199
|
+
const version = timelineTrackData.version;
|
|
2200
|
+
this.context.setPresent({
|
|
2201
|
+
tracks,
|
|
2202
|
+
version
|
|
2203
|
+
});
|
|
2204
|
+
}
|
|
2205
|
+
/**
|
|
2206
|
+
* Trigger undo operation and update timeline data
|
|
2207
|
+
*/
|
|
2208
|
+
undo() {
|
|
2209
|
+
var _a;
|
|
2210
|
+
const result = this.context.handleUndo();
|
|
2211
|
+
if (result && result.tracks) {
|
|
2212
|
+
const tracks = result.tracks.map((t) => Track.fromJSON(t));
|
|
2213
|
+
timelineContextStore.setTimelineData(this.context.contextId, {
|
|
2214
|
+
tracks,
|
|
2215
|
+
version: result.version
|
|
2216
|
+
});
|
|
2217
|
+
this.context.setTotalDuration(getTotalDuration(result.tracks));
|
|
2218
|
+
this.context.updateChangeLog();
|
|
2219
|
+
if ((_a = this.context) == null ? void 0 : _a.setTimelineAction) {
|
|
2220
|
+
this.context.setTimelineAction(TIMELINE_ACTION.UPDATE_PLAYER_DATA, {
|
|
2221
|
+
tracks: result.tracks,
|
|
2222
|
+
version: result.version
|
|
2223
|
+
});
|
|
2224
|
+
}
|
|
2225
|
+
}
|
|
2226
|
+
}
|
|
2227
|
+
/**
|
|
2228
|
+
* Trigger redo operation and update timeline data
|
|
2229
|
+
*/
|
|
2230
|
+
redo() {
|
|
2231
|
+
var _a;
|
|
2232
|
+
const result = this.context.handleRedo();
|
|
2233
|
+
if (result && result.tracks) {
|
|
2234
|
+
const tracks = result.tracks.map((t) => Track.fromJSON(t));
|
|
2235
|
+
timelineContextStore.setTimelineData(this.context.contextId, {
|
|
2236
|
+
tracks,
|
|
2237
|
+
version: result.version
|
|
2238
|
+
});
|
|
2239
|
+
this.context.setTotalDuration(getTotalDuration(result.tracks));
|
|
2240
|
+
this.context.updateChangeLog();
|
|
2241
|
+
if ((_a = this.context) == null ? void 0 : _a.setTimelineAction) {
|
|
2242
|
+
this.context.setTimelineAction(TIMELINE_ACTION.UPDATE_PLAYER_DATA, {
|
|
2243
|
+
tracks: result.tracks,
|
|
2244
|
+
version: result.version
|
|
2245
|
+
});
|
|
2246
|
+
}
|
|
2247
|
+
}
|
|
2248
|
+
}
|
|
2249
|
+
/**
|
|
2250
|
+
* Reset history and clear timeline data
|
|
2251
|
+
*/
|
|
2252
|
+
resetHistory() {
|
|
2253
|
+
var _a;
|
|
2254
|
+
this.context.handleResetHistory();
|
|
2255
|
+
timelineContextStore.setTimelineData(this.context.contextId, {
|
|
2256
|
+
tracks: [],
|
|
2257
|
+
version: 0
|
|
2258
|
+
});
|
|
2259
|
+
this.context.setTotalDuration(0);
|
|
2260
|
+
this.context.updateChangeLog();
|
|
2261
|
+
if ((_a = this.context) == null ? void 0 : _a.setTimelineAction) {
|
|
2262
|
+
this.context.setTimelineAction(TIMELINE_ACTION.UPDATE_PLAYER_DATA, {
|
|
2263
|
+
tracks: [],
|
|
2264
|
+
version: 0
|
|
2265
|
+
});
|
|
2266
|
+
}
|
|
2267
|
+
}
|
|
2268
|
+
loadProject({
|
|
2269
|
+
tracks,
|
|
2270
|
+
version
|
|
2271
|
+
}) {
|
|
2272
|
+
var _a;
|
|
2273
|
+
this.pauseVideo();
|
|
2274
|
+
this.context.handleResetHistory();
|
|
2275
|
+
const timelineTracks = tracks.map((t) => Track.fromJSON(t));
|
|
2276
|
+
this.setTimelineData(timelineTracks, version);
|
|
2277
|
+
if ((_a = this.context) == null ? void 0 : _a.setTimelineAction) {
|
|
2278
|
+
this.context.setTimelineAction(TIMELINE_ACTION.UPDATE_PLAYER_DATA, {
|
|
2279
|
+
tracks,
|
|
2280
|
+
version,
|
|
2281
|
+
forceUpdate: true
|
|
2282
|
+
});
|
|
2283
|
+
}
|
|
2284
|
+
}
|
|
2285
|
+
}
|
|
2286
|
+
const MAX_HISTORY = 20;
|
|
2287
|
+
const deepClone = (obj) => {
|
|
2288
|
+
return JSON.parse(JSON.stringify(obj));
|
|
2289
|
+
};
|
|
2290
|
+
const UndoRedoContext = createContext(
|
|
2291
|
+
void 0
|
|
2292
|
+
);
|
|
2293
|
+
const STORAGE_KEY_PREFIX = "twick_undo_redo_";
|
|
2294
|
+
const saveToStorage = (key, state) => {
|
|
2295
|
+
try {
|
|
2296
|
+
localStorage.setItem(key, JSON.stringify(state));
|
|
2297
|
+
} catch (error) {
|
|
2298
|
+
console.warn("Failed to save undo-redo state to localStorage:", error);
|
|
2299
|
+
}
|
|
2300
|
+
};
|
|
2301
|
+
const loadFromStorage = (key) => {
|
|
2302
|
+
try {
|
|
2303
|
+
const stored = localStorage.getItem(key);
|
|
2304
|
+
if (!stored) return null;
|
|
2305
|
+
return JSON.parse(stored);
|
|
2306
|
+
} catch (error) {
|
|
2307
|
+
console.warn("Failed to load undo-redo state from localStorage:", error);
|
|
2308
|
+
return null;
|
|
2309
|
+
}
|
|
2310
|
+
};
|
|
2311
|
+
const UndoRedoProvider = ({
|
|
2312
|
+
children,
|
|
2313
|
+
persistenceKey,
|
|
2314
|
+
maxHistorySize = MAX_HISTORY
|
|
2315
|
+
}) => {
|
|
2316
|
+
const [state, setState] = useState(() => {
|
|
2317
|
+
if (persistenceKey) {
|
|
2318
|
+
const stored = loadFromStorage(STORAGE_KEY_PREFIX + persistenceKey);
|
|
2319
|
+
if (stored) {
|
|
2320
|
+
return {
|
|
2321
|
+
past: stored.past,
|
|
2322
|
+
present: stored.present,
|
|
2323
|
+
future: stored.future
|
|
2324
|
+
};
|
|
2325
|
+
}
|
|
2326
|
+
}
|
|
2327
|
+
return {
|
|
2328
|
+
past: [],
|
|
2329
|
+
present: null,
|
|
2330
|
+
future: []
|
|
2331
|
+
};
|
|
2332
|
+
});
|
|
2333
|
+
const saveState = (newState) => {
|
|
2334
|
+
if (persistenceKey) {
|
|
2335
|
+
saveToStorage(STORAGE_KEY_PREFIX + persistenceKey, newState);
|
|
2336
|
+
}
|
|
2337
|
+
};
|
|
2338
|
+
const setPresent = (data) => {
|
|
2339
|
+
setState((prevState) => {
|
|
2340
|
+
let newPast = [...prevState.past];
|
|
2341
|
+
if (prevState.present) {
|
|
2342
|
+
newPast.push(deepClone(prevState.present));
|
|
2343
|
+
}
|
|
2344
|
+
const newState = {
|
|
2345
|
+
past: newPast,
|
|
2346
|
+
present: deepClone(data),
|
|
2347
|
+
future: []
|
|
2348
|
+
// Clear future because it's a new change
|
|
2349
|
+
};
|
|
2350
|
+
if (newState.past.length > maxHistorySize) {
|
|
2351
|
+
newState.past.shift();
|
|
2352
|
+
}
|
|
2353
|
+
saveState(newState);
|
|
2354
|
+
return newState;
|
|
2355
|
+
});
|
|
2356
|
+
};
|
|
2357
|
+
const undo = () => {
|
|
2358
|
+
let undoResult = null;
|
|
2359
|
+
setState((prevState) => {
|
|
2360
|
+
if (prevState.past.length === 0) return prevState;
|
|
2361
|
+
const previous = prevState.past[prevState.past.length - 1];
|
|
2362
|
+
const newState = {
|
|
2363
|
+
past: prevState.past.slice(0, -1),
|
|
2364
|
+
// Remove last item
|
|
2365
|
+
present: previous,
|
|
2366
|
+
future: prevState.present ? [deepClone(prevState.present), ...prevState.future] : prevState.future
|
|
2367
|
+
};
|
|
2368
|
+
undoResult = previous;
|
|
2369
|
+
saveState(newState);
|
|
2370
|
+
return newState;
|
|
2371
|
+
});
|
|
2372
|
+
return undoResult;
|
|
2373
|
+
};
|
|
2374
|
+
const redo = () => {
|
|
2375
|
+
let redoResult = null;
|
|
2376
|
+
setState((prevState) => {
|
|
2377
|
+
if (prevState.future.length === 0) return prevState;
|
|
2378
|
+
const next = prevState.future[0];
|
|
2379
|
+
const newState = {
|
|
2380
|
+
past: prevState.present ? [...prevState.past, deepClone(prevState.present)] : prevState.past,
|
|
2381
|
+
present: next,
|
|
2382
|
+
future: prevState.future.slice(1)
|
|
2383
|
+
// Remove first item
|
|
2384
|
+
};
|
|
2385
|
+
if (newState.past.length > maxHistorySize) {
|
|
2386
|
+
newState.past.shift();
|
|
2387
|
+
}
|
|
2388
|
+
redoResult = next;
|
|
2389
|
+
saveState(newState);
|
|
2390
|
+
return newState;
|
|
2391
|
+
});
|
|
2392
|
+
return redoResult;
|
|
2393
|
+
};
|
|
2394
|
+
const getLastPersistedState = () => {
|
|
2395
|
+
if (persistenceKey) {
|
|
2396
|
+
const stored = loadFromStorage(STORAGE_KEY_PREFIX + persistenceKey);
|
|
2397
|
+
return (stored == null ? void 0 : stored.present) || null;
|
|
2398
|
+
}
|
|
2399
|
+
return null;
|
|
2400
|
+
};
|
|
2401
|
+
const resetHistory = () => {
|
|
2402
|
+
const newState = {
|
|
2403
|
+
past: [],
|
|
2404
|
+
present: null,
|
|
2405
|
+
future: []
|
|
2406
|
+
};
|
|
2407
|
+
setState(newState);
|
|
2408
|
+
if (persistenceKey) {
|
|
2409
|
+
localStorage.removeItem(STORAGE_KEY_PREFIX + persistenceKey);
|
|
2410
|
+
}
|
|
2411
|
+
};
|
|
2412
|
+
const disablePersistence = () => {
|
|
2413
|
+
if (persistenceKey) {
|
|
2414
|
+
localStorage.removeItem(STORAGE_KEY_PREFIX + persistenceKey);
|
|
2415
|
+
}
|
|
2416
|
+
};
|
|
2417
|
+
const contextValue = {
|
|
2418
|
+
canUndo: state.past.length > 0,
|
|
2419
|
+
canRedo: state.future.length > 0,
|
|
2420
|
+
present: state.present,
|
|
2421
|
+
setPresent,
|
|
2422
|
+
undo,
|
|
2423
|
+
redo,
|
|
2424
|
+
resetHistory,
|
|
2425
|
+
getLastPersistedState,
|
|
2426
|
+
disablePersistence
|
|
2427
|
+
};
|
|
2428
|
+
return /* @__PURE__ */ jsx(UndoRedoContext.Provider, { value: contextValue, children });
|
|
2429
|
+
};
|
|
2430
|
+
const useUndoRedo = () => {
|
|
2431
|
+
const context = useContext(UndoRedoContext);
|
|
2432
|
+
if (context === void 0) {
|
|
2433
|
+
throw new Error("useUndoRedo must be used within an UndoRedoProvider");
|
|
2434
|
+
}
|
|
2435
|
+
return context;
|
|
2436
|
+
};
|
|
2437
|
+
const editorRegistry = /* @__PURE__ */ new Map();
|
|
2438
|
+
if (typeof window !== "undefined") {
|
|
2439
|
+
window.twickTimelineEditors = editorRegistry;
|
|
2440
|
+
}
|
|
2441
|
+
const TimelineContext = createContext(
|
|
2442
|
+
void 0
|
|
2443
|
+
);
|
|
2444
|
+
const TimelineProviderInner = ({
|
|
2445
|
+
contextId,
|
|
2446
|
+
children,
|
|
2447
|
+
initialData
|
|
2448
|
+
}) => {
|
|
2449
|
+
const [timelineAction, setTimelineActionState] = useState({
|
|
2450
|
+
type: TIMELINE_ACTION.NONE,
|
|
2451
|
+
payload: null
|
|
2452
|
+
});
|
|
2453
|
+
const [selectedItem, setSelectedItem] = useState(
|
|
2454
|
+
null
|
|
2455
|
+
);
|
|
2456
|
+
const [totalDuration, setTotalDuration] = useState(0);
|
|
2457
|
+
const [changeLog, setChangeLog] = useState(0);
|
|
2458
|
+
const undoRedoContext = useUndoRedo();
|
|
2459
|
+
const dataInitialized = useRef(false);
|
|
2460
|
+
const updateChangeLog = () => {
|
|
2461
|
+
setChangeLog((prev) => prev + 1);
|
|
2462
|
+
};
|
|
2463
|
+
const editor = useMemo(() => {
|
|
2464
|
+
if (editorRegistry.has(contextId)) {
|
|
2465
|
+
editorRegistry.delete(contextId);
|
|
2466
|
+
}
|
|
2467
|
+
const newEditor = new TimelineEditor({
|
|
2468
|
+
contextId,
|
|
2469
|
+
setTotalDuration,
|
|
2470
|
+
setPresent: undoRedoContext.setPresent,
|
|
2471
|
+
handleUndo: undoRedoContext.undo,
|
|
2472
|
+
handleRedo: undoRedoContext.redo,
|
|
2473
|
+
handleResetHistory: undoRedoContext.resetHistory,
|
|
2474
|
+
updateChangeLog,
|
|
2475
|
+
setTimelineAction: (action, payload) => {
|
|
2476
|
+
setTimelineActionState({ type: action, payload });
|
|
2477
|
+
}
|
|
2478
|
+
});
|
|
2479
|
+
editorRegistry.set(contextId, newEditor);
|
|
2480
|
+
return newEditor;
|
|
2481
|
+
}, [contextId]);
|
|
2482
|
+
const setTimelineAction = (type, payload) => {
|
|
2483
|
+
setTimelineActionState({ type, payload });
|
|
2484
|
+
};
|
|
2485
|
+
const initialize = (data) => {
|
|
2486
|
+
const lastPersistedState = undoRedoContext.getLastPersistedState();
|
|
2487
|
+
if (lastPersistedState) {
|
|
2488
|
+
editor.loadProject(lastPersistedState);
|
|
2489
|
+
return;
|
|
2490
|
+
} else {
|
|
2491
|
+
editor.loadProject(data);
|
|
2492
|
+
}
|
|
2493
|
+
};
|
|
2494
|
+
useEffect(() => {
|
|
2495
|
+
if (initialData && !dataInitialized.current) {
|
|
2496
|
+
initialize(initialData);
|
|
2497
|
+
dataInitialized.current = true;
|
|
2498
|
+
}
|
|
2499
|
+
}, [initialData]);
|
|
2500
|
+
const contextValue = {
|
|
2501
|
+
contextId,
|
|
2502
|
+
selectedItem,
|
|
2503
|
+
timelineAction,
|
|
2504
|
+
totalDuration,
|
|
2505
|
+
changeLog,
|
|
2506
|
+
present: undoRedoContext.present,
|
|
2507
|
+
canUndo: undoRedoContext.canUndo,
|
|
2508
|
+
canRedo: undoRedoContext.canRedo,
|
|
2509
|
+
setSelectedItem,
|
|
2510
|
+
setTimelineAction,
|
|
2511
|
+
editor
|
|
2512
|
+
// Include the editor instance
|
|
2513
|
+
};
|
|
2514
|
+
return /* @__PURE__ */ jsx(TimelineContext.Provider, { value: contextValue, children });
|
|
2515
|
+
};
|
|
2516
|
+
const TimelineProvider = ({
|
|
2517
|
+
contextId,
|
|
2518
|
+
children,
|
|
2519
|
+
initialData,
|
|
2520
|
+
undoRedoPersistenceKey,
|
|
2521
|
+
maxHistorySize
|
|
2522
|
+
}) => {
|
|
2523
|
+
return /* @__PURE__ */ jsx(
|
|
2524
|
+
UndoRedoProvider,
|
|
2525
|
+
{
|
|
2526
|
+
persistenceKey: undoRedoPersistenceKey,
|
|
2527
|
+
maxHistorySize,
|
|
2528
|
+
children: /* @__PURE__ */ jsx(
|
|
2529
|
+
TimelineProviderInner,
|
|
2530
|
+
{
|
|
2531
|
+
initialData,
|
|
2532
|
+
contextId,
|
|
2533
|
+
undoRedoPersistenceKey,
|
|
2534
|
+
maxHistorySize,
|
|
2535
|
+
children
|
|
2536
|
+
}
|
|
2537
|
+
)
|
|
2538
|
+
}
|
|
2539
|
+
);
|
|
2540
|
+
};
|
|
2541
|
+
const useTimelineContext = () => {
|
|
2542
|
+
const context = useContext(TimelineContext);
|
|
2543
|
+
if (context === void 0) {
|
|
2544
|
+
throw new Error(
|
|
2545
|
+
"useTimelineContext must be used within a TimelineProvider"
|
|
2546
|
+
);
|
|
2547
|
+
}
|
|
2548
|
+
return context;
|
|
2549
|
+
};
|
|
2550
|
+
if (typeof window !== "undefined") {
|
|
2551
|
+
window.Twick = {
|
|
2552
|
+
Track,
|
|
2553
|
+
TrackElement,
|
|
2554
|
+
ElementDeserializer,
|
|
2555
|
+
ElementSerializer,
|
|
2556
|
+
ElementValidator,
|
|
2557
|
+
ElementAdder,
|
|
2558
|
+
ElementRemover,
|
|
2559
|
+
ElementUpdater,
|
|
2560
|
+
ElementSplitter,
|
|
2561
|
+
ElementCloner,
|
|
2562
|
+
TimelineEditor,
|
|
2563
|
+
TimelineProvider,
|
|
2564
|
+
TIMELINE_ELEMENT_TYPE,
|
|
2565
|
+
// Element types
|
|
2566
|
+
CaptionElement,
|
|
2567
|
+
RectElement,
|
|
2568
|
+
TextElement,
|
|
2569
|
+
ImageElement,
|
|
2570
|
+
AudioElement,
|
|
2571
|
+
CircleElement,
|
|
2572
|
+
IconElement,
|
|
2573
|
+
VideoElement,
|
|
2574
|
+
ElementAnimation,
|
|
2575
|
+
ElementFrameEffect,
|
|
2576
|
+
ElementTextEffect,
|
|
2577
|
+
// Utility functions
|
|
2578
|
+
generateShortUuid,
|
|
2579
|
+
getTotalDuration,
|
|
2580
|
+
getCurrentElements,
|
|
2581
|
+
isTrackId,
|
|
2582
|
+
isElementId
|
|
2583
|
+
};
|
|
2584
|
+
}
|
|
2585
|
+
export {
|
|
2586
|
+
AudioElement,
|
|
2587
|
+
CAPTION_COLOR,
|
|
2588
|
+
CAPTION_FONT,
|
|
2589
|
+
CAPTION_STYLE,
|
|
2590
|
+
CAPTION_STYLE_OPTIONS,
|
|
2591
|
+
CaptionElement,
|
|
2592
|
+
CircleElement,
|
|
2593
|
+
ElementAdder,
|
|
2594
|
+
ElementAnimation,
|
|
2595
|
+
ElementCloner,
|
|
2596
|
+
ElementDeserializer,
|
|
2597
|
+
ElementFrameEffect,
|
|
2598
|
+
ElementRemover,
|
|
2599
|
+
ElementSerializer,
|
|
2600
|
+
ElementSplitter,
|
|
2601
|
+
ElementTextEffect,
|
|
2602
|
+
ElementUpdater,
|
|
2603
|
+
ElementValidator,
|
|
2604
|
+
IconElement,
|
|
2605
|
+
ImageElement,
|
|
2606
|
+
PLAYER_STATE,
|
|
2607
|
+
PROCESS_STATE,
|
|
2608
|
+
RectElement,
|
|
2609
|
+
TIMELINE_ACTION,
|
|
2610
|
+
TIMELINE_ELEMENT_TYPE,
|
|
2611
|
+
TextElement,
|
|
2612
|
+
TimelineEditor,
|
|
2613
|
+
TimelineProvider,
|
|
2614
|
+
Track,
|
|
2615
|
+
TrackElement,
|
|
2616
|
+
ValidationError,
|
|
2617
|
+
VideoElement,
|
|
2618
|
+
WORDS_PER_PHRASE,
|
|
2619
|
+
canSplitElement,
|
|
2620
|
+
generateShortUuid,
|
|
2621
|
+
getCurrentElements,
|
|
2622
|
+
getDecimalNumber,
|
|
2623
|
+
getTotalDuration,
|
|
2624
|
+
isElementId,
|
|
2625
|
+
isTrackId,
|
|
2626
|
+
useTimelineContext
|
|
2627
|
+
};
|
|
2628
|
+
//# sourceMappingURL=index.mjs.map
|