lexgui 0.1.31 → 0.1.33

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,630 @@
1
+ import { LX } from 'lexgui';
2
+
3
+ if(!LX) {
4
+ throw("lexgui.js missing!");
5
+ }
6
+
7
+ LX.components.push( 'VideoEditor' );
8
+
9
+ /**
10
+ * @class TimeBar
11
+ */
12
+
13
+ class TimeBar {
14
+
15
+ static TIMEBAR_PLAY = 1;
16
+ static TIMEBAR_TRIM = 2;
17
+
18
+ static BACKGROUND_COLOR = LX.getThemeColor("global-branch-darker");
19
+ static COLOR = LX.getThemeColor("global-button-color");
20
+ static ACTIVE_COLOR = LX.getThemeColor("global-selected-light");
21
+
22
+ constructor( area, type, options = {} ) {
23
+
24
+ this.type = type;
25
+
26
+ // Create canvas
27
+ this.canvas = document.createElement( 'canvas' );
28
+ this.canvas.width = area.size[0];
29
+ this.canvas.height = area.size[1];
30
+ area.attach( this.canvas );
31
+
32
+ this.ctx = this.canvas.getContext("2d");
33
+
34
+ const barHeight = options.barHeight ?? 5;
35
+ this.markerWidth = options.markerWidth ?? 8;
36
+ this.offset = options.offset || 4;
37
+
38
+ this.width = this.canvas.width - this.offset * 2;
39
+ this.height = barHeight;
40
+
41
+ this.position = new LX.vec2( this.offset, this.canvas.height * 0.5 - this.height * 0.5);
42
+ this.startX = this.position.x;
43
+ this.endX = this.width;
44
+ this.currentX = this.startX;
45
+
46
+ const y = this.offset * 2;
47
+ const w = this.markerWidth;
48
+ const h = this.canvas.height - y * 2;
49
+ this.trimRec = [this.startX, y, w, h];
50
+
51
+ this.lastPosition = new LX.vec2( 0, 0 );
52
+
53
+ this._draw();
54
+ }
55
+
56
+ _draw() {
57
+ const ctx = this.ctx;
58
+
59
+ ctx.save();
60
+ ctx.fillStyle = TimeBar.BACKGROUND_COLOR;
61
+ ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
62
+ ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
63
+
64
+ // // Draw background timeline
65
+ ctx.fillStyle = TimeBar.COLOR;
66
+ ctx.fillRect(this.position.x, this.position.y, this.width, this.height);
67
+
68
+ // Draw background trimed timeline
69
+ ctx.fillStyle = TimeBar.ACTIVE_COLOR;
70
+ ctx.fillRect(this.startX, this.position.y, this.endX - this.offset - this.startX, this.height);
71
+
72
+ ctx.restore();
73
+
74
+ // Min-Max time markers
75
+ this._addTrim('start', this.startX, { color: null, fillColor: TimeBar.ACTIVE_COLOR || '#5f88c9'});
76
+ this._addTrim('end', this.endX, { color: null, fillColor: TimeBar.ACTIVE_COLOR || '#5f88c9'});
77
+ this._addMarker('current', this.currentX, { color: '#e5e5e5', fillColor: TimeBar.ACTIVE_COLOR || '#5f88c9', width: this.markerWidth });
78
+ }
79
+
80
+ _addTrim(name, x, options) {
81
+
82
+ options = options || {};
83
+
84
+ const y = this.trimRec[1];
85
+ const w = this.trimRec[2];
86
+ const h = this.trimRec[3];
87
+
88
+ const ctx = this.ctx;
89
+ if(this.hovering == name) {
90
+ // Shadow
91
+ ctx.shadowColor = "white";
92
+ ctx.shadowBlur = 2;
93
+ }
94
+ ctx.globalAlpha = 1;
95
+ ctx.fillStyle = ctx.strokeStyle = options.fillColor || '#111' // "#FFF";
96
+
97
+ ctx.beginPath();
98
+ ctx.roundRect(x - w * 0.5, y, w, h, 2);
99
+ ctx.fill();
100
+ ctx.fillStyle = ctx.strokeStyle = options.fillColor || '#111' // "#FFF";
101
+
102
+ ctx.strokeStyle = "white";
103
+ ctx.beginPath();
104
+ ctx.lineWitdh = 2;
105
+ ctx.moveTo(x, y + 4);
106
+ ctx.lineTo(x, y + h - 4);
107
+ ctx.stroke();
108
+ ctx.shadowBlur = 0;
109
+
110
+ }
111
+
112
+ _addMarker(name, x, options) {
113
+
114
+ options = options || {};
115
+
116
+ let y = this.offset;
117
+ const w = options.width ? options.width : (this.dragging == name ? 6 : 4);
118
+ const h = this.canvas.height - this.offset * 2;
119
+
120
+ let ctx = this.ctx;
121
+
122
+ ctx.globalAlpha = 1;
123
+
124
+ ctx.fillStyle = ctx.strokeStyle = options.fillColor || '#111' // "#FFF";
125
+
126
+
127
+ if(this.hovering == name) {
128
+ // Shadow
129
+ ctx.shadowColor = "white";
130
+ ctx.shadowBlur = 2;
131
+ }
132
+
133
+ // Current time line
134
+ ctx.fillStyle = ctx.strokeStyle = "white";
135
+ ctx.beginPath();
136
+ ctx.moveTo(x, y);
137
+ ctx.lineTo(x, y + h * 0.5);
138
+ ctx.stroke();
139
+ ctx.closePath();
140
+ ctx.fillStyle = ctx.strokeStyle = options.fillColor || '#111' // "#FFF";
141
+
142
+
143
+ y -= this.offset + 4;
144
+ // Current time ball grab
145
+ ctx.fillStyle = options.fillColor || '#e5e5e5';
146
+ ctx.beginPath();
147
+ ctx.roundRect(x - w * 0.5, y + this.offset, w, w, 5);
148
+
149
+ ctx.fill();
150
+ ctx.shadowBlur = 0;
151
+ }
152
+
153
+ onMouseDown (e) {
154
+
155
+ e.preventDefault();
156
+
157
+ if(!this.canvas || e.target != this.canvas) {
158
+ return;
159
+ }
160
+ const canvas = this.canvas;
161
+
162
+ // Process mouse
163
+ const x = e.offsetX;
164
+ const y = e.offsetY;
165
+
166
+ // Check if some marker is clicked
167
+ const threshold = this.markerWidth;
168
+
169
+ if(Math.abs(this.startX - x) < threshold && this.trimRec[1] < y && y < this.trimRec[3] + this.trimRec[1] ) {
170
+ this.dragging = 'start';
171
+ canvas.style.cursor = "grabbing";
172
+ }
173
+ else if(Math.abs(this.endX - x) < threshold && this.trimRec[1] < y && y < this.trimRec[3] + this.trimRec[1] ) {
174
+ this.dragging = 'end';
175
+ canvas.style.cursor = "grabbing";
176
+ }
177
+ else if(Math.abs(this.currentX - x) < threshold) {
178
+ this.dragging = 'current';
179
+ canvas.style.cursor = "grabbing";
180
+ }
181
+ else {
182
+ if(x < this.startX) {
183
+ this.currentX = this.startX;
184
+ }
185
+ else if(x > this.endX) {
186
+ this.currentX = this.endX;
187
+ }
188
+ else {
189
+ this.currentX = x;
190
+ }
191
+ }
192
+
193
+ this._draw();
194
+ }
195
+
196
+ update (x) {
197
+ this.currentX = Math.min(Math.max(this.startX, x), this.endX);
198
+ this._draw();
199
+
200
+ if(this.onDraw) {
201
+ this.onDraw();
202
+ }
203
+ }
204
+
205
+ onMouseUp (e) {
206
+ e.preventDefault();
207
+
208
+ this.dragging = false;
209
+ this.hovering = false;
210
+
211
+ if(!this.canvas) {
212
+ return;
213
+ }
214
+
215
+ const canvas = this.canvas;
216
+ canvas.style.cursor = "default";
217
+
218
+ // Process mouse
219
+ const x = e.target == this.canvas ? e.offsetX : e.offsetX - this.canvas.offsetLeft ;
220
+ const y = e.target == this.canvas ? e.offsetY : e.offsetY - this.canvas.offsetTop ;
221
+ const threshold = 5;
222
+
223
+ if(this.trimRec[1] < y && y < this.trimRec[3] + this.trimRec[1]) {
224
+ if(x < this.startX) {
225
+ this.currentX = this.startX;
226
+ }
227
+ else if(x > this.endX) {
228
+ this.currentX = this.endX;
229
+ }
230
+ else {
231
+ this.currentX = x;
232
+ }
233
+ if(this.onChangeCurrent) {
234
+ this.onChangeCurrent(this.currentX);
235
+ }
236
+ }
237
+ }
238
+
239
+ onMouseMove (e) {
240
+
241
+ e.preventDefault();
242
+
243
+ // Process mouse
244
+ const x = e.target == this.canvas ? e.offsetX : e.offsetX - this.canvas.offsetLeft ;
245
+ const y = e.target == this.canvas ? e.offsetY : e.offsetY - this.canvas.offsetTop ;
246
+ const threshold = 5;
247
+
248
+ if(this.dragging) {
249
+ switch(this.dragging) {
250
+ case 'start':
251
+ if(x < this.position.x) {
252
+ this.currentX = this.startX = this.position.x;
253
+ }
254
+ else if(x > this.endX) {
255
+ this.currentX = this.startX = this.endX;
256
+ }
257
+ else {
258
+ this.currentX = this.startX = x;
259
+ }
260
+
261
+ if(this.onChangeStart) {
262
+ this.onChangeStart(this.startX);
263
+ }
264
+ if(this.onChangeCurrent) {
265
+ this.onChangeCurrent(this.currentX);
266
+ }
267
+ break;
268
+ case 'end':
269
+ if(x > this.width || x <= 0) {
270
+ this.currentX = this.endX = this.width;
271
+ }
272
+ else if(x < this.startX) {
273
+ this.currentX = this.endX = this.startX;
274
+ }
275
+ else {
276
+ this.currentX = this.endX = x;
277
+ }
278
+
279
+ if(this.onChangeEnd) {
280
+ this.onChangeEnd(this.endX);
281
+ }
282
+ if(this.onChangeCurrent) {
283
+ this.onChangeCurrent(this.currentX);
284
+ }
285
+ break;
286
+ case 'current':
287
+
288
+ if(x < this.startX) {
289
+ this.currentX = this.startX;
290
+ }
291
+ else if(x > this.endX) {
292
+ this.currentX = this.endX;
293
+ }
294
+ else {
295
+ this.currentX = x;
296
+ }
297
+ if(this.onChangeCurrent) {
298
+ this.onChangeCurrent(this.currentX);
299
+ }
300
+ break;
301
+ }
302
+ }
303
+ else {
304
+ if(!this.canvas) {
305
+ return;
306
+ }
307
+
308
+ const canvas = this.canvas;
309
+
310
+ if(Math.abs(this.startX - x) < threshold && this.trimRec[1] < y && y < this.trimRec[3] + this.trimRec[1] ) {
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
+ this.hovering = false;
324
+ canvas.style.cursor = "default";
325
+ }
326
+ }
327
+ this._draw();
328
+ }
329
+
330
+ resize (size) {
331
+ this.canvas.width = size[0];
332
+ this.canvas.height = size[1];
333
+
334
+ const startRatio = this.startX / this.width;
335
+ const currentRatio = this.currentX / this.width;
336
+ const endRatio = this.endX / this.width;
337
+ this.width = this.canvas.width - this.offset * 2;
338
+
339
+ this.startX = Math.max(this.width * startRatio, this.offset);
340
+ this.currentX = Math.min(Math.max(this.width * currentRatio, this.offset), this.width);
341
+ this.endX = Math.min(this.width * endRatio, this.width);
342
+
343
+ this._draw();
344
+ }
345
+ }
346
+
347
+ /**
348
+ * @class VideoEditor
349
+ */
350
+
351
+ class VideoEditor {
352
+
353
+ constructor( area, options = {} ) {
354
+
355
+ this.playing = false;
356
+ this.requestId = null;
357
+
358
+ this.currentTime = this.startTime = 0;
359
+ this.startTimeString = "0:0";
360
+ this.endTimeString = "0:0";
361
+
362
+ let [videoArea, controlsArea] = area.split({ type: 'vertical', sizes: ["80%", null], minimizable: false, resize: false });
363
+ controlsArea.root.classList.add('lexconstrolsarea');
364
+
365
+ // Create video element and load it
366
+ let video = this.video = options.video ?? document.createElement( 'video' );
367
+ if(options.src) {
368
+ this.video.src = options.src;
369
+ }
370
+ this.video.loop = true;
371
+ this._loadVideo(options);
372
+ if(options.videoArea) {
373
+ options.videoArea.root.classList.add("lexvideoeditor");
374
+ videoArea.attach(options.videoArea);
375
+ }
376
+ else {
377
+ videoArea.attach(video);
378
+ videoArea.root.classList.add("lexvideoeditor");
379
+ }
380
+
381
+ this.controlsArea = controlsArea;
382
+ // Create playing timeline area and attach panels
383
+ let [topArea, bottomArea] = controlsArea.split({ type: 'vertical', sizes:["50%", null], minimizable: false, resize: false });
384
+ bottomArea.setSize([bottomArea.size[0], 40]);
385
+ let [leftArea, controlsRight] = bottomArea.split({ type: 'horizontal', sizes:["92%", null], minimizable: false, resize: false });
386
+ let [controlsLeft, timeBarArea] = leftArea.split({ type: 'horizontal', sizes:["10%", null], minimizable: false, resize: false });
387
+
388
+ topArea.root.classList.add('lexbar');
389
+ bottomArea.root.classList.add('lexbar');
390
+ this.controlsCurrentPanel = new LX.Panel({className: 'lexcontrolspanel lextime'});
391
+ this.controlsCurrentPanel.refresh = () => {
392
+ this.controlsCurrentPanel.clear();
393
+ this.controlsCurrentPanel.addLabel(this.currentTimeString, {float: "center"});
394
+ }
395
+ topArea.root.classList.add('lexflexarea')
396
+ topArea.attach(this.controlsCurrentPanel);
397
+ this.controlsCurrentPanel.refresh();
398
+
399
+ const style = getComputedStyle(bottomArea.root);
400
+ let padding = Number(style.getPropertyValue('padding').replace("px",""));
401
+ this.timebar = new TimeBar(timeBarArea, TimeBar.TIMEBAR_TRIM, {offset: padding});
402
+
403
+ // Create controls panel (play/pause button and start time)
404
+ this.controlsPanelLeft = new LX.Panel({className: 'lexcontrolspanel'});
405
+ this.controlsPanelLeft.refresh = () => {
406
+ this.controlsPanelLeft.clear();
407
+ this.controlsPanelLeft.sameLine();
408
+ this.controlsPanelLeft.addButton('', '<i class="fa-solid ' + (this.playing ? 'fa-pause' : 'fa-play') + '"></>', (v) => {
409
+ this.playing = !this.playing;
410
+ if(this.playing) {
411
+
412
+ this.video.play();
413
+ // if(!this.requestId) {
414
+ // this.requestId = requestAnimationFrame(this._update.bind(this))
415
+ // }
416
+ }
417
+ else {
418
+ // if(this.requestId) {
419
+ // cancelAnimationFrame(this.requestId);
420
+ // this.requestId = null;
421
+ // }
422
+ this.video.pause();
423
+ }
424
+ this.controlsPanelLeft.refresh();
425
+ }, { width: '40px'});
426
+
427
+ this.controlsPanelLeft.addLabel(this.startTimeString, {width: 50});
428
+ this.controlsPanelLeft.endLine();
429
+
430
+ let availableWidth = leftArea.root.clientWidth - controlsLeft.root.clientWidth;
431
+ this.timebar.resize([availableWidth, timeBarArea.root.clientHeight]);
432
+ }
433
+
434
+ this.controlsPanelLeft.refresh();
435
+ controlsLeft.root.style.minWidth = 'fit-content';
436
+ controlsLeft.attach(this.controlsPanelLeft);
437
+
438
+ // Create right controls panel (ens time)
439
+ this.controlsPanelRight = new LX.Panel({className: 'lexcontrolspanel'});
440
+ this.controlsPanelRight.refresh = () => {
441
+ this.controlsPanelRight.clear();
442
+ this.controlsPanelRight.addLabel(this.endTimeString, {width: 50});
443
+ }
444
+ this.controlsPanelRight.refresh();
445
+ controlsRight.root.style.minWidth = 'fit-content';
446
+ controlsRight.attach(this.controlsPanelRight);
447
+
448
+ this.timebar.onChangeCurrent = this._setCurrentValue.bind(this);
449
+ this.timebar.onChangeStart = this._setStartValue.bind(this);
450
+ this.timebar.onChangeEnd = this._setEndValue.bind(this);
451
+
452
+ window.addEventListener('resize', (v) => {
453
+ if(this.onResize) {
454
+ this.onResize([videoArea.root.clientWidth, videoArea.root.clientHeight]);
455
+ }
456
+ bottomArea.setSize([videoArea.root.clientWidth, 40]);
457
+ let availableWidth = this.controlsArea.root.clientWidth - controlsLeft.root.clientWidth - controlsRight.root.clientWidth;
458
+ this.timebar.resize([availableWidth, timeBarArea.root.clientHeight]);
459
+ })
460
+
461
+ videoArea.onresize = (v) => {
462
+ bottomArea.setSize([v.width, 40]);
463
+ }
464
+
465
+ timeBarArea.onresize = (v) => {
466
+ let availableWidth = this.controlsArea.root.clientWidth - controlsLeft.root.clientWidth - controlsRight.root.clientWidth;
467
+ this.timebar.resize([availableWidth, v.height]);
468
+ }
469
+
470
+ // Add canvas event listeneres
471
+ area.root.addEventListener( "mousedown", (event) => {
472
+ if(this.controls) {
473
+ this.timebar.onMouseDown(event);
474
+ }
475
+ });
476
+ area.root.addEventListener( "mouseup", (event) => {
477
+ if(this.controls) {
478
+ this.timebar.onMouseUp(event);
479
+ }
480
+ });
481
+ area.root.addEventListener( "mousemove", (event) => {
482
+ if(this.controls) {
483
+ this.timebar.onMouseMove(event);
484
+ }
485
+ });
486
+
487
+ }
488
+
489
+ async _loadVideo( options = {} ) {
490
+ while(this.video.duration === Infinity || isNaN(this.video.duration) || !this.timebar) {
491
+ await new Promise(r => setTimeout(r, 1000));
492
+ this.video.currentTime = 10000000 * Math.random();
493
+ }
494
+ this.video.currentTime = 0;
495
+ this.endTime = this.video.duration;
496
+ this.timebar.currentX = this._timeToX(0);
497
+ this._setEndValue(this.timebar.endX);
498
+ this._setStartValue(this.timebar.startX);
499
+ this._setCurrentValue(this.timebar.currentX);
500
+ this.timebar.update(this.timebar.currentX);
501
+ this._update();
502
+ this.controls = options.controls ?? true;
503
+ if(!this.controls) {
504
+ this.hideControls();
505
+ }
506
+
507
+ if(this.onVideoLoaded) {
508
+ this.onVideoLoaded(this.video);
509
+ }
510
+ }
511
+
512
+ _update () {
513
+
514
+ if(this.onDraw) {
515
+ this.onDraw();
516
+ }
517
+ if(this.playing) {
518
+ if(this.video.currentTime >= this.endTime) {
519
+ this.video.currentTime = this.startTime;
520
+ }
521
+ const x = this._timeToX(this.video.currentTime);
522
+ this._setCurrentValue(x, false);
523
+ this.timebar.update(x);
524
+ }
525
+
526
+ this.requestId = requestAnimationFrame(this._update.bind(this));
527
+ }
528
+
529
+ _xToTime (x) {
530
+ return ((x - this.timebar.offset) / (this.timebar.width - this.timebar.offset)) * this.video.duration;
531
+ }
532
+
533
+ _timeToX (time) {
534
+ return (time / this.video.duration) * (this.timebar.width - this.timebar.offset ) + this.timebar.offset;
535
+ }
536
+
537
+ _setCurrentValue ( x, updateTime = true ) {
538
+ const t = this._xToTime(x);
539
+
540
+ if(updateTime) {
541
+ this.video.currentTime = t;
542
+ }
543
+ //console.log( "Computed: " + t)
544
+ let mzminutes = Math.floor(t / 60);
545
+ let mzseconds = Math.floor(t - (mzminutes * 60));
546
+ let mzmiliseconds = Math.floor((t - mzseconds)*100);
547
+
548
+ mzmiliseconds = mzmiliseconds < 10 ? ('0' + mzmiliseconds) : mzmiliseconds;
549
+ mzseconds = mzseconds < 10 ? ('0' + mzseconds) : mzseconds;
550
+ mzminutes = mzminutes < 10 ? ('0' + mzminutes) : mzminutes;
551
+ this.currentTimeString = mzminutes+':'+mzseconds+'.'+mzmiliseconds;
552
+ this.controlsCurrentPanel.refresh();
553
+
554
+ if(this.onSetTime) {
555
+ this.onSetTime(t);
556
+ }
557
+ }
558
+
559
+ _setStartValue ( x ) {
560
+ const t = this._xToTime(x);
561
+ this.startTime = this.video.currentTime = t;
562
+
563
+ let mzminutes = Math.floor(t / 60);
564
+ let mzseconds = Math.floor(t - (mzminutes * 60));
565
+ let mzmiliseconds = Math.floor((t - mzseconds)*100);
566
+
567
+ mzmiliseconds = mzmiliseconds < 10 ? ('0' + mzmiliseconds) : mzmiliseconds;
568
+ mzseconds = mzseconds < 10 ? ('0' + mzseconds) : mzseconds;
569
+ mzminutes = mzminutes < 10 ? ('0' + mzminutes) : mzminutes;
570
+ this.startTimeString = mzminutes+':'+mzseconds+'.'+mzmiliseconds;
571
+ this.controlsPanelLeft.refresh();
572
+ if(this.onSetTime) {
573
+ this.onSetTime(t);
574
+ }
575
+ }
576
+
577
+ _setEndValue ( x ) {
578
+ const t = this._xToTime(x);
579
+ this.endTime = this.video.currentTime = t;
580
+
581
+ let mzminutes = Math.floor(t / 60);
582
+ let mzseconds = Math.floor(t - (mzminutes * 60));
583
+ let mzmiliseconds = Math.floor((t - mzseconds)*100);
584
+
585
+ mzmiliseconds = mzmiliseconds < 10 ? ('0' + mzmiliseconds) : mzmiliseconds;
586
+ mzseconds = mzseconds < 10 ? ('0' + mzseconds) : mzseconds;
587
+ mzminutes = mzminutes < 10 ? ('0' + mzminutes) : mzminutes;
588
+
589
+ this.endTimeString = mzminutes+':'+mzseconds+'.'+mzmiliseconds;
590
+ this.controlsPanelRight.refresh();
591
+ if(this.onSetTime) {
592
+ this.onSetTime(t);
593
+ }
594
+ }
595
+
596
+ getStartTime ( ) {
597
+ return this.startTime;
598
+ }
599
+
600
+ getEndTime ( ) {
601
+ return this.endTime;
602
+ }
603
+
604
+ getTrimedTimes ( ) {
605
+ return {start: this.startTime, end: this.endTime};
606
+ }
607
+
608
+ showControls ( ) {
609
+ this.controls = true;
610
+ this.controlsArea.show();
611
+ }
612
+
613
+ hideControls ( ) {
614
+ this.controls = false;
615
+ this.controlsArea.hide();
616
+ }
617
+
618
+ delete ( ) {
619
+ if(this.requestId) {
620
+ cancelAnimationFrame(this.requestId);
621
+ this.requestId = null;
622
+ }
623
+
624
+ delete this;
625
+ }
626
+ }
627
+
628
+ LX.VideoEditor = VideoEditor;
629
+
630
+ export { VideoEditor }