lexgui 8.1.1 → 8.2.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.
Files changed (52) hide show
  1. package/build/components/Avatar.d.ts +15 -0
  2. package/build/components/NodeTree.d.ts +51 -26
  3. package/build/components/Vector.d.ts +10 -9
  4. package/build/core/Event.d.ts +6 -26
  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 +7 -6
  9. package/build/extensions/AssetView.js +194 -155
  10. package/build/extensions/AssetView.js.map +1 -1
  11. package/build/extensions/Audio.js.map +1 -1
  12. package/build/extensions/CodeEditor.d.ts +358 -350
  13. package/build/extensions/CodeEditor.js +5054 -5022
  14. package/build/extensions/CodeEditor.js.map +1 -1
  15. package/build/extensions/DocMaker.js +330 -327
  16. package/build/extensions/DocMaker.js.map +1 -1
  17. package/build/extensions/GraphEditor.js +2754 -2760
  18. package/build/extensions/GraphEditor.js.map +1 -1
  19. package/build/extensions/Timeline.d.ts +668 -670
  20. package/build/extensions/Timeline.js +3948 -3955
  21. package/build/extensions/Timeline.js.map +1 -1
  22. package/build/extensions/VideoEditor.d.ts +128 -128
  23. package/build/extensions/VideoEditor.js +893 -898
  24. package/build/extensions/VideoEditor.js.map +1 -1
  25. package/build/index.css.d.ts +3 -4
  26. package/build/index.d.ts +57 -56
  27. package/build/lexgui.all.js +1587 -1369
  28. package/build/lexgui.all.js.map +1 -1
  29. package/build/lexgui.all.min.js +1 -1
  30. package/build/lexgui.all.module.js +1584 -1364
  31. package/build/lexgui.all.module.js.map +1 -1
  32. package/build/lexgui.all.module.min.js +1 -1
  33. package/build/lexgui.css +6157 -5583
  34. package/build/lexgui.js +977 -815
  35. package/build/lexgui.js.map +1 -1
  36. package/build/lexgui.min.css +2 -3
  37. package/build/lexgui.min.js +1 -1
  38. package/build/lexgui.module.js +975 -811
  39. package/build/lexgui.module.js.map +1 -1
  40. package/build/lexgui.module.min.js +1 -1
  41. package/changelog.md +52 -1
  42. package/demo.js +167 -65
  43. package/examples/all-components.html +38 -52
  44. package/examples/asset-view.html +27 -0
  45. package/examples/code-editor.html +1 -1
  46. package/examples/editor.html +10 -95
  47. package/examples/index.html +2 -2
  48. package/examples/side-bar.html +1 -1
  49. package/examples/timeline.html +2 -2
  50. package/examples/video-editor.html +1 -1
  51. package/examples/video-editor2.html +2 -2
  52. package/package.json +7 -4
@@ -1,898 +1,893 @@
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.getThemeColor('global-branch-darker');
19
- static COLOR = LX.getThemeColor('global-button-color');
20
- static ACTIVE_COLOR = '#668ee4';
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.getThemeColor, 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.getThemeColor('global-color-secondary');
72
- TimeBar.COLOR = LX.getThemeColor('global-color-quaternary');
73
- TimeBar.ACTIVE_COLOR = '#668ee4';
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',
136
- fillColor: TimeBar.ACTIVE_COLOR || '#5f88c9', width: this.markerWidth });
137
- if (this.onDraw) {
138
- this.onDraw();
139
- }
140
- }
141
- _drawTrimMarker(name, x, options = {}) {
142
- const w = this.markerWidth;
143
- const h = this.markerHeight;
144
- const y = this.canvas.height * 0.5 - h * 0.5;
145
- const ctx = this.ctx;
146
- if (!ctx)
147
- return;
148
- // Shadow
149
- if (this.hovering == name) {
150
- ctx.shadowColor = 'white';
151
- ctx.shadowBlur = 2;
152
- }
153
- ctx.globalAlpha = 1;
154
- ctx.fillStyle = ctx.strokeStyle = options.fillColor || '#111'; // "#FFF";
155
- ctx.beginPath();
156
- ctx.roundRect(x - w * 0.5, y, w, h, 2);
157
- ctx.fill();
158
- ctx.fillStyle = ctx.strokeStyle = options.fillColor || '#111'; // "#FFF";
159
- ctx.strokeStyle = 'white';
160
- ctx.beginPath();
161
- ctx.lineWidth = 2;
162
- ctx.moveTo(x, y + 4);
163
- ctx.lineTo(x, y + h - 4);
164
- ctx.stroke();
165
- ctx.shadowBlur = 0;
166
- }
167
- _drawTimeMarker(name, x, options = {}) {
168
- let y = this.offset;
169
- const w = options.width ? options.width : (this.dragging == name ? 6 : 4);
170
- const h = this.canvas.height - this.offset * 2;
171
- let ctx = this.ctx;
172
- if (!ctx)
173
- return;
174
- ctx.globalAlpha = 1;
175
- ctx.fillStyle = ctx.strokeStyle = options.fillColor || '#111'; // "#FFF";
176
- // Shadow
177
- if (this.hovering == name) {
178
- ctx.shadowColor = 'white';
179
- ctx.shadowBlur = 2;
180
- }
181
- // Current time line
182
- ctx.fillStyle = ctx.strokeStyle = 'white';
183
- ctx.beginPath();
184
- ctx.moveTo(x, y);
185
- ctx.lineTo(x, y + h * 0.5);
186
- ctx.stroke();
187
- ctx.closePath();
188
- ctx.fillStyle = ctx.strokeStyle = options.fillColor || '#111'; // "#FFF";
189
- y -= this.offset + 8;
190
- // Current time ball grab
191
- ctx.fillStyle = options.fillColor || '#e5e5e5';
192
- ctx.beginPath();
193
- ctx.roundRect(x - w * 0.5, y + this.offset, w, w, 5);
194
- ctx.fill();
195
- ctx.shadowBlur = 0;
196
- }
197
- update(x) {
198
- this.currentX = Math.min(Math.max(this.startX, x), this.endX);
199
- this._draw();
200
- }
201
- onMouseDown(e) {
202
- if (this.onMouse) {
203
- this.onMouse(e);
204
- }
205
- e.preventDefault();
206
- if (!this.canvas || e.target != this.canvas || e.cancelBubble) {
207
- return;
208
- }
209
- const canvas = this.canvas;
210
- // Process mouse
211
- const x = e.offsetX;
212
- const y = e.offsetY;
213
- // Check if some marker is clicked
214
- const threshold = this.markerWidth;
215
- // grab trim markers only from the bottom
216
- if (Math.abs(this.startX - x) < threshold && this.position.y < y) {
217
- this.dragging = 'start';
218
- canvas.style.cursor = 'grabbing';
219
- }
220
- else if (Math.abs(this.endX - x) < threshold && this.position.y < y) {
221
- this.dragging = 'end';
222
- canvas.style.cursor = 'grabbing';
223
- }
224
- else {
225
- this.dragging = 'current';
226
- canvas.style.cursor = 'grabbing';
227
- if (x < this.startX) {
228
- this.currentX = this.startX;
229
- }
230
- else if (x > this.endX) {
231
- this.currentX = this.endX;
232
- }
233
- else {
234
- this.currentX = x;
235
- }
236
- this.onSetCurrentValue(this.currentX);
237
- }
238
- this._draw();
239
- }
240
- onMouseUp(e) {
241
- if (this.onMouse) {
242
- this.onMouse(e);
243
- }
244
- e.preventDefault();
245
- delete this.dragging;
246
- delete this.hovering;
247
- if (!this.canvas || e.cancelBubble) {
248
- return;
249
- }
250
- const canvas = this.canvas;
251
- canvas.style.cursor = 'default';
252
- }
253
- onMouseMove(e) {
254
- if (this.onMouse) {
255
- this.onMouse(e);
256
- }
257
- if (!this.canvas || e.cancelBubble) {
258
- return;
259
- }
260
- e.preventDefault();
261
- const canvas = this.canvas;
262
- // Process mouse
263
- const x = e.target == canvas ? e.offsetX : e.clientX - canvas.offsetLeft;
264
- e.target == canvas ? e.offsetY : e.clientY - canvas.offsetTop;
265
- if (this.dragging) {
266
- switch (this.dragging) {
267
- case 'start':
268
- this.startX = Math.max(this.position.x, Math.min(this.endX, x));
269
- this.currentX = this.startX;
270
- this.onSetStartValue(this.startX);
271
- break;
272
- case 'end':
273
- this.endX = Math.max(this.startX, Math.min(this.position.x + this.lineWidth, x));
274
- this.currentX = this.endX;
275
- this.onSetEndValue(this.endX);
276
- break;
277
- default:
278
- this.currentX = Math.max(this.startX, Math.min(this.endX, x));
279
- break;
280
- }
281
- this.onSetCurrentValue(this.currentX);
282
- }
283
- else {
284
- const threshold = this.markerWidth * 0.5;
285
- if (Math.abs(this.startX - x) < threshold) {
286
- this.hovering = 'start';
287
- canvas.style.cursor = 'grab';
288
- }
289
- else if (Math.abs(this.endX - x) < threshold) {
290
- this.hovering = 'end';
291
- canvas.style.cursor = 'grab';
292
- }
293
- else if (Math.abs(this.currentX - x) < threshold) {
294
- this.hovering = 'current';
295
- canvas.style.cursor = 'grab';
296
- }
297
- else {
298
- delete this.hovering;
299
- canvas.style.cursor = 'default';
300
- }
301
- }
302
- this._draw();
303
- }
304
- resize(size) {
305
- this.canvas.width = Math.max(0, size[0]);
306
- this.canvas.height = Math.max(0, size[1]);
307
- let newWidth = size[0] - this.offset * 2;
308
- newWidth = newWidth < 0.00001 ? 0.00001 : newWidth; // actual width of the line = canvas.width - offsetleft - offsetRight
309
- const startRatio = (this.startX - this.offset) / this.lineWidth;
310
- const currentRatio = (this.currentX - this.offset) / this.lineWidth;
311
- const endRatio = (this.endX - this.offset) / this.lineWidth;
312
- this.lineWidth = newWidth;
313
- this.startX = Math.min(Math.max(newWidth * startRatio, 0), newWidth) + this.offset;
314
- this.currentX = Math.min(Math.max(newWidth * currentRatio, 0), newWidth) + this.offset;
315
- this.endX = Math.min(Math.max(newWidth * endRatio, 0), newWidth) + this.offset;
316
- this._draw();
317
- }
318
- }
319
- LX.TimeBar = TimeBar;
320
- /**
321
- * @class VideoEditor
322
- */
323
- class VideoEditor {
324
- static CROP_HANDLE_L = 0x01;
325
- static CROP_HANDLE_R = 0x02;
326
- static CROP_HANDLE_T = 0x04;
327
- static CROP_HANDLE_B = 0x08;
328
- static CROP_HANDLE_TL = VideoEditor.CROP_HANDLE_L | VideoEditor.CROP_HANDLE_T;
329
- static CROP_HANDLE_BL = VideoEditor.CROP_HANDLE_L | VideoEditor.CROP_HANDLE_B;
330
- static CROP_HANDLE_TR = VideoEditor.CROP_HANDLE_R | VideoEditor.CROP_HANDLE_T;
331
- static CROP_HANDLE_BR = VideoEditor.CROP_HANDLE_R | VideoEditor.CROP_HANDLE_B;
332
- options = {};
333
- playing = false;
334
- videoReady = false;
335
- controls = true;
336
- startTimeString = '0:0';
337
- endTimeString = '0:0';
338
- speed = 1.0;
339
- currentTime = 0.0;
340
- startTime = 0.0;
341
- endTime = 0.0;
342
- requestId;
343
- video;
344
- loop = false;
345
- isDragging = false;
346
- isResizing = null; // holds the HTMLElement of the crop handle, if resizing
347
- crop = false;
348
- dragOffsetX = 0.0;
349
- dragOffsetY = 0.0;
350
- currentTimeString = '';
351
- timebar;
352
- mainArea;
353
- cropArea; // HTMLElement with normCoord attribute;
354
- controlsArea;
355
- controlsPanelLeft;
356
- controlsPanelRight;
357
- controlsCurrentPanel;
358
- onChangeCurrent;
359
- onChangeStart;
360
- onChangeEnd;
361
- onKeyUp;
362
- onSetTime;
363
- onVideoLoaded;
364
- onCropArea;
365
- onResize;
366
- onChangeSpeed;
367
- _updateTime = true;
368
- _onCropMouseUp;
369
- _onCropMouseMove;
370
- resize;
371
- constructor(area, options = {}) {
372
- this.options = options ?? {};
373
- this.speed = options.speed ?? this.speed;
374
- this.mainArea = area;
375
- let videoArea = null;
376
- let controlsArea = null;
377
- if (options.controlsArea) {
378
- videoArea = area;
379
- controlsArea = options.controlsArea;
380
- }
381
- else {
382
- [videoArea, controlsArea] = area.split({ type: 'vertical', sizes: ['85%', null], minimizable: false,
383
- resize: false });
384
- }
385
- controlsArea.root.classList.add('lexconstrolsarea');
386
- this.cropArea = document.createElement('div');
387
- this.cropArea.id = 'cropArea';
388
- this.cropArea.className = 'resize-area hidden';
389
- this.cropArea.normCoords = { x: 0, y: 0, w: 1, h: 1 };
390
- const flags = 0x0f;
391
- this.setCropAreaHandles(flags);
392
- this.crop = options.crop;
393
- this.dragOffsetX = 0;
394
- this.dragOffsetY = 0;
395
- // Create video element and load it
396
- let video = this.video = options.video ?? document.createElement('video');
397
- this.loop = options.loop ?? this.loop;
398
- if (options.src) {
399
- this.video.src = options.src;
400
- this.loadVideo(options);
401
- }
402
- if (options.videoArea) {
403
- options.videoArea.root.classList.add('lexvideoeditor');
404
- options.videoArea.attach(this.cropArea);
405
- videoArea.attach(options.videoArea);
406
- }
407
- else {
408
- videoArea.attach(video);
409
- videoArea.attach(this.cropArea);
410
- videoArea.root.classList.add('lexvideoeditor');
411
- }
412
- this.controlsArea = controlsArea;
413
- // Create playing timeline area and attach panels
414
- let [topArea, bottomArea] = controlsArea.split({ type: 'vertical', sizes: ['50%', null],
415
- minimizable: false, resize: false });
416
- bottomArea.setSize([bottomArea.size[0], 40]);
417
- let [leftArea, controlsRight] = bottomArea.split({ type: 'horizontal', sizes: ['92%', null],
418
- minimizable: false, resize: false });
419
- let [controlsLeft, timeBarArea] = leftArea.split({ type: 'horizontal', sizes: ['10%', null],
420
- minimizable: false, resize: false });
421
- topArea.root.classList.add('lexbar');
422
- bottomArea.root.classList.add('lexbar');
423
- this.controlsCurrentPanel = new LX.Panel({ className: 'lexcontrolspanel lextime' });
424
- this.controlsCurrentPanel.refresh = () => {
425
- this.controlsCurrentPanel.clear();
426
- this.controlsCurrentPanel.addLabel(this.currentTimeString, { float: 'center' });
427
- };
428
- topArea.root.classList.add('lexflexarea');
429
- topArea.attach(this.controlsCurrentPanel);
430
- this.controlsCurrentPanel.refresh();
431
- const style = getComputedStyle(bottomArea.root);
432
- let padding = Number(style.getPropertyValue('padding').replace('px', ''));
433
- this.timebar = new TimeBar(timeBarArea, TimeBar.TIMEBAR_TRIM, { offset: padding });
434
- // Create controls panel (play/pause button and start time)
435
- this.controlsPanelLeft = new LX.Panel({ className: 'lexcontrolspanel' });
436
- this.controlsPanelLeft.refresh = () => {
437
- this.controlsPanelLeft.clear();
438
- this.controlsPanelLeft.sameLine();
439
- let playbtn = this.controlsPanelLeft.addButton('Play', '', (v) => {
440
- this.playing = v;
441
- if (this.playing) {
442
- if (this.video.currentTime + 0.000001 >= this.endTime) {
443
- this.video.currentTime = this.startTime;
444
- }
445
- this.video.play();
446
- }
447
- else {
448
- this.video.pause();
449
- }
450
- }, { width: '40px', icon: 'Play@solid', swap: 'Pause@solid', hideName: true,
451
- className: 'justify-center' });
452
- playbtn.setState(this.playing, true);
453
- this.controlsPanelLeft.addButton('', '', (v, e) => {
454
- const panel = new LX.Panel();
455
- panel.addRange('Speed', this.speed, (v) => {
456
- this.speed = v;
457
- this.video.playbackRate = v;
458
- if (this.onChangeSpeed) {
459
- this.onChangeSpeed(v);
460
- }
461
- }, { min: 0, max: 2.5, step: 0.01, hideName: true });
462
- new LX.Popover(e.target, [panel], { align: 'start', side: 'top', sideOffset: 12 });
463
- }, { width: '40px', title: 'speed', icon: 'Timer@solid', className: 'justify-center' });
464
- this.controlsPanelLeft.addButton('', 'Loop', (v) => {
465
- this.loop = v;
466
- }, { width: '40px', title: 'loop', icon: ('Repeat@solid'), className: `justify-center`, selectable: true,
467
- selected: this.loop });
468
- this.controlsPanelLeft.addLabel(this.startTimeString, { width: '100px' });
469
- this.controlsPanelLeft.endLine();
470
- let availableWidth = leftArea.root.clientWidth - controlsLeft.root.clientWidth;
471
- this.timebar.resize([availableWidth, timeBarArea.root.clientHeight]);
472
- };
473
- this.controlsPanelLeft.refresh();
474
- controlsLeft.root.style.minWidth = 'fit-content';
475
- // controlsLeft.root.classList.add();
476
- controlsLeft.attach(this.controlsPanelLeft);
477
- // Create right controls panel (ens time)
478
- this.controlsPanelRight = new LX.Panel({ className: 'lexcontrolspanel' });
479
- this.controlsPanelRight.refresh = () => {
480
- this.controlsPanelRight.clear();
481
- this.controlsPanelRight.addLabel(this.endTimeString, { width: 100 });
482
- };
483
- this.controlsPanelRight.refresh();
484
- controlsRight.root.style.minWidth = 'fit-content';
485
- controlsRight.attach(this.controlsPanelRight);
486
- this.timebar.onChangeCurrent = this._setCurrentTime.bind(this);
487
- this.timebar.onChangeStart = this._setStartTime.bind(this);
488
- this.timebar.onChangeEnd = this._setEndTime.bind(this);
489
- this.resize = () => {
490
- bottomArea.setSize([this.controlsArea.root.clientWidth, 40]);
491
- let availableWidth = this.controlsArea.root.clientWidth - controlsLeft.root.clientWidth
492
- - controlsRight.root.clientWidth;
493
- this.timebar.resize([availableWidth, timeBarArea.root.clientHeight]);
494
- this.moveCropArea(this.cropArea.normCoords.x, this.cropArea.normCoords.y, true);
495
- this.resizeCropArea(this.cropArea.normCoords.w, this.cropArea.normCoords.h, true);
496
- if (this.onResize) {
497
- this.onResize([videoArea.root.clientWidth, videoArea.root.clientHeight]);
498
- }
499
- };
500
- area.onresize = this.resize.bind(this);
501
- window.addEventListener('resize', area.onresize);
502
- this.onKeyUp = (e) => {
503
- if (this.controls && e.key == ' ') {
504
- e.preventDefault();
505
- e.stopPropagation();
506
- this.playing = !this.playing;
507
- if (this.playing) {
508
- if (this.video.currentTime + 0.000001 >= this.endTime) {
509
- this.video.currentTime = this.startTime;
510
- }
511
- this.video.play();
512
- }
513
- else {
514
- this.video.pause();
515
- }
516
- this.controlsPanelLeft.refresh();
517
- }
518
- };
519
- window.addEventListener('keyup', this.onKeyUp);
520
- const parent = controlsArea.parentElement ? controlsArea.parentElement : controlsArea.root.parentElement;
521
- // Add canvas event listeneres
522
- parent.addEventListener('mousedown', (e) => {
523
- // if( this.controls) {
524
- // this.timebar.onMouseDown(e);
525
- // }
526
- });
527
- this._onCropMouseUp = (event) => {
528
- // if(this.controls) {
529
- // this.timebar.onMouseUp(event);
530
- // }
531
- event.preventDefault();
532
- event.stopPropagation();
533
- if ((this.isDragging || this.isResizing) && this.onCropArea) {
534
- this.onCropArea(this.getCroppedArea());
535
- }
536
- this.isDragging = false;
537
- this.isResizing = false;
538
- document.removeEventListener('mouseup', this._onCropMouseUp); // self destroy. Added during mouseDown on cropArea and handles
539
- document.removeEventListener('mousemove', this._onCropMouseMove); // self destroy. Added during mouseDown on cropArea and handles
540
- };
541
- this._onCropMouseMove = (event) => {
542
- // if(this.controls) {
543
- // this.timebar.onMouseMove(event);
544
- // }
545
- window.getSelection()?.removeAllRanges();
546
- event.preventDefault();
547
- event.stopPropagation();
548
- if (this.isResizing) {
549
- const rectCrop = this.cropArea.getBoundingClientRect();
550
- const rectVideo = this.video.getBoundingClientRect();
551
- const mov = this.isResizing.movement;
552
- let x = rectCrop.left, y = rectCrop.top, w = rectCrop.width, h = rectCrop.height;
553
- if (mov & VideoEditor.CROP_HANDLE_L) {
554
- let mouseX = Math.min(rectCrop.right - 4, Math.max(rectVideo.left, event.clientX)); // -4 because of border
555
- w = rectCrop.left + rectCrop.width - mouseX;
556
- x = mouseX;
557
- if (mouseX < rectCrop.left) {
558
- this.moveCropArea(x, y, false);
559
- this.resizeCropArea(w, h, false);
560
- }
561
- else {
562
- this.resizeCropArea(w, h, false);
563
- this.moveCropArea(x, y, false);
564
- }
565
- }
566
- if (mov & VideoEditor.CROP_HANDLE_R) {
567
- w = event.clientX - rectCrop.left;
568
- this.resizeCropArea(w, h, false);
569
- }
570
- if (mov & VideoEditor.CROP_HANDLE_T) {
571
- const mouseY = Math.min(rectCrop.bottom - 4, Math.max(rectVideo.top, event.clientY));
572
- h = rectCrop.top + rectCrop.height - mouseY;
573
- y = mouseY;
574
- if (mouseY < rectCrop.top) {
575
- this.moveCropArea(x, y, false);
576
- this.resizeCropArea(w, h, false);
577
- }
578
- else {
579
- this.resizeCropArea(w, h, false);
580
- this.moveCropArea(x, y, false);
581
- }
582
- }
583
- if (mov & VideoEditor.CROP_HANDLE_B) {
584
- h = event.clientY - rectCrop.top;
585
- this.resizeCropArea(w, h, false);
586
- }
587
- }
588
- if (this.isDragging) {
589
- this.moveCropArea(event.clientX - this.dragOffsetX, event.clientY - this.dragOffsetY, false);
590
- }
591
- };
592
- this.cropArea.addEventListener('mousedown', (e) => {
593
- if (e.target === this.cropArea) {
594
- const rect = this.cropArea.getBoundingClientRect();
595
- this.isDragging = true;
596
- this.dragOffsetX = e.clientX - rect.left;
597
- this.dragOffsetY = e.clientY - rect.top;
598
- document.addEventListener('mouseup', this._onCropMouseUp);
599
- document.addEventListener('mousemove', this._onCropMouseMove);
600
- }
601
- });
602
- this.onChangeStart = null;
603
- this.onChangeEnd = null;
604
- }
605
- setCropAreaHandles(flags) {
606
- // remove existing resizer handles
607
- const resizers = this.cropArea.getElementsByClassName('resize-handle');
608
- for (let i = resizers.length - 1; i > -1; --i) {
609
- resizers[i].remove();
610
- }
611
- const buildResizer = (className, movement) => {
612
- const handle = document.createElement('div');
613
- handle.className = ' resize-handle ' + className;
614
- handle.movement = movement;
615
- if (this.options.handleStyle) {
616
- Object.assign(handle.style, this.options.handleStyle);
617
- }
618
- this.cropArea.append(handle);
619
- handle.addEventListener('mousedown', (e) => {
620
- e.stopPropagation();
621
- e.preventDefault();
622
- this.isResizing = handle;
623
- document.addEventListener('mouseup', this._onCropMouseUp);
624
- document.addEventListener('mousemove', this._onCropMouseMove);
625
- });
626
- };
627
- if (flags & VideoEditor.CROP_HANDLE_L)
628
- buildResizer('l', VideoEditor.CROP_HANDLE_L);
629
- if (flags & VideoEditor.CROP_HANDLE_R)
630
- buildResizer('r', VideoEditor.CROP_HANDLE_R);
631
- if (flags & VideoEditor.CROP_HANDLE_T)
632
- buildResizer('t', VideoEditor.CROP_HANDLE_T);
633
- if (flags & VideoEditor.CROP_HANDLE_B)
634
- buildResizer('b', VideoEditor.CROP_HANDLE_B);
635
- if ((flags & VideoEditor.CROP_HANDLE_TL) == VideoEditor.CROP_HANDLE_TL) {
636
- buildResizer('tl', VideoEditor.CROP_HANDLE_TL);
637
- }
638
- if ((flags & VideoEditor.CROP_HANDLE_BL) == VideoEditor.CROP_HANDLE_BL) {
639
- buildResizer('bl', VideoEditor.CROP_HANDLE_BL);
640
- }
641
- if ((flags & VideoEditor.CROP_HANDLE_TR) == VideoEditor.CROP_HANDLE_TR) {
642
- buildResizer('tr', VideoEditor.CROP_HANDLE_TR);
643
- }
644
- if ((flags & VideoEditor.CROP_HANDLE_BR) == VideoEditor.CROP_HANDLE_BR) {
645
- buildResizer('br', VideoEditor.CROP_HANDLE_BR);
646
- }
647
- }
648
- resizeCropArea(sx, sy, isNormalized = true) {
649
- const rectVideo = this.video.getBoundingClientRect();
650
- if (!isNormalized) {
651
- sx = (rectVideo.width) ? (sx / rectVideo.width) : 1;
652
- sy = (rectVideo.height) ? (sy / rectVideo.height) : 1;
653
- }
654
- sx = Math.min(1 - this.cropArea.normCoords.x, Math.max(0, sx));
655
- sy = Math.min(1 - this.cropArea.normCoords.y, Math.max(0, sy));
656
- this.cropArea.normCoords.w = sx;
657
- this.cropArea.normCoords.h = sy;
658
- const widthPx = rectVideo.width * sx;
659
- const heightPx = rectVideo.height * sy;
660
- const xPx = rectVideo.width * this.cropArea.normCoords.x + rectVideo.left;
661
- const yPx = rectVideo.height * this.cropArea.normCoords.y + rectVideo.top;
662
- if (!this.cropArea.classList.contains('hidden')) {
663
- const nodes = this.cropArea.parentElement.childNodes;
664
- for (let i = 0; i < nodes.length; i++) {
665
- if (nodes[i] != this.cropArea) {
666
- const rectEl = nodes[i].getBoundingClientRect();
667
- 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)`;
668
- nodes[i].style.webkitMaskRepeat = 'no-repeat';
669
- }
670
- }
671
- }
672
- this.cropArea.style.width = widthPx + 'px';
673
- this.cropArea.style.height = heightPx + 'px';
674
- }
675
- // screen pixel (event.clientX) or video normalized (0 is top left of video, 1 bot right)
676
- moveCropArea(x, y, isNormalized = true) {
677
- const rectVideo = this.video.getBoundingClientRect();
678
- if (!isNormalized) {
679
- x = (rectVideo.width) ? ((x - rectVideo.left) / rectVideo.width) : 0;
680
- y = (rectVideo.height) ? ((y - rectVideo.top) / rectVideo.height) : 0;
681
- }
682
- x = Math.max(0, Math.min(1 - this.cropArea.normCoords.w, x));
683
- y = Math.max(0, Math.min(1 - this.cropArea.normCoords.h, y));
684
- this.cropArea.normCoords.x = x;
685
- this.cropArea.normCoords.y = y;
686
- const xPx = rectVideo.width * x + rectVideo.left;
687
- const yPx = rectVideo.height * y + rectVideo.top;
688
- const widthPx = rectVideo.width * this.cropArea.normCoords.w;
689
- const heightPx = rectVideo.height * this.cropArea.normCoords.h;
690
- if (!this.cropArea.classList.contains('hidden')) {
691
- const nodes = this.cropArea.parentElement.childNodes;
692
- for (let i = 0; i < nodes.length; i++) {
693
- if (nodes[i] != this.cropArea) {
694
- const rectEl = nodes[i].getBoundingClientRect();
695
- 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)`;
696
- nodes[i].style.webkitMaskRepeat = 'no-repeat';
697
- }
698
- }
699
- }
700
- const rectParent = this.cropArea.parentElement.getBoundingClientRect();
701
- this.cropArea.style.left = xPx - rectParent.left + 'px';
702
- this.cropArea.style.top = yPx - rectParent.top + 'px';
703
- }
704
- async loadVideo(options = {}) {
705
- this.videoReady = false;
706
- while (this.video.duration === Infinity || isNaN(this.video.duration) || !this.timebar) {
707
- await new Promise((r) => setTimeout(r, 1000));
708
- this.video.currentTime = 10000000 * Math.random();
709
- }
710
- this.video.currentTime = 0.01; // BUG: some videos will not play unless this line is present
711
- // Duration can change if the video is dynamic (stream). This function is to ensure to load all buffer data
712
- const forceLoadChunks = () => {
713
- const state = this.videoReady;
714
- if (this.video.readyState > 3) {
715
- this.videoReady = true;
716
- }
717
- if (!state) {
718
- this.video.currentTime = this.video.duration;
719
- }
720
- };
721
- this.video.addEventListener('canplaythrough', forceLoadChunks, { passive: true });
722
- this.video.ondurationchange = (v) => {
723
- if (this.video.duration != this.endTime) {
724
- this.video.currentTime = this.startTime;
725
- console.log('duration changed from', this.endTime, ' to ', this.video.duration);
726
- this.endTime = this.video.duration;
727
- this.timebar.setDuration(this.endTime);
728
- this.timebar.setEndTime(this.endTime);
729
- }
730
- this.video.currentTime = this.startTime;
731
- this.timebar.setCurrentTime(this.video.currentTime);
732
- };
733
- this.timebar.startX = this.timebar.position.x;
734
- this.timebar.endX = this.timebar.position.x + this.timebar.lineWidth;
735
- this.startTime = 0;
736
- this.endTime = this.video.duration;
737
- this.timebar.setDuration(this.endTime);
738
- this.timebar.setEndTime(this.video.duration);
739
- this.timebar.setStartTime(this.startTime);
740
- this.timebar.setCurrentTime(this.startTime);
741
- // this.timebar.setStartValue( this.timebar.startX);
742
- // this.timebar.currentX = this._timeToX( this.video.currentTime);
743
- // this.timebar.setCurrentValue( this.timebar.currentX);
744
- // this.timebar.update( this.timebar.currentX );
745
- // only have one update on flight
746
- if (!this.requestId) {
747
- this._update();
748
- }
749
- this.controls = options.controls ?? true;
750
- if (!this.controls) {
751
- this.hideControls();
752
- }
753
- this.cropArea.style.height = this.video.clientHeight + 'px';
754
- this.cropArea.style.width = this.video.clientWidth + 'px';
755
- this.moveCropArea(0, 0, true);
756
- this.resizeCropArea(1, 1, true);
757
- if (this.crop) {
758
- this.showCropArea();
759
- }
760
- else {
761
- this.hideCropArea();
762
- }
763
- window.addEventListener('keyup', this.onKeyUp);
764
- if (this.onVideoLoaded) {
765
- this.onVideoLoaded(this.video);
766
- }
767
- }
768
- _update() {
769
- // if( this.onDraw ) {
770
- // this.onDraw();
771
- // }
772
- if (this.playing) {
773
- if (this.video.currentTime + 0.000001 >= this.endTime) {
774
- this.video.pause();
775
- if (!this.loop) {
776
- this.playing = false;
777
- this.controlsPanelLeft.refresh();
778
- }
779
- else {
780
- this.video.currentTime = this.startTime;
781
- this.video.play();
782
- }
783
- }
784
- this._updateTime = false;
785
- this.timebar.setCurrentTime(this.video.currentTime);
786
- this._updateTime = true;
787
- }
788
- this.requestId = requestAnimationFrame(this._update.bind(this));
789
- }
790
- timeToString(t) {
791
- let mzminutes = Math.floor(t / 60);
792
- let mzseconds = Math.floor(t - (mzminutes * 60));
793
- let mzmiliseconds = Math.floor((t - mzseconds) * 100);
794
- let mzmilisecondsStr = mzmiliseconds < 10 ? ('0' + mzmiliseconds) : mzmiliseconds.toString();
795
- let mzsecondsStr = mzseconds < 10 ? ('0' + mzseconds) : mzseconds.toString();
796
- let mzminutesStr = mzminutes < 10 ? ('0' + mzminutes) : mzminutes.toString();
797
- return `${mzminutesStr}:${mzsecondsStr}.${mzmilisecondsStr}`;
798
- }
799
- _setCurrentTime(t) {
800
- if (this.video.currentTime != t && this._updateTime) {
801
- this.video.currentTime = t;
802
- }
803
- this.currentTimeString = this.timeToString(t);
804
- this.controlsCurrentPanel.refresh();
805
- if (this.onSetTime) {
806
- this.onSetTime(t);
807
- }
808
- if (this.onChangeCurrent) {
809
- this.onChangeCurrent(t);
810
- }
811
- }
812
- _setStartTime(t) {
813
- this.startTime = this.video.currentTime = t;
814
- this.startTimeString = this.timeToString(t);
815
- this.controlsPanelLeft.refresh();
816
- if (this.onSetTime) {
817
- this.onSetTime(t);
818
- }
819
- if (this.onChangeStart) {
820
- this.onChangeStart(t);
821
- }
822
- }
823
- _setEndTime(t) {
824
- this.endTime = this.video.currentTime = t;
825
- this.endTimeString = this.timeToString(t);
826
- this.controlsPanelRight.refresh();
827
- if (this.onSetTime) {
828
- this.onSetTime(t);
829
- }
830
- if (this.onChangeEnd) {
831
- this.onChangeEnd(t);
832
- }
833
- }
834
- getStartTime() {
835
- return this.startTime;
836
- }
837
- getEndTime() {
838
- return this.endTime;
839
- }
840
- getTrimedTimes() {
841
- return { start: this.startTime, end: this.endTime };
842
- }
843
- getCroppedArea() {
844
- return this.cropArea.getBoundingClientRect();
845
- }
846
- showCropArea() {
847
- this.cropArea.classList.remove('hidden');
848
- const nodes = this.cropArea.parentElement?.childNodes ?? [];
849
- const rect = this.cropArea.getBoundingClientRect();
850
- for (let i = 0; i < nodes.length; i++) {
851
- const node = nodes[i];
852
- if (node == this.cropArea)
853
- continue;
854
- const rectEl = node.getBoundingClientRect();
855
- 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)`;
856
- node.style.webkitMaskRepeat = 'no-repeat';
857
- }
858
- }
859
- hideCropArea() {
860
- this.cropArea.classList.add('hidden');
861
- const nodes = this.cropArea.parentElement?.childNodes ?? [];
862
- for (let i = 0; i < nodes.length; i++) {
863
- const node = nodes[i];
864
- if (node == this.cropArea)
865
- continue;
866
- node.style.webkitMask = '';
867
- node.style.webkitMaskRepeat = 'no-repeat';
868
- }
869
- }
870
- showControls() {
871
- this.controls = true;
872
- this.controlsArea.show();
873
- }
874
- hideControls() {
875
- this.controls = false;
876
- this.controlsArea.hide();
877
- }
878
- stopUpdates() {
879
- if (this.requestId) {
880
- cancelAnimationFrame(this.requestId);
881
- this.requestId = null;
882
- }
883
- }
884
- unbind() {
885
- this.stopUpdates();
886
- this.video.pause();
887
- this.playing = false;
888
- this.controlsPanelLeft.refresh();
889
- this.video.src = '';
890
- window.removeEventListener('keyup', this.onKeyUp);
891
- document.removeEventListener('mouseup', this._onCropMouseUp);
892
- document.removeEventListener('mousemove', this._onCropMouseMove);
893
- }
894
- }
895
- LX.VideoEditor = VideoEditor;
896
-
897
- export { TimeBar, VideoEditor };
898
- //# 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
+ 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