lexgui 8.2.0 → 8.2.2

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.
Files changed (45) hide show
  1. package/build/components/Avatar.d.ts +15 -15
  2. package/build/components/NodeTree.d.ts +51 -51
  3. package/build/components/Vector.d.ts +10 -10
  4. package/build/core/Event.d.ts +6 -6
  5. package/build/core/Namespace.js +1 -1
  6. package/build/core/Namespace.js.map +1 -1
  7. package/build/core/Panel.d.ts +538 -538
  8. package/build/extensions/AssetView.d.ts +1 -1
  9. package/build/extensions/AssetView.js +9 -10
  10. package/build/extensions/AssetView.js.map +1 -1
  11. package/build/extensions/CodeEditor.d.ts +358 -358
  12. package/build/extensions/CodeEditor.js +5054 -5054
  13. package/build/extensions/CodeEditor.js.map +1 -1
  14. package/build/extensions/DocMaker.js +331 -330
  15. package/build/extensions/DocMaker.js.map +1 -1
  16. package/build/extensions/GraphEditor.js +2754 -2754
  17. package/build/extensions/Timeline.d.ts +668 -668
  18. package/build/extensions/Timeline.js +3948 -3948
  19. package/build/extensions/Timeline.js.map +1 -1
  20. package/build/extensions/VideoEditor.d.ts +150 -128
  21. package/build/extensions/VideoEditor.js +1014 -893
  22. package/build/extensions/VideoEditor.js.map +1 -1
  23. package/build/index.css.d.ts +3 -3
  24. package/build/index.d.ts +57 -57
  25. package/build/lexgui.all.js +332 -190
  26. package/build/lexgui.all.js.map +1 -1
  27. package/build/lexgui.all.min.js +1 -1
  28. package/build/lexgui.all.module.js +332 -190
  29. package/build/lexgui.all.module.js.map +1 -1
  30. package/build/lexgui.all.module.min.js +1 -1
  31. package/build/lexgui.css +226 -230
  32. package/build/lexgui.js +26 -5
  33. package/build/lexgui.js.map +1 -1
  34. package/build/lexgui.min.css +1 -1
  35. package/build/lexgui.min.js +1 -1
  36. package/build/lexgui.module.js +26 -5
  37. package/build/lexgui.module.js.map +1 -1
  38. package/build/lexgui.module.min.js +1 -1
  39. package/changelog.md +33 -1
  40. package/examples/all-components.html +4 -5
  41. package/examples/asset-view.html +1 -1
  42. package/examples/code-editor.html +11 -0
  43. package/examples/dialogs.html +13 -2
  44. package/examples/side-bar.html +3 -1
  45. package/package.json +1 -1
@@ -1,893 +1,1014 @@
1
- // This is a generated file. Do not edit.
2
- import { LX } from '../core/Namespace.js';
3
-
4
- // VideoEditor.ts @evallsg
5
- if (!LX) {
6
- throw ('Missing LX namespace!');
7
- }
8
- LX.extensions.push('VideoEditor');
9
- const vec2 = LX.vec2;
10
- LX.Area;
11
- LX.Panel;
12
- /**
13
- * @class TimeBar
14
- */
15
- class TimeBar {
16
- static TIMEBAR_PLAY = 1;
17
- static TIMEBAR_TRIM = 2;
18
- static BACKGROUND_COLOR = LX.getCSSVariable('secondary');
19
- static COLOR = LX.getCSSVariable('accent');
20
- static ACTIVE_COLOR = LX.getCSSVariable('color-blue-400');
21
- type = TimeBar.TIMEBAR_PLAY;
22
- duration = 1.0;
23
- canvas;
24
- ctx;
25
- markerWidth = 8;
26
- markerHeight;
27
- offset;
28
- lineWidth;
29
- lineHeight;
30
- position;
31
- startX;
32
- endX;
33
- currentX;
34
- hovering;
35
- dragging;
36
- onChangeCurrent;
37
- onChangeStart;
38
- onChangeEnd;
39
- onDraw;
40
- onMouse;
41
- constructor(area, type, options = {}) {
42
- this.type = type ?? TimeBar.TIMEBAR_PLAY;
43
- this.duration = options.duration ?? this.duration;
44
- // Create canvas
45
- this.canvas = document.createElement('canvas');
46
- this.canvas.width = area.size[0];
47
- this.canvas.height = area.size[1];
48
- area.attach(this.canvas);
49
- this.ctx = this.canvas.getContext('2d');
50
- this.markerWidth = options.markerWidth ?? this.markerWidth;
51
- this.markerHeight = options.markerHeight ?? (this.canvas.height * 0.5);
52
- this.offset = options.offset || (this.markerWidth * 0.5 + 5);
53
- // dimensions of line (not canvas)
54
- this.lineWidth = this.canvas.width - this.offset * 2;
55
- this.lineHeight = options.barHeight ?? 5;
56
- this.position = new vec2(this.offset, this.canvas.height * 0.5 - this.lineHeight * 0.5);
57
- this.startX = this.position.x;
58
- this.endX = this.position.x + this.lineWidth;
59
- this.currentX = this.startX;
60
- this._draw();
61
- this.updateTheme();
62
- LX.addSignal('@on_new_color_scheme', () => {
63
- // Retrieve again the color using LX.getCSSVariable, which checks the applied theme
64
- this.updateTheme();
65
- });
66
- this.canvas.onmousedown = (e) => this.onMouseDown(e);
67
- this.canvas.onmousemove = (e) => this.onMouseMove(e);
68
- this.canvas.onmouseup = (e) => this.onMouseUp(e);
69
- }
70
- updateTheme() {
71
- TimeBar.BACKGROUND_COLOR = LX.getCSSVariable('secondary');
72
- TimeBar.COLOR = LX.getCSSVariable('accent');
73
- TimeBar.ACTIVE_COLOR = LX.getCSSVariable('color-blue-400');
74
- }
75
- setDuration(duration) {
76
- this.duration = duration;
77
- }
78
- xToTime(x) {
79
- return ((x - this.offset) / (this.lineWidth)) * this.duration;
80
- }
81
- timeToX(time) {
82
- return (time / this.duration) * (this.lineWidth) + this.offset;
83
- }
84
- setCurrentTime(time) {
85
- this.currentX = this.timeToX(time);
86
- this.onSetCurrentValue(this.currentX);
87
- }
88
- setStartTime(time) {
89
- this.startX = this.timeToX(time);
90
- this.onSetStartValue(this.startX);
91
- }
92
- setEndTime(time) {
93
- this.endX = this.timeToX(time);
94
- this.onSetEndValue(this.endX);
95
- }
96
- onSetCurrentValue(x) {
97
- this.update(x);
98
- const t = this.xToTime(x);
99
- if (this.onChangeCurrent) {
100
- this.onChangeCurrent(t);
101
- }
102
- }
103
- onSetStartValue(x) {
104
- this.update(x);
105
- const t = this.xToTime(x);
106
- if (this.onChangeStart) {
107
- this.onChangeStart(t);
108
- }
109
- }
110
- onSetEndValue(x) {
111
- this.update(x);
112
- const t = this.xToTime(x);
113
- if (this.onChangeEnd) {
114
- this.onChangeEnd(t);
115
- }
116
- }
117
- _draw() {
118
- const ctx = this.ctx;
119
- if (!ctx)
120
- return;
121
- ctx.save();
122
- ctx.fillStyle = TimeBar.BACKGROUND_COLOR;
123
- ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
124
- ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
125
- // Draw background timeline
126
- ctx.fillStyle = TimeBar.COLOR;
127
- ctx.fillRect(this.position.x, this.position.y, this.lineWidth, this.lineHeight);
128
- // Draw background trimed timeline
129
- ctx.fillStyle = TimeBar.ACTIVE_COLOR;
130
- ctx.fillRect(this.startX, this.position.y, this.endX - this.startX, this.lineHeight);
131
- ctx.restore();
132
- // Min-Max time markers
133
- this._drawTrimMarker('start', this.startX, { color: null, fillColor: TimeBar.ACTIVE_COLOR || '#5f88c9' });
134
- this._drawTrimMarker('end', this.endX, { color: null, fillColor: TimeBar.ACTIVE_COLOR || '#5f88c9' });
135
- this._drawTimeMarker('current', this.currentX, { color: '#e5e5e5', fillColor: TimeBar.ACTIVE_COLOR || '#5f88c9', width: this.markerWidth });
136
- if (this.onDraw) {
137
- this.onDraw();
138
- }
139
- }
140
- _drawTrimMarker(name, x, options = {}) {
141
- const w = this.markerWidth;
142
- const h = this.markerHeight;
143
- const y = this.canvas.height * 0.5 - h * 0.5;
144
- const ctx = this.ctx;
145
- if (!ctx)
146
- return;
147
- // Shadow
148
- if (this.hovering == name) {
149
- ctx.shadowColor = 'white';
150
- ctx.shadowBlur = 2;
151
- }
152
- ctx.globalAlpha = 1;
153
- ctx.fillStyle = ctx.strokeStyle = options.fillColor || '#111'; // "#FFF";
154
- ctx.beginPath();
155
- ctx.roundRect(x - w * 0.5, y, w, h, 2);
156
- ctx.fill();
157
- ctx.fillStyle = ctx.strokeStyle = options.fillColor || '#111'; // "#FFF";
158
- ctx.strokeStyle = 'white';
159
- ctx.beginPath();
160
- ctx.lineWidth = 2;
161
- ctx.moveTo(x, y + 4);
162
- ctx.lineTo(x, y + h - 4);
163
- ctx.stroke();
164
- ctx.shadowBlur = 0;
165
- }
166
- _drawTimeMarker(name, x, options = {}) {
167
- let y = this.offset;
168
- const w = options.width ? options.width : (this.dragging == name ? 6 : 4);
169
- const h = this.canvas.height - this.offset * 2;
170
- let ctx = this.ctx;
171
- if (!ctx)
172
- return;
173
- ctx.globalAlpha = 1;
174
- ctx.fillStyle = ctx.strokeStyle = options.fillColor || '#111'; // "#FFF";
175
- // Shadow
176
- if (this.hovering == name) {
177
- ctx.shadowColor = 'white';
178
- ctx.shadowBlur = 2;
179
- }
180
- // Current time line
181
- ctx.fillStyle = ctx.strokeStyle = 'white';
182
- ctx.beginPath();
183
- ctx.moveTo(x, y);
184
- ctx.lineTo(x, y + h * 0.5);
185
- ctx.stroke();
186
- ctx.closePath();
187
- ctx.fillStyle = ctx.strokeStyle = options.fillColor || '#111'; // "#FFF";
188
- y -= this.offset + 8;
189
- // Current time ball grab
190
- ctx.fillStyle = options.fillColor || '#e5e5e5';
191
- ctx.beginPath();
192
- ctx.roundRect(x - w * 0.5, y + this.offset, w, w, 5);
193
- ctx.fill();
194
- ctx.shadowBlur = 0;
195
- }
196
- update(x) {
197
- this.currentX = Math.min(Math.max(this.startX, x), this.endX);
198
- this._draw();
199
- }
200
- onMouseDown(e) {
201
- if (this.onMouse) {
202
- this.onMouse(e);
203
- }
204
- e.preventDefault();
205
- if (!this.canvas || e.target != this.canvas || e.cancelBubble) {
206
- return;
207
- }
208
- const canvas = this.canvas;
209
- // Process mouse
210
- const x = e.offsetX;
211
- const y = e.offsetY;
212
- // Check if some marker is clicked
213
- const threshold = this.markerWidth;
214
- // grab trim markers only from the bottom
215
- if (Math.abs(this.startX - x) < threshold && this.position.y < y) {
216
- this.dragging = 'start';
217
- canvas.style.cursor = 'grabbing';
218
- }
219
- else if (Math.abs(this.endX - x) < threshold && this.position.y < y) {
220
- this.dragging = 'end';
221
- canvas.style.cursor = 'grabbing';
222
- }
223
- else {
224
- this.dragging = 'current';
225
- canvas.style.cursor = 'grabbing';
226
- if (x < this.startX) {
227
- this.currentX = this.startX;
228
- }
229
- else if (x > this.endX) {
230
- this.currentX = this.endX;
231
- }
232
- else {
233
- this.currentX = x;
234
- }
235
- this.onSetCurrentValue(this.currentX);
236
- }
237
- this._draw();
238
- }
239
- onMouseUp(e) {
240
- if (this.onMouse) {
241
- this.onMouse(e);
242
- }
243
- e.preventDefault();
244
- delete this.dragging;
245
- delete this.hovering;
246
- if (!this.canvas || e.cancelBubble) {
247
- return;
248
- }
249
- const canvas = this.canvas;
250
- canvas.style.cursor = 'default';
251
- }
252
- onMouseMove(e) {
253
- if (this.onMouse) {
254
- this.onMouse(e);
255
- }
256
- if (!this.canvas || e.cancelBubble) {
257
- return;
258
- }
259
- e.preventDefault();
260
- const canvas = this.canvas;
261
- // Process mouse
262
- const x = e.target == canvas ? e.offsetX : e.clientX - canvas.offsetLeft;
263
- e.target == canvas ? e.offsetY : e.clientY - canvas.offsetTop;
264
- if (this.dragging) {
265
- switch (this.dragging) {
266
- case 'start':
267
- this.startX = Math.max(this.position.x, Math.min(this.endX, x));
268
- this.currentX = this.startX;
269
- this.onSetStartValue(this.startX);
270
- break;
271
- case 'end':
272
- this.endX = Math.max(this.startX, Math.min(this.position.x + this.lineWidth, x));
273
- this.currentX = this.endX;
274
- this.onSetEndValue(this.endX);
275
- break;
276
- default:
277
- this.currentX = Math.max(this.startX, Math.min(this.endX, x));
278
- break;
279
- }
280
- this.onSetCurrentValue(this.currentX);
281
- }
282
- else {
283
- const threshold = this.markerWidth * 0.5;
284
- if (Math.abs(this.startX - x) < threshold) {
285
- this.hovering = 'start';
286
- canvas.style.cursor = 'grab';
287
- }
288
- else if (Math.abs(this.endX - x) < threshold) {
289
- this.hovering = 'end';
290
- canvas.style.cursor = 'grab';
291
- }
292
- else if (Math.abs(this.currentX - x) < threshold) {
293
- this.hovering = 'current';
294
- canvas.style.cursor = 'grab';
295
- }
296
- else {
297
- delete this.hovering;
298
- canvas.style.cursor = 'default';
299
- }
300
- }
301
- this._draw();
302
- }
303
- resize(size) {
304
- this.canvas.width = Math.max(0, size[0]);
305
- this.canvas.height = Math.max(0, size[1]);
306
- let newWidth = size[0] - this.offset * 2;
307
- newWidth = newWidth < 0.00001 ? 0.00001 : newWidth; // actual width of the line = canvas.width - offsetleft - offsetRight
308
- const startRatio = (this.startX - this.offset) / this.lineWidth;
309
- const currentRatio = (this.currentX - this.offset) / this.lineWidth;
310
- const endRatio = (this.endX - this.offset) / this.lineWidth;
311
- this.lineWidth = newWidth;
312
- this.startX = Math.min(Math.max(newWidth * startRatio, 0), newWidth) + this.offset;
313
- this.currentX = Math.min(Math.max(newWidth * currentRatio, 0), newWidth) + this.offset;
314
- this.endX = Math.min(Math.max(newWidth * endRatio, 0), newWidth) + this.offset;
315
- this._draw();
316
- }
317
- }
318
- LX.TimeBar = TimeBar;
319
- /**
320
- * @class VideoEditor
321
- */
322
- class VideoEditor {
323
- static CROP_HANDLE_L = 0x01;
324
- static CROP_HANDLE_R = 0x02;
325
- static CROP_HANDLE_T = 0x04;
326
- static CROP_HANDLE_B = 0x08;
327
- static CROP_HANDLE_TL = VideoEditor.CROP_HANDLE_L | VideoEditor.CROP_HANDLE_T;
328
- static CROP_HANDLE_BL = VideoEditor.CROP_HANDLE_L | VideoEditor.CROP_HANDLE_B;
329
- static CROP_HANDLE_TR = VideoEditor.CROP_HANDLE_R | VideoEditor.CROP_HANDLE_T;
330
- static CROP_HANDLE_BR = VideoEditor.CROP_HANDLE_R | VideoEditor.CROP_HANDLE_B;
331
- options = {};
332
- playing = false;
333
- videoReady = false;
334
- controls = true;
335
- startTimeString = '0:0';
336
- endTimeString = '0:0';
337
- speed = 1.0;
338
- currentTime = 0.0;
339
- startTime = 0.0;
340
- endTime = 0.0;
341
- requestId;
342
- video;
343
- loop = false;
344
- isDragging = false;
345
- isResizing = null; // holds the HTMLElement of the crop handle, if resizing
346
- crop = false;
347
- dragOffsetX = 0.0;
348
- dragOffsetY = 0.0;
349
- currentTimeString = '';
350
- timebar;
351
- mainArea;
352
- cropArea; // HTMLElement with normCoord attribute;
353
- controlsArea;
354
- controlsPanelLeft;
355
- controlsPanelRight;
356
- controlsCurrentPanel;
357
- onChangeCurrent;
358
- onChangeStart;
359
- onChangeEnd;
360
- onKeyUp;
361
- onSetTime;
362
- onVideoLoaded;
363
- onCropArea;
364
- onResize;
365
- onChangeSpeed;
366
- _updateTime = true;
367
- _onCropMouseUp;
368
- _onCropMouseMove;
369
- resize;
370
- constructor(area, options = {}) {
371
- this.options = options ?? {};
372
- this.speed = options.speed ?? this.speed;
373
- this.mainArea = area;
374
- let videoArea = null;
375
- let controlsArea = null;
376
- if (options.controlsArea) {
377
- videoArea = area;
378
- controlsArea = options.controlsArea;
379
- }
380
- else {
381
- [videoArea, controlsArea] = area.split({ type: 'vertical', sizes: ['85%', null], minimizable: false, resize: false });
382
- }
383
- controlsArea.root.classList.add('lexconstrolsarea');
384
- this.cropArea = document.createElement('div');
385
- this.cropArea.id = 'cropArea';
386
- this.cropArea.className = 'resize-area hidden';
387
- this.cropArea.normCoords = { x: 0, y: 0, w: 1, h: 1 };
388
- const flags = 0x0f;
389
- this.setCropAreaHandles(flags);
390
- this.crop = options.crop;
391
- this.dragOffsetX = 0;
392
- this.dragOffsetY = 0;
393
- // Create video element and load it
394
- let video = this.video = options.video ?? document.createElement('video');
395
- this.loop = options.loop ?? this.loop;
396
- if (options.src) {
397
- this.video.src = options.src;
398
- this.loadVideo(options);
399
- }
400
- if (options.videoArea) {
401
- options.videoArea.root.classList.add('lexvideoeditor');
402
- options.videoArea.root.style.position = 'relative';
403
- options.videoArea.attach(this.cropArea);
404
- videoArea.attach(options.videoArea);
405
- }
406
- else {
407
- videoArea.attach(video);
408
- videoArea.attach(this.cropArea);
409
- videoArea.root.classList.add('lexvideoeditor');
410
- }
411
- videoArea.root.style.position = 'relative';
412
- this.controlsArea = controlsArea;
413
- // Create playing timeline area and attach panels
414
- let [topArea, bottomArea] = controlsArea.split({ type: 'vertical', sizes: ['50%', null], minimizable: false, resize: false });
415
- bottomArea.setSize([bottomArea.size[0], 40]);
416
- let [leftArea, controlsRight] = bottomArea.split({ type: 'horizontal', sizes: ['92%', null], minimizable: false, resize: false });
417
- let [controlsLeft, timeBarArea] = leftArea.split({ type: 'horizontal', sizes: ['10%', null], minimizable: false, resize: false });
418
- topArea.root.classList.add('lexbar');
419
- bottomArea.root.classList.add('lexbar');
420
- this.controlsCurrentPanel = new LX.Panel({ className: 'lexcontrolspanel lextime' });
421
- this.controlsCurrentPanel.refresh = () => {
422
- this.controlsCurrentPanel.clear();
423
- this.controlsCurrentPanel.addLabel(this.currentTimeString, { float: 'center' });
424
- };
425
- topArea.root.classList.add('lexflexarea');
426
- topArea.attach(this.controlsCurrentPanel);
427
- this.controlsCurrentPanel.refresh();
428
- const style = getComputedStyle(bottomArea.root);
429
- let padding = Number(style.getPropertyValue('padding').replace('px', ''));
430
- this.timebar = new TimeBar(timeBarArea, TimeBar.TIMEBAR_TRIM, { offset: padding });
431
- // Create controls panel (play/pause button and start time)
432
- this.controlsPanelLeft = new LX.Panel({ className: 'lexcontrolspanel' });
433
- this.controlsPanelLeft.refresh = () => {
434
- this.controlsPanelLeft.clear();
435
- this.controlsPanelLeft.sameLine();
436
- let playbtn = this.controlsPanelLeft.addButton(null, 'PlayButton', (v) => {
437
- this.playing = v;
438
- if (this.playing) {
439
- if (this.video.currentTime + 0.000001 >= this.endTime) {
440
- this.video.currentTime = this.startTime;
441
- }
442
- this.video.play();
443
- }
444
- else {
445
- this.video.pause();
446
- }
447
- }, { icon: 'Play@solid', swap: 'Pause@solid', hideName: true, title: 'Play', tooltip: true, className: 'justify-center' });
448
- playbtn.setState(this.playing, true);
449
- this.controlsPanelLeft.addButton(null, '', (v, e) => {
450
- const panel = new LX.Panel();
451
- panel.addRange('Speed', this.speed, (v) => {
452
- this.speed = v;
453
- this.video.playbackRate = v;
454
- if (this.onChangeSpeed) {
455
- this.onChangeSpeed(v);
456
- }
457
- }, { min: 0, max: 2.5, step: 0.01, hideName: true });
458
- new LX.Popover(e.target, [panel], { align: 'start', side: 'top', sideOffset: 12 });
459
- }, { icon: 'Timer@solid', title: 'Speed', tooltip: true, className: 'justify-center' });
460
- this.controlsPanelLeft.addButton(null, 'Loop', (v) => {
461
- this.loop = v;
462
- }, { title: 'Loop', tooltip: true, icon: ('Repeat@solid'), className: `justify-center`, selectable: true, selected: this.loop });
463
- this.controlsPanelLeft.addLabel(this.startTimeString, { width: '100px' });
464
- this.controlsPanelLeft.endLine();
465
- let availableWidth = leftArea.root.clientWidth - controlsLeft.root.clientWidth;
466
- this.timebar.resize([availableWidth, timeBarArea.root.clientHeight]);
467
- };
468
- this.controlsPanelLeft.refresh();
469
- controlsLeft.root.style.minWidth = 'fit-content';
470
- // controlsLeft.root.classList.add();
471
- controlsLeft.attach(this.controlsPanelLeft);
472
- // Create right controls panel (ens time)
473
- this.controlsPanelRight = new LX.Panel({ className: 'lexcontrolspanel' });
474
- this.controlsPanelRight.refresh = () => {
475
- this.controlsPanelRight.clear();
476
- this.controlsPanelRight.addLabel(this.endTimeString, { width: 100 });
477
- };
478
- this.controlsPanelRight.refresh();
479
- controlsRight.root.style.minWidth = 'fit-content';
480
- controlsRight.attach(this.controlsPanelRight);
481
- this.timebar.onChangeCurrent = this._setCurrentTime.bind(this);
482
- this.timebar.onChangeStart = this._setStartTime.bind(this);
483
- this.timebar.onChangeEnd = this._setEndTime.bind(this);
484
- this.resize = () => {
485
- bottomArea.setSize([this.controlsArea.root.clientWidth, 40]);
486
- let availableWidth = this.controlsArea.root.clientWidth - controlsLeft.root.clientWidth
487
- - controlsRight.root.clientWidth;
488
- this.timebar.resize([availableWidth, timeBarArea.root.clientHeight]);
489
- this.moveCropArea(this.cropArea.normCoords.x, this.cropArea.normCoords.y, true);
490
- this.resizeCropArea(this.cropArea.normCoords.w, this.cropArea.normCoords.h, true);
491
- if (this.onResize) {
492
- this.onResize([videoArea.root.clientWidth, videoArea.root.clientHeight]);
493
- }
494
- };
495
- area.onresize = this.resize.bind(this);
496
- window.addEventListener('resize', area.onresize);
497
- this.onKeyUp = (e) => {
498
- if (this.controls && e.key == ' ') {
499
- e.preventDefault();
500
- e.stopPropagation();
501
- this.playing = !this.playing;
502
- if (this.playing) {
503
- if (this.video.currentTime + 0.000001 >= this.endTime) {
504
- this.video.currentTime = this.startTime;
505
- }
506
- this.video.play();
507
- }
508
- else {
509
- this.video.pause();
510
- }
511
- this.controlsPanelLeft.refresh();
512
- }
513
- };
514
- window.addEventListener('keyup', this.onKeyUp);
515
- const parent = controlsArea.parentElement ? controlsArea.parentElement : controlsArea.root.parentElement;
516
- // Add canvas event listeneres
517
- parent.addEventListener('mousedown', (e) => {
518
- // if( this.controls) {
519
- // this.timebar.onMouseDown(e);
520
- // }
521
- });
522
- this._onCropMouseUp = (event) => {
523
- // if(this.controls) {
524
- // this.timebar.onMouseUp(event);
525
- // }
526
- event.preventDefault();
527
- event.stopPropagation();
528
- if ((this.isDragging || this.isResizing) && this.onCropArea) {
529
- this.onCropArea(this.getCroppedArea());
530
- }
531
- this.isDragging = false;
532
- this.isResizing = false;
533
- document.removeEventListener('mouseup', this._onCropMouseUp); // self destroy. Added during mouseDown on cropArea and handles
534
- document.removeEventListener('mousemove', this._onCropMouseMove); // self destroy. Added during mouseDown on cropArea and handles
535
- };
536
- this._onCropMouseMove = (event) => {
537
- // if(this.controls) {
538
- // this.timebar.onMouseMove(event);
539
- // }
540
- window.getSelection()?.removeAllRanges();
541
- event.preventDefault();
542
- event.stopPropagation();
543
- if (this.isResizing) {
544
- const rectCrop = this.cropArea.getBoundingClientRect();
545
- const rectVideo = this.video.getBoundingClientRect();
546
- const mov = this.isResizing.movement;
547
- let x = rectCrop.left, y = rectCrop.top, w = rectCrop.width, h = rectCrop.height;
548
- if (mov & VideoEditor.CROP_HANDLE_L) {
549
- let mouseX = Math.min(rectCrop.right - 4, Math.max(rectVideo.left, event.clientX)); // -4 because of border
550
- w = rectCrop.left + rectCrop.width - mouseX;
551
- x = mouseX;
552
- if (mouseX < rectCrop.left) {
553
- this.moveCropArea(x, y, false);
554
- this.resizeCropArea(w, h, false);
555
- }
556
- else {
557
- this.resizeCropArea(w, h, false);
558
- this.moveCropArea(x, y, false);
559
- }
560
- }
561
- if (mov & VideoEditor.CROP_HANDLE_R) {
562
- w = event.clientX - rectCrop.left;
563
- this.resizeCropArea(w, h, false);
564
- }
565
- if (mov & VideoEditor.CROP_HANDLE_T) {
566
- const mouseY = Math.min(rectCrop.bottom - 4, Math.max(rectVideo.top, event.clientY));
567
- h = rectCrop.top + rectCrop.height - mouseY;
568
- y = mouseY;
569
- if (mouseY < rectCrop.top) {
570
- this.moveCropArea(x, y, false);
571
- this.resizeCropArea(w, h, false);
572
- }
573
- else {
574
- this.resizeCropArea(w, h, false);
575
- this.moveCropArea(x, y, false);
576
- }
577
- }
578
- if (mov & VideoEditor.CROP_HANDLE_B) {
579
- h = event.clientY - rectCrop.top;
580
- this.resizeCropArea(w, h, false);
581
- }
582
- }
583
- if (this.isDragging) {
584
- this.moveCropArea(event.clientX - this.dragOffsetX, event.clientY - this.dragOffsetY, false);
585
- }
586
- };
587
- this.cropArea.addEventListener('mousedown', (e) => {
588
- if (e.target === this.cropArea) {
589
- const rect = this.cropArea.getBoundingClientRect();
590
- this.isDragging = true;
591
- this.dragOffsetX = e.clientX - rect.left;
592
- this.dragOffsetY = e.clientY - rect.top;
593
- document.addEventListener('mouseup', this._onCropMouseUp);
594
- document.addEventListener('mousemove', this._onCropMouseMove);
595
- }
596
- });
597
- this.onChangeStart = null;
598
- this.onChangeEnd = null;
599
- }
600
- setCropAreaHandles(flags) {
601
- // remove existing resizer handles
602
- const resizers = this.cropArea.getElementsByClassName('resize-handle');
603
- for (let i = resizers.length - 1; i > -1; --i) {
604
- resizers[i].remove();
605
- }
606
- const buildResizer = (className, movement) => {
607
- const handle = document.createElement('div');
608
- handle.className = ' resize-handle ' + className;
609
- handle.movement = movement;
610
- if (this.options.handleStyle) {
611
- Object.assign(handle.style, this.options.handleStyle);
612
- }
613
- this.cropArea.append(handle);
614
- handle.addEventListener('mousedown', (e) => {
615
- e.stopPropagation();
616
- e.preventDefault();
617
- this.isResizing = handle;
618
- document.addEventListener('mouseup', this._onCropMouseUp);
619
- document.addEventListener('mousemove', this._onCropMouseMove);
620
- });
621
- };
622
- if (flags & VideoEditor.CROP_HANDLE_L)
623
- buildResizer('l', VideoEditor.CROP_HANDLE_L);
624
- if (flags & VideoEditor.CROP_HANDLE_R)
625
- buildResizer('r', VideoEditor.CROP_HANDLE_R);
626
- if (flags & VideoEditor.CROP_HANDLE_T)
627
- buildResizer('t', VideoEditor.CROP_HANDLE_T);
628
- if (flags & VideoEditor.CROP_HANDLE_B)
629
- buildResizer('b', VideoEditor.CROP_HANDLE_B);
630
- if ((flags & VideoEditor.CROP_HANDLE_TL) == VideoEditor.CROP_HANDLE_TL) {
631
- buildResizer('tl', VideoEditor.CROP_HANDLE_TL);
632
- }
633
- if ((flags & VideoEditor.CROP_HANDLE_BL) == VideoEditor.CROP_HANDLE_BL) {
634
- buildResizer('bl', VideoEditor.CROP_HANDLE_BL);
635
- }
636
- if ((flags & VideoEditor.CROP_HANDLE_TR) == VideoEditor.CROP_HANDLE_TR) {
637
- buildResizer('tr', VideoEditor.CROP_HANDLE_TR);
638
- }
639
- if ((flags & VideoEditor.CROP_HANDLE_BR) == VideoEditor.CROP_HANDLE_BR) {
640
- buildResizer('br', VideoEditor.CROP_HANDLE_BR);
641
- }
642
- }
643
- resizeCropArea(sx, sy, isNormalized = true) {
644
- const rectVideo = this.video.getBoundingClientRect();
645
- if (!isNormalized) {
646
- sx = (rectVideo.width) ? (sx / rectVideo.width) : 1;
647
- sy = (rectVideo.height) ? (sy / rectVideo.height) : 1;
648
- }
649
- sx = Math.min(1 - this.cropArea.normCoords.x, Math.max(0, sx));
650
- sy = Math.min(1 - this.cropArea.normCoords.y, Math.max(0, sy));
651
- this.cropArea.normCoords.w = sx;
652
- this.cropArea.normCoords.h = sy;
653
- const widthPx = rectVideo.width * sx;
654
- const heightPx = rectVideo.height * sy;
655
- const xPx = rectVideo.width * this.cropArea.normCoords.x + rectVideo.left;
656
- const yPx = rectVideo.height * this.cropArea.normCoords.y + rectVideo.top;
657
- if (!this.cropArea.classList.contains('hidden')) {
658
- const nodes = this.cropArea.parentElement.childNodes;
659
- for (let i = 0; i < nodes.length; i++) {
660
- if (nodes[i] != this.cropArea) {
661
- const rectEl = nodes[i].getBoundingClientRect();
662
- nodes[i].style.webkitMask = `linear-gradient(#000 0 0) ${xPx - rectEl.left}px ${yPx - rectEl.top}px / ${widthPx}px ${heightPx}px, linear-gradient(rgba(0, 0, 0, 0.3) 0 0)`;
663
- nodes[i].style.webkitMaskRepeat = 'no-repeat';
664
- }
665
- }
666
- }
667
- this.cropArea.style.width = widthPx + 'px';
668
- this.cropArea.style.height = heightPx + 'px';
669
- }
670
- // screen pixel (event.clientX) or video normalized (0 is top left of video, 1 bot right)
671
- moveCropArea(x, y, isNormalized = true) {
672
- const rectVideo = this.video.getBoundingClientRect();
673
- if (!isNormalized) {
674
- x = (rectVideo.width) ? ((x - rectVideo.left) / rectVideo.width) : 0;
675
- y = (rectVideo.height) ? ((y - rectVideo.top) / rectVideo.height) : 0;
676
- }
677
- x = Math.max(0, Math.min(1 - this.cropArea.normCoords.w, x));
678
- y = Math.max(0, Math.min(1 - this.cropArea.normCoords.h, y));
679
- this.cropArea.normCoords.x = x;
680
- this.cropArea.normCoords.y = y;
681
- const xPx = rectVideo.width * x + rectVideo.left;
682
- const yPx = rectVideo.height * y + rectVideo.top;
683
- const widthPx = rectVideo.width * this.cropArea.normCoords.w;
684
- const heightPx = rectVideo.height * this.cropArea.normCoords.h;
685
- if (!this.cropArea.classList.contains('hidden')) {
686
- const nodes = this.cropArea.parentElement.childNodes;
687
- for (let i = 0; i < nodes.length; i++) {
688
- if (nodes[i] != this.cropArea) {
689
- const rectEl = nodes[i].getBoundingClientRect();
690
- nodes[i].style.webkitMask = `linear-gradient(#000 0 0) ${xPx - rectEl.left}px ${yPx - rectEl.top}px / ${widthPx}px ${heightPx}px, linear-gradient(rgba(0, 0, 0, 0.3) 0 0)`;
691
- nodes[i].style.webkitMaskRepeat = 'no-repeat';
692
- }
693
- }
694
- }
695
- const rectParent = this.cropArea.parentElement.getBoundingClientRect();
696
- this.cropArea.style.left = xPx - rectParent.left + 'px';
697
- this.cropArea.style.top = yPx - rectParent.top + 'px';
698
- }
699
- async loadVideo(options = {}) {
700
- this.videoReady = false;
701
- while (this.video.duration === Infinity || isNaN(this.video.duration) || !this.timebar) {
702
- await new Promise((r) => setTimeout(r, 1000));
703
- this.video.currentTime = 10000000 * Math.random();
704
- }
705
- this.video.currentTime = 0.01; // BUG: some videos will not play unless this line is present
706
- // Duration can change if the video is dynamic (stream). This function is to ensure to load all buffer data
707
- const forceLoadChunks = () => {
708
- const state = this.videoReady;
709
- if (this.video.readyState > 3) {
710
- this.videoReady = true;
711
- }
712
- if (!state) {
713
- this.video.currentTime = this.video.duration;
714
- }
715
- };
716
- this.video.addEventListener('canplaythrough', forceLoadChunks, { passive: true });
717
- this.video.ondurationchange = (v) => {
718
- if (this.video.duration != this.endTime) {
719
- this.video.currentTime = this.startTime;
720
- console.log('duration changed from', this.endTime, ' to ', this.video.duration);
721
- this.endTime = this.video.duration;
722
- this.timebar.setDuration(this.endTime);
723
- this.timebar.setEndTime(this.endTime);
724
- }
725
- this.video.currentTime = this.startTime;
726
- this.timebar.setCurrentTime(this.video.currentTime);
727
- };
728
- this.timebar.startX = this.timebar.position.x;
729
- this.timebar.endX = this.timebar.position.x + this.timebar.lineWidth;
730
- this.startTime = 0;
731
- this.endTime = this.video.duration;
732
- this.timebar.setDuration(this.endTime);
733
- this.timebar.setEndTime(this.video.duration);
734
- this.timebar.setStartTime(this.startTime);
735
- this.timebar.setCurrentTime(this.startTime);
736
- // this.timebar.setStartValue( this.timebar.startX);
737
- // this.timebar.currentX = this._timeToX( this.video.currentTime);
738
- // this.timebar.setCurrentValue( this.timebar.currentX);
739
- // this.timebar.update( this.timebar.currentX );
740
- // only have one update on flight
741
- if (!this.requestId) {
742
- this._update();
743
- }
744
- this.controls = options.controls ?? true;
745
- if (!this.controls) {
746
- this.hideControls();
747
- }
748
- this.cropArea.style.height = this.video.clientHeight + 'px';
749
- this.cropArea.style.width = this.video.clientWidth + 'px';
750
- this.moveCropArea(0, 0, true);
751
- this.resizeCropArea(1, 1, true);
752
- if (this.crop) {
753
- this.showCropArea();
754
- }
755
- else {
756
- this.hideCropArea();
757
- }
758
- window.addEventListener('keyup', this.onKeyUp);
759
- if (this.onVideoLoaded) {
760
- this.onVideoLoaded(this.video);
761
- }
762
- }
763
- _update() {
764
- // if( this.onDraw ) {
765
- // this.onDraw();
766
- // }
767
- if (this.playing) {
768
- if (this.video.currentTime + 0.000001 >= this.endTime) {
769
- this.video.pause();
770
- if (!this.loop) {
771
- this.playing = false;
772
- this.controlsPanelLeft.refresh();
773
- }
774
- else {
775
- this.video.currentTime = this.startTime;
776
- this.video.play();
777
- }
778
- }
779
- this._updateTime = false;
780
- this.timebar.setCurrentTime(this.video.currentTime);
781
- this._updateTime = true;
782
- }
783
- this.requestId = requestAnimationFrame(this._update.bind(this));
784
- }
785
- timeToString(t) {
786
- let mzminutes = Math.floor(t / 60);
787
- let mzseconds = Math.floor(t - (mzminutes * 60));
788
- let mzmiliseconds = Math.floor((t - mzseconds) * 100);
789
- let mzmilisecondsStr = mzmiliseconds < 10 ? ('0' + mzmiliseconds) : mzmiliseconds.toString();
790
- let mzsecondsStr = mzseconds < 10 ? ('0' + mzseconds) : mzseconds.toString();
791
- let mzminutesStr = mzminutes < 10 ? ('0' + mzminutes) : mzminutes.toString();
792
- return `${mzminutesStr}:${mzsecondsStr}.${mzmilisecondsStr}`;
793
- }
794
- _setCurrentTime(t) {
795
- if (this.video.currentTime != t && this._updateTime) {
796
- this.video.currentTime = t;
797
- }
798
- this.currentTimeString = this.timeToString(t);
799
- this.controlsCurrentPanel.refresh();
800
- if (this.onSetTime) {
801
- this.onSetTime(t);
802
- }
803
- if (this.onChangeCurrent) {
804
- this.onChangeCurrent(t);
805
- }
806
- }
807
- _setStartTime(t) {
808
- this.startTime = this.video.currentTime = t;
809
- this.startTimeString = this.timeToString(t);
810
- this.controlsPanelLeft.refresh();
811
- if (this.onSetTime) {
812
- this.onSetTime(t);
813
- }
814
- if (this.onChangeStart) {
815
- this.onChangeStart(t);
816
- }
817
- }
818
- _setEndTime(t) {
819
- this.endTime = this.video.currentTime = t;
820
- this.endTimeString = this.timeToString(t);
821
- this.controlsPanelRight.refresh();
822
- if (this.onSetTime) {
823
- this.onSetTime(t);
824
- }
825
- if (this.onChangeEnd) {
826
- this.onChangeEnd(t);
827
- }
828
- }
829
- getStartTime() {
830
- return this.startTime;
831
- }
832
- getEndTime() {
833
- return this.endTime;
834
- }
835
- getTrimedTimes() {
836
- return { start: this.startTime, end: this.endTime };
837
- }
838
- getCroppedArea() {
839
- return this.cropArea.getBoundingClientRect();
840
- }
841
- showCropArea() {
842
- this.cropArea.classList.remove('hidden');
843
- const nodes = this.cropArea.parentElement?.childNodes ?? [];
844
- const rect = this.cropArea.getBoundingClientRect();
845
- for (let i = 0; i < nodes.length; i++) {
846
- const node = nodes[i];
847
- if (node == this.cropArea)
848
- continue;
849
- const rectEl = node.getBoundingClientRect();
850
- node.style.webkitMask = `linear-gradient(#000 0 0) ${rect.left - rectEl.left}px ${rect.top - rectEl.top}px / ${rect.width}px ${rect.height}px, linear-gradient(rgba(0, 0, 0, 0.3) 0 0)`;
851
- node.style.webkitMaskRepeat = 'no-repeat';
852
- }
853
- }
854
- hideCropArea() {
855
- this.cropArea.classList.add('hidden');
856
- const nodes = this.cropArea.parentElement?.childNodes ?? [];
857
- for (let i = 0; i < nodes.length; i++) {
858
- const node = nodes[i];
859
- if (node == this.cropArea)
860
- continue;
861
- node.style.webkitMask = '';
862
- node.style.webkitMaskRepeat = 'no-repeat';
863
- }
864
- }
865
- showControls() {
866
- this.controls = true;
867
- this.controlsArea.show();
868
- }
869
- hideControls() {
870
- this.controls = false;
871
- this.controlsArea.hide();
872
- }
873
- stopUpdates() {
874
- if (this.requestId) {
875
- cancelAnimationFrame(this.requestId);
876
- this.requestId = null;
877
- }
878
- }
879
- unbind() {
880
- this.stopUpdates();
881
- this.video.pause();
882
- this.playing = false;
883
- this.controlsPanelLeft.refresh();
884
- this.video.src = '';
885
- window.removeEventListener('keyup', this.onKeyUp);
886
- document.removeEventListener('mouseup', this._onCropMouseUp);
887
- document.removeEventListener('mousemove', this._onCropMouseMove);
888
- }
889
- }
890
- LX.VideoEditor = VideoEditor;
891
-
892
- export { TimeBar, VideoEditor };
893
- //# sourceMappingURL=VideoEditor.js.map
1
+ // This is a generated file. Do not edit.
2
+ import { LX } from '../core/Namespace.js';
3
+
4
+ // VideoEditor.ts @evallsg
5
+ if (!LX) {
6
+ throw ('Missing LX namespace!');
7
+ }
8
+ LX.extensions.push('VideoEditor');
9
+ const vec2 = LX.vec2;
10
+ LX.Area;
11
+ LX.Panel;
12
+ /**
13
+ * @class TimeBar
14
+ */
15
+ class TimeBar {
16
+ static TIMEBAR_PLAY = 1;
17
+ static TIMEBAR_TRIM = 2;
18
+ static BACKGROUND_COLOR = LX.getCSSVariable('secondary');
19
+ static COLOR = LX.getCSSVariable('accent');
20
+ static ACTIVE_COLOR = LX.getCSSVariable('color-blue-400');
21
+ type = TimeBar.TIMEBAR_PLAY;
22
+ duration = 1.0;
23
+ canvas;
24
+ ctx;
25
+ options;
26
+ markerWidth = 8;
27
+ markerHeight;
28
+ offset;
29
+ lineWidth;
30
+ lineHeight;
31
+ linePosition;
32
+ startX;
33
+ endX;
34
+ currentX;
35
+ hovering;
36
+ dragging;
37
+ _onMouseUpListener;
38
+ _onMouseMoveListener;
39
+ _mouseDownCanvasRect = null;
40
+ updateTheme;
41
+ onChangeCurrent;
42
+ onChangeStart;
43
+ onChangeEnd;
44
+ onDraw;
45
+ onMouse;
46
+ constructor(area, type, options = {}) {
47
+ this.type = type ?? TimeBar.TIMEBAR_PLAY;
48
+ this.options = options ?? {};
49
+ this.duration = options.duration ?? this.duration;
50
+ // Create canvas
51
+ this.canvas = document.createElement('canvas');
52
+ this.canvas.style.borderRadius = '6px';
53
+ this.canvas.width = area.size[0];
54
+ this.canvas.height = area.size[1];
55
+ area.attach(this.canvas);
56
+ this.ctx = this.canvas.getContext('2d');
57
+ this.markerWidth = options.markerWidth ?? this.markerWidth;
58
+ this.markerHeight = (options.markerHeight ?? 0.5) * this.canvas.height;
59
+ const defaultOffset = this.markerWidth * 0.5 + 5;
60
+ if (typeof (options.offset) == 'number') {
61
+ this.offset = new vec2(options.offset, options.offset);
62
+ }
63
+ else if (Array.isArray(options.offset)) {
64
+ this.offset = new vec2(options.offset[0] ?? defaultOffset, options.offset[1] ?? defaultOffset);
65
+ }
66
+ else {
67
+ this.offset = new vec2(defaultOffset, defaultOffset);
68
+ }
69
+ // dimensions of line (not canvas)
70
+ this.lineWidth = this.canvas.width - this.offset.x * 2;
71
+ this.lineHeight = options.barHeight ?? 5;
72
+ this.linePosition = new vec2(this.offset.x, this.canvas.height * 0.5 - this.lineHeight * 0.5);
73
+ this.startX = this.linePosition.x;
74
+ this.endX = this.linePosition.x + this.lineWidth;
75
+ this.currentX = this.startX;
76
+ this._draw();
77
+ function updateTheme() {
78
+ TimeBar.BACKGROUND_COLOR = LX.getCSSVariable('secondary');
79
+ TimeBar.COLOR = LX.getCSSVariable('accent');
80
+ TimeBar.ACTIVE_COLOR = LX.getCSSVariable('color-blue-400');
81
+ }
82
+ this.updateTheme = updateTheme.bind(this);
83
+ LX.addSignal('@on_new_color_scheme', this.updateTheme);
84
+ this.updateTheme();
85
+ // prepare event listeners' functions
86
+ this._onMouseUpListener = this.onMouseUp.bind(this);
87
+ this._onMouseMoveListener = this.onMouseMove.bind(this);
88
+ this.canvas.onmousedown = (e) => this.onMouseDown(e);
89
+ this.canvas.onmousemove = (e) => {
90
+ if (this.dragging)
91
+ return; // already handled by _onMouseMoveListener
92
+ this.onMouseMove(e);
93
+ };
94
+ }
95
+ unbind() {
96
+ removeEventListener('mousemove', this._onMouseMoveListener);
97
+ removeEventListener('mouseup', this._onMouseUpListener);
98
+ }
99
+ setDuration(duration) {
100
+ this.duration = duration;
101
+ }
102
+ xToTime(x) {
103
+ return ((x - this.offset.x) / (this.lineWidth)) * this.duration;
104
+ }
105
+ timeToX(time) {
106
+ return (time / this.duration) * (this.lineWidth) + this.offset.x;
107
+ }
108
+ setCurrentTime(time) {
109
+ this.currentX = this.timeToX(time);
110
+ this.onSetCurrentValue(this.currentX);
111
+ }
112
+ setStartTime(time) {
113
+ this.startX = this.timeToX(time);
114
+ this.onSetStartValue(this.startX);
115
+ }
116
+ setEndTime(time) {
117
+ this.endX = this.timeToX(time);
118
+ this.onSetEndValue(this.endX);
119
+ }
120
+ onSetCurrentValue(x) {
121
+ this.update(x);
122
+ const t = this.xToTime(x);
123
+ if (this.onChangeCurrent) {
124
+ this.onChangeCurrent(t);
125
+ }
126
+ }
127
+ onSetStartValue(x) {
128
+ this.update(x);
129
+ const t = this.xToTime(x);
130
+ if (this.onChangeStart) {
131
+ this.onChangeStart(t);
132
+ }
133
+ }
134
+ onSetEndValue(x) {
135
+ this.update(x);
136
+ const t = this.xToTime(x);
137
+ if (this.onChangeEnd) {
138
+ this.onChangeEnd(t);
139
+ }
140
+ }
141
+ _draw() {
142
+ const ctx = this.ctx;
143
+ if (!ctx)
144
+ return;
145
+ ctx.save();
146
+ ctx.fillStyle = TimeBar.BACKGROUND_COLOR;
147
+ ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
148
+ ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
149
+ // Draw background timeline
150
+ ctx.fillStyle = TimeBar.COLOR;
151
+ ctx.fillRect(this.linePosition.x, this.linePosition.y, this.lineWidth, this.lineHeight);
152
+ // Draw background trimed timeline
153
+ ctx.fillStyle = TimeBar.ACTIVE_COLOR;
154
+ ctx.fillRect(this.startX, this.linePosition.y, this.endX - this.startX, this.lineHeight);
155
+ ctx.restore();
156
+ // Min-Max time markers
157
+ this._drawTrimMarker('start', this.startX, { color: null, fillColor: TimeBar.ACTIVE_COLOR || '#5f88c9' });
158
+ this._drawTrimMarker('end', this.endX, { color: null, fillColor: TimeBar.ACTIVE_COLOR || '#5f88c9' });
159
+ this._drawTimeMarker('current', this.currentX, { color: '#e5e5e5', fillColor: TimeBar.ACTIVE_COLOR || '#5f88c9', width: this.markerWidth });
160
+ if (this.onDraw) {
161
+ this.onDraw();
162
+ }
163
+ }
164
+ _drawTrimMarker(name, x, options = {}) {
165
+ const w = this.markerWidth;
166
+ const h = this.markerHeight;
167
+ const y = this.canvas.height * 0.5 - h * 0.5;
168
+ const ctx = this.ctx;
169
+ if (!ctx)
170
+ return;
171
+ // Shadow
172
+ if (this.hovering == name) {
173
+ ctx.shadowColor = 'white';
174
+ ctx.shadowBlur = 2;
175
+ }
176
+ ctx.globalAlpha = 1;
177
+ ctx.fillStyle = ctx.strokeStyle = options.fillColor || '#111'; // "#FFF";
178
+ ctx.beginPath();
179
+ ctx.roundRect(x - w * 0.5, y, w, h, 2);
180
+ ctx.fill();
181
+ ctx.fillStyle = ctx.strokeStyle = options.fillColor || '#111'; // "#FFF";
182
+ ctx.strokeStyle = 'white';
183
+ ctx.beginPath();
184
+ ctx.lineWidth = 2;
185
+ ctx.moveTo(x, y + 4);
186
+ ctx.lineTo(x, y + h - 4);
187
+ ctx.stroke();
188
+ ctx.shadowBlur = 0;
189
+ }
190
+ _drawTimeMarker(name, x, options = {}) {
191
+ let y = this.offset.y;
192
+ const w = options.width ? options.width : (this.dragging == name ? 6 : 4);
193
+ this.canvas.height - this.offset.y * 2;
194
+ let ctx = this.ctx;
195
+ if (!ctx)
196
+ return;
197
+ ctx.globalAlpha = 1;
198
+ ctx.fillStyle = ctx.strokeStyle = options.fillColor || '#111'; // "#FFF";
199
+ // Shadow
200
+ if (this.hovering == name) {
201
+ ctx.shadowColor = 'white';
202
+ ctx.shadowBlur = 2;
203
+ }
204
+ // Current time line
205
+ ctx.fillStyle = ctx.strokeStyle = 'white';
206
+ ctx.beginPath();
207
+ ctx.moveTo(x, y);
208
+ ctx.lineTo(x, this.linePosition.y + this.lineHeight * 0.5);
209
+ ctx.stroke();
210
+ ctx.closePath();
211
+ ctx.fillStyle = ctx.strokeStyle = options.fillColor || '#111'; // "#FFF";
212
+ // Current time ball grab
213
+ ctx.fillStyle = options.fillColor || '#e5e5e5';
214
+ ctx.beginPath();
215
+ ctx.roundRect(x - w * 0.5, y - w * 0.5, w, w, 5);
216
+ ctx.fill();
217
+ ctx.shadowBlur = 0;
218
+ }
219
+ update(x) {
220
+ this.currentX = Math.min(Math.max(this.startX, x), this.endX);
221
+ this._draw();
222
+ }
223
+ onMouseDown(e) {
224
+ if (this.onMouse) {
225
+ this.onMouse(e);
226
+ }
227
+ e.preventDefault();
228
+ if (!this.canvas || e.target != this.canvas || e.cancelBubble) {
229
+ return;
230
+ }
231
+ const canvas = this.canvas;
232
+ // Process mouse
233
+ const x = e.offsetX;
234
+ const y = e.offsetY;
235
+ // Check if some marker is clicked
236
+ const threshold = this.markerWidth;
237
+ const startDist = Math.abs(this.startX - x);
238
+ const endDist = Math.abs(this.endX - x);
239
+ // grab trim markers only from the bottom
240
+ if ((startDist < threshold || endDist < threshold) && this.linePosition.y < y) {
241
+ this.dragging = (startDist < endDist || x < this.startX) ? 'start' : 'end';
242
+ canvas.style.cursor = 'grabbing';
243
+ }
244
+ else {
245
+ this.dragging = 'current';
246
+ canvas.style.cursor = 'grabbing';
247
+ if (x < this.startX) {
248
+ this.currentX = this.startX;
249
+ }
250
+ else if (x > this.endX) {
251
+ this.currentX = this.endX;
252
+ }
253
+ else {
254
+ this.currentX = x;
255
+ }
256
+ this.onSetCurrentValue(this.currentX);
257
+ }
258
+ this._mouseDownCanvasRect = canvas.getBoundingClientRect(); // cache this to avoid stalls during mousemove
259
+ window.addEventListener('mousemove', this._onMouseMoveListener);
260
+ window.addEventListener('mouseup', this._onMouseUpListener);
261
+ this._draw();
262
+ }
263
+ onMouseUp(e) {
264
+ window.removeEventListener('mousemove', this._onMouseMoveListener);
265
+ window.removeEventListener('mouseup', this._onMouseUpListener);
266
+ if (this.onMouse) {
267
+ this.onMouse(e);
268
+ }
269
+ e.preventDefault();
270
+ delete this.dragging;
271
+ delete this.hovering;
272
+ if (!this.canvas || e.cancelBubble) {
273
+ return;
274
+ }
275
+ const canvas = this.canvas;
276
+ canvas.style.cursor = 'default';
277
+ }
278
+ onMouseMove(e) {
279
+ if (this.onMouse) {
280
+ this.onMouse(e);
281
+ }
282
+ if (!this.canvas || e.cancelBubble) {
283
+ return;
284
+ }
285
+ e.preventDefault();
286
+ const canvas = this.canvas;
287
+ // Process mouse
288
+ const x = e.target == canvas ? e.offsetX : (e.clientX - this._mouseDownCanvasRect.left);
289
+ e.target == canvas ? e.offsetY : (e.clientY - this._mouseDownCanvasRect.top);
290
+ if (this.dragging) {
291
+ switch (this.dragging) {
292
+ case 'start':
293
+ this.startX = Math.max(this.linePosition.x, Math.min(this.endX, x));
294
+ this.currentX = this.startX;
295
+ this.onSetStartValue(this.startX);
296
+ break;
297
+ case 'end':
298
+ this.endX = Math.max(this.startX, Math.min(this.linePosition.x + this.lineWidth, x));
299
+ this.currentX = this.endX;
300
+ this.onSetEndValue(this.endX);
301
+ break;
302
+ default:
303
+ this.currentX = Math.max(this.startX, Math.min(this.endX, x));
304
+ break;
305
+ }
306
+ this.onSetCurrentValue(this.currentX);
307
+ }
308
+ else {
309
+ const threshold = this.markerWidth * 0.5;
310
+ if (Math.abs(this.startX - x) < threshold) {
311
+ this.hovering = 'start';
312
+ canvas.style.cursor = 'grab';
313
+ }
314
+ else if (Math.abs(this.endX - x) < threshold) {
315
+ this.hovering = 'end';
316
+ canvas.style.cursor = 'grab';
317
+ }
318
+ else if (Math.abs(this.currentX - x) < threshold) {
319
+ this.hovering = 'current';
320
+ canvas.style.cursor = 'grab';
321
+ }
322
+ else {
323
+ delete this.hovering;
324
+ canvas.style.cursor = 'default';
325
+ }
326
+ }
327
+ this._draw();
328
+ }
329
+ resize(size) {
330
+ this.canvas.width = Math.max(0, size[0]);
331
+ this.canvas.height = Math.max(0, size[1]);
332
+ this.markerHeight = (this.options.markerHeight ?? 0.5) * this.canvas.height;
333
+ let newWidth = size[0] - this.offset.x * 2;
334
+ newWidth = newWidth < 0.00001 ? 0.00001 : newWidth; // actual width of the line = canvas.width - offsetleft - offsetRight
335
+ const startRatio = (this.startX - this.offset.x) / this.lineWidth;
336
+ const currentRatio = (this.currentX - this.offset.x) / this.lineWidth;
337
+ const endRatio = (this.endX - this.offset.x) / this.lineWidth;
338
+ this.lineWidth = newWidth;
339
+ this.linePosition.x = this.offset.x;
340
+ this.linePosition.y = this.canvas.height * 0.5 - this.lineHeight * 0.5;
341
+ this.startX = Math.min(Math.max(newWidth * startRatio, 0), newWidth) + this.offset.x;
342
+ this.currentX = Math.min(Math.max(newWidth * currentRatio, 0), newWidth) + this.offset.x;
343
+ this.endX = Math.min(Math.max(newWidth * endRatio, 0), newWidth) + this.offset.x;
344
+ this._draw();
345
+ }
346
+ }
347
+ LX.TimeBar = TimeBar;
348
+ /**
349
+ * @class VideoEditor
350
+ */
351
+ class VideoEditor {
352
+ static CROP_HANDLE_L = 0x01;
353
+ static CROP_HANDLE_R = 0x02;
354
+ static CROP_HANDLE_T = 0x04;
355
+ static CROP_HANDLE_B = 0x08;
356
+ static CROP_HANDLE_TL = VideoEditor.CROP_HANDLE_L | VideoEditor.CROP_HANDLE_T;
357
+ static CROP_HANDLE_BL = VideoEditor.CROP_HANDLE_L | VideoEditor.CROP_HANDLE_B;
358
+ static CROP_HANDLE_TR = VideoEditor.CROP_HANDLE_R | VideoEditor.CROP_HANDLE_T;
359
+ static CROP_HANDLE_BR = VideoEditor.CROP_HANDLE_R | VideoEditor.CROP_HANDLE_B;
360
+ options = {};
361
+ playing = false;
362
+ videoReady = false;
363
+ controls = true;
364
+ speed = 1.0;
365
+ startTime = 0.0;
366
+ endTime = 0.0;
367
+ requestId;
368
+ video;
369
+ loop = false;
370
+ isDragging = false;
371
+ isResizing = null; // holds the HTMLElement of the crop handle, if resizing
372
+ crop = false;
373
+ dragOffsetX = 0.0;
374
+ dragOffsetY = 0.0;
375
+ timebar = null;
376
+ mainArea;
377
+ cropArea; // HTMLElement with normCoord attribute;
378
+ videoArea;
379
+ controlsArea;
380
+ controlsComponents;
381
+ onChangeCurrent;
382
+ onChangeStart;
383
+ onChangeEnd;
384
+ onKeyUp;
385
+ onSetTime;
386
+ onVideoLoaded;
387
+ onResize;
388
+ onCropArea;
389
+ onChangeSpeed;
390
+ onChangeState;
391
+ onChangeLoop;
392
+ _updateTime = true;
393
+ _onCropMouseUp;
394
+ _onCropMouseMove;
395
+ resize = null;
396
+ resizeControls = null;
397
+ resizeVideo = null;
398
+ constructor(area, options = {}) {
399
+ this.options = options ?? {};
400
+ this.speed = options.speed ?? this.speed;
401
+ this.mainArea = area;
402
+ let videoArea = null;
403
+ let controlsArea = null;
404
+ if (options.controlsArea) {
405
+ videoArea = area;
406
+ controlsArea = options.controlsArea;
407
+ }
408
+ else {
409
+ [videoArea, controlsArea] = area.split({ type: 'vertical', sizes: ['85%', null], minimizable: false, resize: false });
410
+ }
411
+ controlsArea.root.classList.add('lexconstrolsarea');
412
+ this.cropArea = document.createElement('div');
413
+ this.cropArea.id = 'cropArea';
414
+ this.cropArea.className = 'resize-area hidden';
415
+ this.cropArea.normCoords = { x: 0, y: 0, w: 1, h: 1 };
416
+ const flags = 0x0f;
417
+ this.setCropAreaHandles(flags);
418
+ this.crop = options.crop;
419
+ this.dragOffsetX = 0;
420
+ this.dragOffsetY = 0;
421
+ // Create video element and load it
422
+ let video = this.video = options.video ?? document.createElement('video');
423
+ this.loop = options.loop ?? this.loop;
424
+ if (options.src) {
425
+ this.video.src = options.src;
426
+ this.loadVideo(options);
427
+ }
428
+ if (options.videoArea) {
429
+ options.videoArea.root.classList.add('lexvideoeditor');
430
+ options.videoArea.root.style.position = 'relative';
431
+ options.videoArea.attach(this.cropArea);
432
+ videoArea.attach(options.videoArea);
433
+ }
434
+ else {
435
+ videoArea.attach(video);
436
+ videoArea.attach(this.cropArea);
437
+ videoArea.root.classList.add('lexvideoeditor');
438
+ }
439
+ videoArea.root.style.position = 'relative';
440
+ this.videoArea = videoArea;
441
+ this.controlsArea = controlsArea;
442
+ this.controlsComponents = {
443
+ timebar: null,
444
+ playBtn: null,
445
+ speedBtn: null,
446
+ loopBtn: null,
447
+ trimStartText: null,
448
+ trimEndText: null,
449
+ curTimeText: null,
450
+ resetCropBtn: null
451
+ };
452
+ this.createControls();
453
+ this.resizeVideo = () => {
454
+ this.moveCropArea(this.cropArea.normCoords.x, this.cropArea.normCoords.y, true);
455
+ this.resizeCropArea(this.cropArea.normCoords.w, this.cropArea.normCoords.h, true);
456
+ if (this.onResize) {
457
+ this.onResize([videoArea.root.clientWidth, videoArea.root.clientHeight]);
458
+ }
459
+ };
460
+ this.resize = () => {
461
+ this.resizeVideo();
462
+ this.resizeControls();
463
+ };
464
+ area.onresize = this.resize.bind(this);
465
+ window.addEventListener('resize', area.onresize);
466
+ this.onKeyUp = (e) => {
467
+ if (this.controls && e.key == ' ') {
468
+ e.preventDefault();
469
+ e.stopPropagation();
470
+ // do not skip callback
471
+ this.controlsComponents.playBtn?.setState(!this.playing, false);
472
+ }
473
+ };
474
+ window.addEventListener('keyup', this.onKeyUp);
475
+ this._onCropMouseUp = (event) => {
476
+ event.preventDefault();
477
+ event.stopPropagation();
478
+ if ((this.isDragging || this.isResizing) && this.onCropArea) {
479
+ this.onCropArea(this.getCroppedArea());
480
+ }
481
+ this.isDragging = false;
482
+ this.isResizing = false;
483
+ document.removeEventListener('mouseup', this._onCropMouseUp); // self destroy. Added during mouseDown on cropArea and handles
484
+ document.removeEventListener('mousemove', this._onCropMouseMove); // self destroy. Added during mouseDown on cropArea and handles
485
+ };
486
+ this._onCropMouseMove = (event) => {
487
+ window.getSelection()?.removeAllRanges();
488
+ event.preventDefault();
489
+ event.stopPropagation();
490
+ if (this.isResizing) {
491
+ const rectCrop = this.cropArea.getBoundingClientRect();
492
+ const rectVideo = this.video.getBoundingClientRect();
493
+ const mov = this.isResizing.movement;
494
+ let x = rectCrop.left, y = rectCrop.top, w = rectCrop.width, h = rectCrop.height;
495
+ if (mov & VideoEditor.CROP_HANDLE_L) {
496
+ let mouseX = Math.min(rectCrop.right - 4, Math.max(rectVideo.left, event.clientX)); // -4 because of border
497
+ w = rectCrop.left + rectCrop.width - mouseX;
498
+ x = mouseX;
499
+ if (mouseX < rectCrop.left) {
500
+ this.moveCropArea(x, y, false);
501
+ this.resizeCropArea(w, h, false);
502
+ }
503
+ else {
504
+ this.resizeCropArea(w, h, false);
505
+ this.moveCropArea(x, y, false);
506
+ }
507
+ }
508
+ if (mov & VideoEditor.CROP_HANDLE_R) {
509
+ w = event.clientX - rectCrop.left;
510
+ this.resizeCropArea(w, h, false);
511
+ }
512
+ if (mov & VideoEditor.CROP_HANDLE_T) {
513
+ const mouseY = Math.min(rectCrop.bottom - 4, Math.max(rectVideo.top, event.clientY));
514
+ h = rectCrop.top + rectCrop.height - mouseY;
515
+ y = mouseY;
516
+ if (mouseY < rectCrop.top) {
517
+ this.moveCropArea(x, y, false);
518
+ this.resizeCropArea(w, h, false);
519
+ }
520
+ else {
521
+ this.resizeCropArea(w, h, false);
522
+ this.moveCropArea(x, y, false);
523
+ }
524
+ }
525
+ if (mov & VideoEditor.CROP_HANDLE_B) {
526
+ h = event.clientY - rectCrop.top;
527
+ this.resizeCropArea(w, h, false);
528
+ }
529
+ }
530
+ if (this.isDragging) {
531
+ this.moveCropArea(event.clientX - this.dragOffsetX, event.clientY - this.dragOffsetY, false);
532
+ }
533
+ };
534
+ this.cropArea.addEventListener('mousedown', (e) => {
535
+ if (e.target === this.cropArea) {
536
+ const rect = this.cropArea.getBoundingClientRect();
537
+ this.isDragging = true;
538
+ this.dragOffsetX = e.clientX - rect.left;
539
+ this.dragOffsetY = e.clientY - rect.top;
540
+ document.addEventListener('mouseup', this._onCropMouseUp);
541
+ document.addEventListener('mousemove', this._onCropMouseMove);
542
+ }
543
+ });
544
+ this.onChangeStart = null;
545
+ this.onChangeEnd = null;
546
+ }
547
+ createControls(options = null) {
548
+ const controlsArea = this.controlsArea;
549
+ options = options ?? this.options;
550
+ // clear area. Signals are not cleared !!! (not a problem if there are no signals)
551
+ while (controlsArea.root.children.length) {
552
+ controlsArea.root.children[0].remove();
553
+ }
554
+ controlsArea.sections.length = 0;
555
+ // start trimming text
556
+ this.controlsComponents.trimStartText = new LX.TextInput(null, this.timeToString(this.startTime), null, { width: '100px',
557
+ title: 'Trimmed Start Time', disabled: true, inputClass: 'bg-none' });
558
+ this.controlsComponents.trimEndText = new LX.TextInput(null, this.timeToString(this.endTime), null, { width: '100px',
559
+ title: 'Trimmed End Time', disabled: true, inputClass: 'bg-none' });
560
+ this.controlsComponents.curTimeText = new LX.TextInput(null, this.video.currentTime, null, { title: 'Current Time', float: 'center',
561
+ disabled: true, inputClass: 'bg-none' });
562
+ // reset crop area
563
+ this.controlsComponents.resetCropBtn = new LX.Button('ResetCrop', null, (v) => {
564
+ this.moveCropArea(0, 0, true);
565
+ this.resizeCropArea(1, 1, true);
566
+ if (this.onCropArea) {
567
+ this.onCropArea(this.getCroppedArea());
568
+ }
569
+ }, { width: '40px', title: 'Reset Crop Area', icon: 'Crop@solid', hideName: true,
570
+ className: 'justify-center' + (this.crop ? '' : ' hidden') });
571
+ // play button
572
+ this.controlsComponents.playBtn = new LX.Button('Play', '', (v) => {
573
+ this.playing = v;
574
+ if (this.playing) {
575
+ if (this.video.currentTime + 0.000001 >= this.endTime) {
576
+ this.video.currentTime = this.startTime;
577
+ }
578
+ this.video.play();
579
+ }
580
+ else {
581
+ this.video.pause();
582
+ }
583
+ if (this.onChangeState) {
584
+ this.onChangeState(v);
585
+ }
586
+ }, { width: '40px', title: 'Play/Pause', icon: 'Play@solid', swap: 'Pause@solid', hideName: true, className: 'justify-center' });
587
+ this.controlsComponents.playBtn.setState(this.playing, true);
588
+ // speed button
589
+ this.controlsComponents.speedBtn = new LX.Button('Speed', '', (v, e) => {
590
+ const panel = new LX.Panel();
591
+ panel.addRange('Speed', this.speed, (v) => {
592
+ this.speed = v;
593
+ this.video.playbackRate = v;
594
+ if (this.onChangeSpeed) {
595
+ this.onChangeSpeed(v);
596
+ }
597
+ }, { min: 0, max: 2.5, step: 0.01, hideName: true });
598
+ new LX.Popover(e.target, [panel], { align: 'start', side: 'top', sideOffset: 12 });
599
+ }, { width: '40px', title: 'Speed', hideName: true, icon: 'Timer@solid', className: 'justify-center' });
600
+ // loop button
601
+ this.controlsComponents.loopBtn = new LX.Button('', 'Loop', (v) => {
602
+ this.loop = v;
603
+ if (this.onChangeLoop) {
604
+ this.onChangeLoop(v);
605
+ }
606
+ }, { width: '40px', hideName: true, title: 'Loop', icon: 'Repeat@solid', className: `justify-center`, selectable: true,
607
+ selected: this.loop });
608
+ let timeBarArea = null;
609
+ if (typeof (options.controlsLayout) == 'function') {
610
+ timeBarArea = options.controlsLayout;
611
+ }
612
+ else if (options.controlsLayout == 1) {
613
+ timeBarArea = this._createControlsLayout_1();
614
+ }
615
+ else {
616
+ timeBarArea = this._createControlsLayout_0();
617
+ }
618
+ if (this.timebar) {
619
+ this.timebar.unbind();
620
+ }
621
+ this.timebar = this.controlsComponents.timebar = new TimeBar(timeBarArea, TimeBar.TIMEBAR_TRIM, { offset: [12, null] });
622
+ this.timebar.onChangeCurrent = this._setCurrentTime.bind(this);
623
+ this.timebar.onChangeStart = this._setStartTime.bind(this);
624
+ this.timebar.onChangeEnd = this._setEndTime.bind(this);
625
+ let duration = 1;
626
+ if (this.video.duration !== Infinity && !isNaN(this.video.duration)) {
627
+ duration = this.video.duration;
628
+ }
629
+ this.timebar.setDuration(duration);
630
+ this.timebar.setEndTime(this.endTime);
631
+ this.timebar.setStartTime(this.startTime);
632
+ this.timebar.setCurrentTime(this.startTime);
633
+ this.resizeControls();
634
+ }
635
+ /**
636
+ * Creates the areas where components will be.
637
+ * Attaches all (desired) components of controlsComponents except the timebar
638
+ * @returns {Area} for the timebar
639
+ * Layout:
640
+ * |--------------------------timebar--------------------------|
641
+ * play speed loop resetCrop curTime trimStart / trimEnd
642
+ */
643
+ _createControlsLayout_1() {
644
+ const controlsArea = this.controlsArea;
645
+ // Create playing timeline area and attach panels
646
+ let [timeBarArea, bottomArea] = controlsArea.split({ type: 'vertical', sizes: ['50%', null], minimizable: false, resize: false });
647
+ bottomArea.root.classList.add('relative');
648
+ let separator = document.createElement('p');
649
+ separator.style.alignContent = 'center';
650
+ separator.innerText = '/';
651
+ let trimDiv = LX.makeContainer(['fit-content', '100%'], 'relative flex flex-row pb-2', null, bottomArea, { float: 'right' });
652
+ trimDiv.appendChild(this.controlsComponents.trimStartText.root);
653
+ trimDiv.appendChild(separator);
654
+ trimDiv.appendChild(this.controlsComponents.trimEndText.root);
655
+ this.controlsComponents.trimStartText.root.querySelector('input').classList.add('text-end');
656
+ this.controlsComponents.trimStartText.root.classList.add('top-0', 'bottom-0');
657
+ this.controlsComponents.trimEndText.root.classList.add('top-0', 'bottom-0');
658
+ // current time
659
+ let curTimeDiv = LX.makeContainer(['100%', '100%'], 'absolute top-0 left-0 flex flex-row justify-center items-center pb-2', null, bottomArea, {});
660
+ curTimeDiv.appendChild(this.controlsComponents.curTimeText.root);
661
+ // Buttons
662
+ const buttonsPanel = bottomArea.addPanel({ className: 'absolute top-0 left-0 flex flex-row pl-4 pr-4 pt-1 pb-2' });
663
+ buttonsPanel.root.classList.remove('pad-md');
664
+ buttonsPanel._attachComponent(this.controlsComponents.playBtn);
665
+ buttonsPanel._attachComponent(this.controlsComponents.speedBtn);
666
+ buttonsPanel._attachComponent(this.controlsComponents.loopBtn);
667
+ buttonsPanel._attachComponent(this.controlsComponents.resetCropBtn);
668
+ this.controlsComponents.playBtn.root.classList.add('pl-0');
669
+ this.controlsComponents.resetCropBtn.root.classList.add('pr-0');
670
+ // timebar
671
+ timeBarArea.root.classList.add('p-4', 'pb-0');
672
+ this.resizeControls = () => {
673
+ const style = getComputedStyle(timeBarArea.root);
674
+ let pleft = parseFloat(style.paddingLeft);
675
+ let pright = parseFloat(style.paddingRight);
676
+ let ptop = parseFloat(style.paddingTop);
677
+ let pbot = parseFloat(style.paddingBottom);
678
+ // assuming timeBarArea will not overflow
679
+ this.timebar.resize([timeBarArea.root.clientWidth - pleft - pright, timeBarArea.root.clientHeight - ptop - pbot]);
680
+ };
681
+ return timeBarArea;
682
+ }
683
+ /**
684
+ * Creates the areas where components will be.
685
+ * Attaches all (desired) components of controlsComponents except the timebar
686
+ * @returns {Area} for the timebar
687
+ * Layout:
688
+ * curTime
689
+ * play speed loop trimStart |---timebar---| trimend
690
+ */
691
+ _createControlsLayout_0() {
692
+ const controlsArea = this.controlsArea;
693
+ // Create playing timeline area and attach panels
694
+ let [topArea, bottomArea] = controlsArea.split({ type: 'vertical', sizes: ['50%', null], minimizable: false, resize: false });
695
+ bottomArea.setSize([bottomArea.size[0], 40]);
696
+ let [leftArea, controlsRight] = bottomArea.split({ type: 'horizontal', sizes: ['92%', null], minimizable: false, resize: false });
697
+ let [controlsLeft, timeBarArea] = leftArea.split({ type: 'horizontal', sizes: ['10%', null], minimizable: false, resize: false });
698
+ const controlsCurrentPanel = topArea.addPanel({ className: 'flex' });
699
+ controlsCurrentPanel._attachComponent(this.controlsComponents.curTimeText);
700
+ // Create controls panel (play/pause button and start time)
701
+ controlsLeft.root.classList.add('min-w-fit');
702
+ const controlsPanelLeft = controlsLeft.addPanel({ className: 'lexcontrolspanel p-0 pl-2' });
703
+ controlsPanelLeft.root.classList.remove('pad-md');
704
+ controlsPanelLeft.sameLine();
705
+ controlsPanelLeft._attachComponent(this.controlsComponents.playBtn);
706
+ controlsPanelLeft._attachComponent(this.controlsComponents.speedBtn);
707
+ controlsPanelLeft._attachComponent(this.controlsComponents.loopBtn);
708
+ controlsPanelLeft._attachComponent(this.controlsComponents.trimStartText);
709
+ controlsPanelLeft.endLine();
710
+ // Create right controls panel (end time)
711
+ controlsRight.root.classList.add('min-w-fit');
712
+ const controlsPanelRight = controlsRight.addPanel({ className: 'lexcontrolspanel p-0' });
713
+ controlsPanelRight.root.classList.remove('pad-md');
714
+ controlsPanelRight._attachComponent(this.controlsComponents.trimEndText);
715
+ this.resizeControls = () => {
716
+ bottomArea.setSize([this.controlsArea.root.clientWidth, 40]);
717
+ let availableWidth = this.controlsArea.root.clientWidth - controlsLeft.root.clientWidth
718
+ - controlsRight.root.clientWidth;
719
+ this.timebar.resize([availableWidth, timeBarArea.root.clientHeight]);
720
+ };
721
+ return timeBarArea;
722
+ }
723
+ setCropAreaHandles(flags) {
724
+ // remove existing resizer handles
725
+ const resizers = this.cropArea.getElementsByClassName('resize-handle');
726
+ for (let i = resizers.length - 1; i > -1; --i) {
727
+ resizers[i].remove();
728
+ }
729
+ const buildResizer = (className, movement) => {
730
+ const handle = document.createElement('div');
731
+ handle.className = ' resize-handle ' + className;
732
+ handle.movement = movement;
733
+ if (this.options.handleStyle) {
734
+ Object.assign(handle.style, this.options.handleStyle);
735
+ }
736
+ this.cropArea.append(handle);
737
+ handle.addEventListener('mousedown', (e) => {
738
+ e.stopPropagation();
739
+ e.preventDefault();
740
+ this.isResizing = handle;
741
+ document.addEventListener('mouseup', this._onCropMouseUp);
742
+ document.addEventListener('mousemove', this._onCropMouseMove);
743
+ });
744
+ };
745
+ if (flags & VideoEditor.CROP_HANDLE_L)
746
+ buildResizer('l', VideoEditor.CROP_HANDLE_L);
747
+ if (flags & VideoEditor.CROP_HANDLE_R)
748
+ buildResizer('r', VideoEditor.CROP_HANDLE_R);
749
+ if (flags & VideoEditor.CROP_HANDLE_T)
750
+ buildResizer('t', VideoEditor.CROP_HANDLE_T);
751
+ if (flags & VideoEditor.CROP_HANDLE_B)
752
+ buildResizer('b', VideoEditor.CROP_HANDLE_B);
753
+ if ((flags & VideoEditor.CROP_HANDLE_TL) == VideoEditor.CROP_HANDLE_TL) {
754
+ buildResizer('tl', VideoEditor.CROP_HANDLE_TL);
755
+ }
756
+ if ((flags & VideoEditor.CROP_HANDLE_BL) == VideoEditor.CROP_HANDLE_BL) {
757
+ buildResizer('bl', VideoEditor.CROP_HANDLE_BL);
758
+ }
759
+ if ((flags & VideoEditor.CROP_HANDLE_TR) == VideoEditor.CROP_HANDLE_TR) {
760
+ buildResizer('tr', VideoEditor.CROP_HANDLE_TR);
761
+ }
762
+ if ((flags & VideoEditor.CROP_HANDLE_BR) == VideoEditor.CROP_HANDLE_BR) {
763
+ buildResizer('br', VideoEditor.CROP_HANDLE_BR);
764
+ }
765
+ }
766
+ resizeCropArea(sx, sy, isNormalized = true) {
767
+ const rectVideo = this.video.getBoundingClientRect();
768
+ if (!isNormalized) {
769
+ sx = (rectVideo.width) ? (sx / rectVideo.width) : 1;
770
+ sy = (rectVideo.height) ? (sy / rectVideo.height) : 1;
771
+ }
772
+ sx = Math.min(1 - this.cropArea.normCoords.x, Math.max(0, sx));
773
+ sy = Math.min(1 - this.cropArea.normCoords.y, Math.max(0, sy));
774
+ this.cropArea.normCoords.w = sx;
775
+ this.cropArea.normCoords.h = sy;
776
+ const widthPx = rectVideo.width * sx;
777
+ const heightPx = rectVideo.height * sy;
778
+ const xPx = rectVideo.width * this.cropArea.normCoords.x + rectVideo.left;
779
+ const yPx = rectVideo.height * this.cropArea.normCoords.y + rectVideo.top;
780
+ if (!this.cropArea.classList.contains('hidden')) {
781
+ const nodes = this.cropArea.parentElement.childNodes;
782
+ for (let i = 0; i < nodes.length; i++) {
783
+ if (nodes[i] != this.cropArea) {
784
+ const rectEl = nodes[i].getBoundingClientRect();
785
+ nodes[i].style.webkitMask = `linear-gradient(#000 0 0) ${xPx - rectEl.left}px ${yPx - rectEl.top}px / ${widthPx}px ${heightPx}px, linear-gradient(rgba(0, 0, 0, 0.3) 0 0)`;
786
+ nodes[i].style.webkitMaskRepeat = 'no-repeat';
787
+ }
788
+ }
789
+ }
790
+ this.cropArea.style.width = widthPx + 'px';
791
+ this.cropArea.style.height = heightPx + 'px';
792
+ }
793
+ // screen pixel (event.clientX) or video normalized (0 is top left of video, 1 bot right)
794
+ moveCropArea(x, y, isNormalized = true) {
795
+ const rectVideo = this.video.getBoundingClientRect();
796
+ if (!isNormalized) {
797
+ x = (rectVideo.width) ? ((x - rectVideo.left) / rectVideo.width) : 0;
798
+ y = (rectVideo.height) ? ((y - rectVideo.top) / rectVideo.height) : 0;
799
+ }
800
+ x = Math.max(0, Math.min(1 - this.cropArea.normCoords.w, x));
801
+ y = Math.max(0, Math.min(1 - this.cropArea.normCoords.h, y));
802
+ this.cropArea.normCoords.x = x;
803
+ this.cropArea.normCoords.y = y;
804
+ const xPx = rectVideo.width * x + rectVideo.left;
805
+ const yPx = rectVideo.height * y + rectVideo.top;
806
+ const widthPx = rectVideo.width * this.cropArea.normCoords.w;
807
+ const heightPx = rectVideo.height * this.cropArea.normCoords.h;
808
+ if (!this.cropArea.classList.contains('hidden')) {
809
+ const nodes = this.cropArea.parentElement.childNodes;
810
+ for (let i = 0; i < nodes.length; i++) {
811
+ if (nodes[i] != this.cropArea) {
812
+ const rectEl = nodes[i].getBoundingClientRect();
813
+ nodes[i].style.webkitMask = `linear-gradient(#000 0 0) ${xPx - rectEl.left}px ${yPx - rectEl.top}px / ${widthPx}px ${heightPx}px, linear-gradient(rgba(0, 0, 0, 0.3) 0 0)`;
814
+ nodes[i].style.webkitMaskRepeat = 'no-repeat';
815
+ }
816
+ }
817
+ }
818
+ const rectParent = this.cropArea.parentElement.getBoundingClientRect();
819
+ this.cropArea.style.left = xPx - rectParent.left + 'px';
820
+ this.cropArea.style.top = yPx - rectParent.top + 'px';
821
+ }
822
+ async loadVideo(options = {}) {
823
+ this.videoReady = false;
824
+ while (this.video.duration === Infinity || isNaN(this.video.duration) || !this.timebar) {
825
+ await new Promise((r) => setTimeout(r, 1000));
826
+ this.video.currentTime = 10000000 * Math.random();
827
+ }
828
+ this.video.currentTime = 0.01; // BUG: some videos will not play unless this line is present
829
+ // Duration can change if the video is dynamic (stream). This function is to ensure to load all buffer data
830
+ const forceLoadChunks = () => {
831
+ const state = this.videoReady;
832
+ if (this.video.readyState > 3) {
833
+ this.videoReady = true;
834
+ }
835
+ if (!state) {
836
+ this.video.currentTime = this.video.duration;
837
+ }
838
+ };
839
+ this.video.addEventListener('canplaythrough', forceLoadChunks, { passive: true });
840
+ this.video.ondurationchange = (v) => {
841
+ if (this.video.duration != this.endTime) {
842
+ this.video.currentTime = this.startTime;
843
+ console.log('duration changed from', this.endTime, ' to ', this.video.duration);
844
+ this.endTime = this.video.duration;
845
+ this.timebar.setDuration(this.endTime);
846
+ this.timebar.setEndTime(this.endTime);
847
+ }
848
+ this.video.currentTime = this.startTime;
849
+ this.timebar.setCurrentTime(this.video.currentTime);
850
+ };
851
+ this.startTime = 0;
852
+ this.endTime = this.video.duration;
853
+ this.timebar.setDuration(this.endTime);
854
+ this.timebar.setEndTime(this.video.duration);
855
+ this.timebar.setStartTime(this.startTime);
856
+ this.timebar.setCurrentTime(this.startTime);
857
+ // only have one update on flight
858
+ if (!this.requestId) {
859
+ this._update();
860
+ }
861
+ this.controls = options.controls ?? true;
862
+ if (!this.controls) {
863
+ this.hideControls();
864
+ }
865
+ this.cropArea.style.height = this.video.clientHeight + 'px';
866
+ this.cropArea.style.width = this.video.clientWidth + 'px';
867
+ this.moveCropArea(0, 0, true);
868
+ this.resizeCropArea(1, 1, true);
869
+ if (this.crop) {
870
+ this.showCropArea();
871
+ }
872
+ else {
873
+ this.hideCropArea();
874
+ }
875
+ window.addEventListener('keyup', this.onKeyUp);
876
+ if (this.onVideoLoaded) {
877
+ this.onVideoLoaded(this.video);
878
+ }
879
+ }
880
+ _update() {
881
+ // if( this.onDraw ) {
882
+ // this.onDraw();
883
+ // }
884
+ if (this.playing) {
885
+ if (this.video.currentTime + 0.000001 >= this.endTime) {
886
+ this.video.pause();
887
+ if (!this.loop) {
888
+ this.playing = false;
889
+ this.controlsComponents.playBtn?.setState(false, true); // skip callback
890
+ }
891
+ else {
892
+ this.video.currentTime = this.startTime;
893
+ this.video.play();
894
+ }
895
+ }
896
+ this._updateTime = false;
897
+ this.timebar.setCurrentTime(this.video.currentTime);
898
+ this._updateTime = true;
899
+ }
900
+ this.requestId = requestAnimationFrame(this._update.bind(this));
901
+ }
902
+ timeToString(t) {
903
+ let mzminutes = Math.floor(t / 60);
904
+ let mzseconds = Math.floor(t - (mzminutes * 60));
905
+ let mzmiliseconds = Math.floor((t - mzseconds) * 100);
906
+ let mzmilisecondsStr = mzmiliseconds < 10 ? ('0' + mzmiliseconds) : mzmiliseconds.toString();
907
+ let mzsecondsStr = mzseconds < 10 ? ('0' + mzseconds) : mzseconds.toString();
908
+ let mzminutesStr = mzminutes < 10 ? ('0' + mzminutes) : mzminutes.toString();
909
+ return `${mzminutesStr}:${mzsecondsStr}.${mzmilisecondsStr}`;
910
+ }
911
+ _setCurrentTime(t) {
912
+ if (this.video.currentTime != t && this._updateTime) {
913
+ this.video.currentTime = t;
914
+ }
915
+ this.controlsComponents.curTimeText?.set(this.timeToString(t));
916
+ if (this.onSetTime) {
917
+ this.onSetTime(t);
918
+ }
919
+ if (this.onChangeCurrent) {
920
+ this.onChangeCurrent(t);
921
+ }
922
+ }
923
+ _setStartTime(t) {
924
+ this.startTime = this.video.currentTime = t;
925
+ this.controlsComponents.trimStartText?.set(this.timeToString(t));
926
+ if (this.onSetTime) {
927
+ this.onSetTime(t);
928
+ }
929
+ if (this.onChangeStart) {
930
+ this.onChangeStart(t);
931
+ }
932
+ }
933
+ _setEndTime(t) {
934
+ this.endTime = this.video.currentTime = t;
935
+ this.controlsComponents.trimEndText?.set(this.timeToString(t));
936
+ if (this.onSetTime) {
937
+ this.onSetTime(t);
938
+ }
939
+ if (this.onChangeEnd) {
940
+ this.onChangeEnd(t);
941
+ }
942
+ }
943
+ getStartTime() {
944
+ return this.startTime;
945
+ }
946
+ getEndTime() {
947
+ return this.endTime;
948
+ }
949
+ getTrimedTimes() {
950
+ return { start: this.startTime, end: this.endTime };
951
+ }
952
+ getCroppedArea() {
953
+ return this.cropArea.getBoundingClientRect();
954
+ }
955
+ showCropArea() {
956
+ this.crop = true;
957
+ this.cropArea.classList.remove('hidden');
958
+ this.controlsComponents.resetCropBtn?.root.classList.remove('hidden');
959
+ const nodes = this.cropArea.parentElement?.childNodes ?? [];
960
+ const rect = this.cropArea.getBoundingClientRect();
961
+ for (let i = 0; i < nodes.length; i++) {
962
+ const node = nodes[i];
963
+ if (node == this.cropArea)
964
+ continue;
965
+ const rectEl = node.getBoundingClientRect();
966
+ node.style.webkitMask = `linear-gradient(#000 0 0) ${rect.left - rectEl.left}px ${rect.top - rectEl.top}px / ${rect.width}px ${rect.height}px, linear-gradient(rgba(0, 0, 0, 0.3) 0 0)`;
967
+ node.style.webkitMaskRepeat = 'no-repeat';
968
+ }
969
+ }
970
+ hideCropArea() {
971
+ this.crop = false;
972
+ this.cropArea.classList.add('hidden');
973
+ this.controlsComponents.resetCropBtn?.root.classList.add('hidden');
974
+ const nodes = this.cropArea.parentElement?.childNodes ?? [];
975
+ for (let i = 0; i < nodes.length; i++) {
976
+ const node = nodes[i];
977
+ if (node == this.cropArea)
978
+ continue;
979
+ node.style.webkitMask = '';
980
+ node.style.webkitMaskRepeat = 'no-repeat';
981
+ }
982
+ }
983
+ showControls() {
984
+ this.controls = true;
985
+ this.controlsArea.show();
986
+ }
987
+ hideControls() {
988
+ this.controls = false;
989
+ this.controlsArea.hide();
990
+ }
991
+ stopUpdates() {
992
+ if (this.requestId) {
993
+ cancelAnimationFrame(this.requestId);
994
+ this.requestId = null;
995
+ }
996
+ }
997
+ unbind() {
998
+ this.stopUpdates();
999
+ this.video.pause();
1000
+ this.playing = false;
1001
+ this.controlsComponents.playBtn?.setState(false, true); // skip callback
1002
+ this.video.src = '';
1003
+ if (this.timebar) {
1004
+ this.timebar.unbind();
1005
+ }
1006
+ window.removeEventListener('keyup', this.onKeyUp);
1007
+ document.removeEventListener('mouseup', this._onCropMouseUp);
1008
+ document.removeEventListener('mousemove', this._onCropMouseMove);
1009
+ }
1010
+ }
1011
+ LX.VideoEditor = VideoEditor;
1012
+
1013
+ export { TimeBar, VideoEditor };
1014
+ //# sourceMappingURL=VideoEditor.js.map