lexgui 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,4599 @@
1
+ import { LX } from 'lexgui';
2
+
3
+ if(!LX) {
4
+ throw("lexgui.js missing!");
5
+ }
6
+
7
+ LX.components.push( 'Timeline' );
8
+
9
+ /**
10
+ * @class Session
11
+ * @description Store info about timeline session
12
+ */
13
+
14
+ class Session {
15
+
16
+ constructor() {
17
+
18
+ this.start_time = - 0.01;
19
+ this.left_margin = 0;
20
+ // this.current_time = 0;
21
+ // this.last_time = 0;
22
+ // this.seconds_to_pixels = 50;
23
+ this.scroll_y = 0;
24
+ // this.offset_y = 0;
25
+ // this.selection = null;
26
+ }
27
+ };
28
+
29
+ /**
30
+ * @class Timeline
31
+ * @description Agnostic timeline, do not impose any timeline content. Renders to a canvas
32
+ */
33
+
34
+ class Timeline {
35
+
36
+ /**
37
+ * @param {string} name
38
+ * @param {object} options = {animationClip, selectedItems, position = [0,0], width, height, canvas, trackHeight, skipLock, skipVisibility}
39
+ */
40
+ constructor( name, options = {} ) {
41
+
42
+ this.name = name ?? '';
43
+ this.currentTime = 0;
44
+ this.framerate = 30;
45
+ this.opacity = options.opacity || 1;
46
+ this.sidebarWidth = 0// 200;
47
+ this.topMargin = 47;
48
+ this.renderOutFrames = false;
49
+ this.lastMouse = [];
50
+ this.lastKeyFramesSelected = [];
51
+ this.tracksDrawn = [];
52
+ this.buttonsDrawn = [];
53
+ this.trackState = [];
54
+ this.clipboard = null;
55
+ this.grabTime = 0;
56
+ this.timeBeforeMove = 0;
57
+ this.tracksPerItem = {};
58
+ this.tracksDictionary = {};
59
+ this._times = [];
60
+
61
+ this.session = new Session();
62
+
63
+ this.canvas = options.canvas ?? document.createElement('canvas');
64
+
65
+ this.duration = 1;
66
+ this.position = [options.x ?? 0, options.y ?? 0];
67
+ this.size = [ options.width ?? 400, options.height ?? 100];
68
+
69
+ this.currentScroll = 0; //in percentage
70
+ this.currentScrollInPixels = 0; //in pixels
71
+ this.scrollableHeight = this.size[1]; //true height of the timeline content
72
+ this.secondsToPixels = 300;
73
+ this.pixelsToSeconds = 1 / this.secondsToPixels;
74
+ this.selectedItems = options.selectedItems ?? null;
75
+ this.animationClip = options.animationClip ?? null;
76
+ this.trackHeight = options.trackHeight ?? 25;
77
+
78
+ this.active = true;
79
+ this.skipVisibility = options.skipVisibility ?? false;
80
+ this.skipLock = options.skipLock ?? false;
81
+
82
+ this.optimizeThreshold = 0.025;
83
+
84
+ this.root = new LX.Area({className : 'lextimeline'});
85
+
86
+ this.header_offset = 38;
87
+
88
+ let width = options.width ? options.width : null;
89
+ let height = options.height ? options.height - this.header_offset : null;
90
+
91
+ let area = new LX.Area( {id: "bottom-timeline-area", width: width || "calc(100% - 7px)", height: height || "100%"});
92
+ area.split({ type: "horizontal", sizes: ["15%", "85%"]});
93
+ this.content_area = area;
94
+ let [left, right] = area.sections;
95
+
96
+
97
+ right.root.appendChild(this.canvas);
98
+ this.canvasArea = right;
99
+ this.canvasArea.root.classList.add("lextimelinearea");
100
+ this.updateHeader();
101
+ this.updateLeftPanel(left);
102
+ this.root.root.appendChild(area.root);
103
+
104
+ if(!options.canvas && this.name != '') {
105
+ this.root.root.id = this.name;
106
+ this.canvas.id = this.name + '-canvas';
107
+ }
108
+
109
+ // Process mouse events
110
+ this.canvas.addEventListener("mousedown", this.processMouse.bind(this));
111
+ this.canvas.addEventListener("mouseup", this.processMouse.bind(this));
112
+ this.canvas.addEventListener("mousemove", this.processMouse.bind(this));
113
+ this.canvas.addEventListener("wheel", this.processMouse.bind(this));
114
+ this.canvas.addEventListener("dblclick", this.processMouse.bind(this));
115
+ this.canvas.addEventListener("contextmenu", this.processMouse.bind(this));
116
+
117
+ this.canvas.tabIndex = 1;
118
+ // Process keys events
119
+ this.canvas.addEventListener("keydown", this.processKeys.bind(this));
120
+
121
+ right.onresize = bounding => {
122
+ if(!(bounding.width && bounding.height))
123
+ return;
124
+ this.resizeCanvas( [ bounding.width, bounding.height + this.header_offset ] );
125
+ }
126
+ }
127
+
128
+ /**
129
+ * @method updateHeader
130
+ * @param {*}
131
+ */
132
+
133
+ updateHeader() {
134
+
135
+ if(this.header)
136
+ this.header.clear();
137
+ else {
138
+ this.header = new LX.Panel({id:'lextimelineheader', height: this.header_offset+"px"});
139
+ this.root.root.appendChild(this.header.root);
140
+ }
141
+
142
+ let header = this.header;
143
+ LX.DEFAULT_NAME_WIDTH = "50%";
144
+ header.sameLine();
145
+ header.addTitle(this.name);
146
+
147
+ header.addNumber("Duration", +this.duration.toFixed(3), (value, event) => {
148
+ this.setDuration(value, false)}, {step: 0.01, min: 0, signal: "@on_set_duration"});
149
+ header.addNumber("Current Time", this.currentTime, (value, event) => {
150
+ if(value > this.duration) {
151
+ value = this.duration;
152
+ LX.emit( "@on_current_time_" + this.constructor.name, value);
153
+ }
154
+
155
+ this.currentTime = value;
156
+ if(this.onSetTime)
157
+ this.onSetTime(this.currentTime);
158
+ this.draw();
159
+
160
+ }, {signal: "@on_current_time_" + this.constructor.name, step: 0.01, min: 0, precision: 3, skipSlider: true});
161
+
162
+
163
+ for(let i = 0; i < this.buttonsDrawn.length; i++) {
164
+ let button = this.buttonsDrawn[i];
165
+ this.header.addButton( button.title || "", button.name, button.callback, button);
166
+ }
167
+
168
+ if(this.onShowOptimizeMenu)
169
+ header.addButton("", '<i class="fa-solid fa-filter"></i>', (value, event) => {this.onShowOptimizeMenu(event)}, {width: "40px"});
170
+
171
+ header.addButton("", '<i class="fa-solid fa-gear"></i>', (value, event) => {
172
+ if(this.dialog)
173
+ return;
174
+ this.dialog = new LX.Dialog("Configuration", d => {
175
+ d.addNumber("Framerate", this.framerate, (v) => {
176
+ this.framerate = v;
177
+ }, {min: 0, disabled: false});
178
+ d.addNumber("Num items", Object.keys(this.tracksPerItem).length, null, {disabled: true});
179
+ d.addNumber("Num tracks", this.animationClip ? this.animationClip.tracks.length : 0, null, {disabled: true});
180
+ if(this.onShowOptimizeMenu)
181
+ d.addNumber("Optimize Threshold", this.optimizeThreshold, v => {
182
+ this.optimizeThreshold = v;
183
+ }, {min: 0, max: 0.25, step: 0.001, precision: 4});
184
+
185
+ }, {
186
+ onclose: (root) => {
187
+
188
+ root.remove();
189
+ this.dialog = null;
190
+ }
191
+ })
192
+ }, {width: "40px"})
193
+ header.endLine();
194
+ LX.DEFAULT_NAME_WIDTH = "30%";
195
+ }
196
+
197
+ /**
198
+ * @method updateLeftPanel
199
+ *
200
+ */
201
+ updateLeftPanel(area) {
202
+
203
+
204
+ if(this.leftPanel)
205
+ this.leftPanel.clear();
206
+ else {
207
+ this.leftPanel = area.addPanel({className: 'lextimelinepanel', width: "100%"});
208
+ }
209
+
210
+ let panel = this.leftPanel;
211
+ let title = panel.addTitle("Tracks");
212
+ const styles = window.getComputedStyle(title);
213
+ const titleHeight = title.clientHeight + parseFloat(styles['marginTop']) + parseFloat(styles['marginBottom']);
214
+ let p = new LX.Panel({height: "calc(100% - " + titleHeight + "px)"});
215
+
216
+ if(this.animationClip && this.selectedItems) {
217
+ let items = {'id': '', 'children': []};
218
+
219
+ for(let i = 0; i < this.selectedItems.length; i++ ) {
220
+ let selected = this.selectedItems[i];
221
+ let t = {
222
+ 'id': selected,
223
+ 'skipVisibility': true,
224
+ 'children': []
225
+ }
226
+ for(let j = 0; j < this.tracksPerItem[selected].length; j++) {
227
+ let track = this.tracksPerItem[selected][j];
228
+ let id = track.type ? track.type : track.name;
229
+
230
+ t.children.push({'id': id, 'skipVisibility': this.skipVisibility, visible: track.active, 'children':[], actions : this.skipLock ? null : [{
231
+ 'name':'Lock edition',
232
+ 'icon': 'fa-solid '+ (track.locked ? 'fa-lock' : 'fa-lock-open'),
233
+ 'callback': (el, node) => {
234
+ // TO DO (apply functionality)
235
+ let value = el.classList.contains('fa-lock');
236
+
237
+ if(value) {
238
+ el.title = 'Lock edition';
239
+ el.classList.remove('fa-lock');
240
+ el.classList.add('fa-lock-open');
241
+ }
242
+ else {
243
+ el.title = 'Unlock edition';
244
+ el.classList.remove('fa-lock-open');
245
+ el.classList.add('fa-lock');
246
+ }
247
+ let tracks = this.tracksPerItem[node.parent.id];
248
+ let type = node.id.replaceAll(node.parent.id, "").replaceAll(" (", "").replaceAll(")","");
249
+ let track = null;
250
+ for(let i = 0; i < tracks.length; i++) {
251
+ if(tracks[i].name == node.parent.id && type.includes(tracks[i].type)) {
252
+ tracks[i].locked = !value;
253
+ track = tracks[i];
254
+ }
255
+ }
256
+ if(this.onLockTrack)
257
+ this.onLockTrack(el, track, node)
258
+ this.draw();
259
+ }
260
+
261
+ }]})
262
+ // panel.addTitle(track.name + (track.type? '(' + track.type + ')' : ''));
263
+ }
264
+ items.children.push(t);
265
+ let el = p.addTree(null, t, {filter: false, rename: false, draggable: false, onevent: (e) => {
266
+ switch(e.type) {
267
+ case LX.TreeEvent.NODE_SELECTED:
268
+ this.selectTrack(e.node);
269
+ break;
270
+ case LX.TreeEvent.NODE_VISIBILITY:
271
+ this.changeTrackVisibility(e.node, e.value);
272
+ break;
273
+ case LX.TreeEvent.NODE_CARETCHANGED:
274
+ this.changeTrackDisplay(e.node, e.node.closed);
275
+ break;
276
+ }
277
+ }});
278
+
279
+ }
280
+ }
281
+ panel.attach(p.root)
282
+ p.root.style.overflowY = "scroll";
283
+ p.root.addEventListener("scroll", (e) => {
284
+
285
+ this.currentScroll = e.currentTarget.scrollTop/(e.currentTarget.scrollHeight - e.currentTarget.clientHeight);
286
+ })
287
+ // for(let i = 0; i < this.animationClip.tracks.length; i++) {
288
+ // let track = this.animationClip.tracks[i];
289
+ // panel.addTitle(track.name + (track.type? '(' + track.type + ')' : ''));
290
+ // }
291
+ if(this.leftPanel.parent.root.classList.contains("hidden") || !this.root.root.parent)
292
+ return;
293
+ this.resizeCanvas([ this.root.root.clientWidth - this.leftPanel.root.clientWidth - 8, this.size[1]]);
294
+ }
295
+
296
+ /**
297
+ * @method addButtons
298
+ * @param buttons: array
299
+ */
300
+
301
+ addButtons(buttons) {
302
+ this.buttonsDrawn = buttons || this.buttonsDrawn;
303
+ this.updateHeader();
304
+ }
305
+
306
+ /**
307
+ * @method addNewTrack
308
+ */
309
+
310
+ addNewTrack() {
311
+
312
+ if(!this.animationClip)
313
+ this.animationClip = {tracks:[]};
314
+
315
+ let trackInfo = {
316
+ idx: this.animationClip.tracks.length,
317
+ values: [], times: [],
318
+ selected: [], edited: [], hovered: []
319
+ };
320
+
321
+ this.animationClip.tracks.push(trackInfo);
322
+ return trackInfo.idx;
323
+ }
324
+
325
+ getTracksInRange( minY, maxY, threshold ) {
326
+
327
+ let tracks = [];
328
+
329
+ // Manage negative selection
330
+ if(minY > maxY) {
331
+ let aux = minY;
332
+ minY = maxY;
333
+ maxY = aux;
334
+ }
335
+
336
+ for(let i = this.tracksDrawn.length - 1; i >= 0; --i) {
337
+ let t = this.tracksDrawn[i];
338
+ let pos = t[1] - this.topMargin, size = t[2];
339
+ if( pos + threshold >= minY && (pos + size - threshold) <= maxY ) {
340
+ tracks.push( t[0] );
341
+ }
342
+ }
343
+
344
+ return tracks;
345
+ }
346
+
347
+ getCurrentContent(track, time, threshold) {
348
+
349
+ if(this.getCurrentKeyFrame)
350
+ this.getCurrentKeyFrame(track, time, threshold);
351
+
352
+ if(this.getCurrentClip)
353
+ this.getCurrentClip(track, time, threshold);
354
+ }
355
+
356
+ /**
357
+ * @method setAnimationClip
358
+ * @param {*} animation
359
+ * TODO
360
+ */
361
+
362
+ setAnimationClip( animation ) {
363
+ this.animationClip = animation;
364
+ this.duration = animation.duration;
365
+ this.speed = animation.speed || 1;
366
+ var w = Math.max(300, this.canvas.width);
367
+ this.secondsToPixels = ( w - this.session.left_margin ) / this.duration;
368
+ // if(this.secondsToPixels < 1)
369
+ // this.secondsToPixels = 100;
370
+ // this.session.start_time = -50 / this.secondsToPixels;
371
+
372
+ if(this.animationClip && this.animationClip.tracks.length)
373
+ this.processTracks(animation);
374
+
375
+ this.updateHeader();
376
+ this.updateLeftPanel();
377
+ }
378
+
379
+ drawTimeInfo (w, h = this.topMargin) {
380
+
381
+ let ctx = this.canvas.getContext("2d");
382
+ let canvas = this.canvas;
383
+ // Draw time markers
384
+ let startx = Math.round( this.timeToX( this.startTime ) ) + 0.5;
385
+ let endx = Math.round( this.timeToX( this.endTime ) ) + 0.5;
386
+ let tick_time = this.secondsToPixels > 400 ? 0.1 : 0.5;
387
+ if(this.secondsToPixels < 100 )
388
+ tick_time = 1;
389
+
390
+ ctx.save();
391
+
392
+ ctx.fillStyle = Timeline.BACKGROUND_COLOR;
393
+ ctx.fillRect( this.session.left_margin,0, canvas.width, h );
394
+
395
+ if(this.secondsToPixels > 200 )
396
+ {
397
+ ctx.strokeStyle = LX.getThemeColor("global-selected-light");
398
+ ctx.globalAlpha = 0.5 * (1.0 - LX.UTILS.clamp( 200 / this.secondsToPixels, 0, 1));
399
+ ctx.beginPath();
400
+ for( let time = this.startTime; time <= this.endTime; time += 1 / this.framerate )
401
+ {
402
+ let x = this.timeToX( time );
403
+ if(x < this.session.left_margin)
404
+ continue;
405
+ ctx.moveTo(Math.round(x) + 0.5, h * 0.75);
406
+ ctx.lineTo(Math.round(x) + 0.5, h - 1);
407
+ }
408
+ ctx.stroke();
409
+ ctx.globalAlpha = this.opacity;
410
+ }
411
+
412
+ ctx.globalAlpha = 0.5;
413
+ ctx.strokeStyle = LX.getThemeColor("global-selected-light");
414
+ ctx.beginPath();
415
+ let times = this._times;
416
+ this._times.length = 0;
417
+ for( let time = this.startTime; time <= this.endTime; time += tick_time)
418
+ {
419
+ let x = this.timeToX( time );
420
+
421
+ if(x < this.session.left_margin)
422
+ continue;
423
+
424
+ let is_tick = time % 5 == 0;
425
+ if(is_tick || this.secondsToPixels > 70 ) {
426
+
427
+ times.push([x,time]);
428
+ ctx.moveTo(Math.round(x) + 0.5, h * 0.5 + (is_tick ? 0 : h * 0.25) );
429
+ ctx.lineTo(Math.round(x) + 0.5, h);
430
+ }
431
+
432
+ }
433
+
434
+ let x = startx;
435
+ if(x < this.session.left_margin)
436
+ x = this.session.left_margin;
437
+ ctx.moveTo( x, h - 0.5);
438
+ ctx.lineTo( endx, h - 0.5);
439
+ ctx.stroke();
440
+ ctx.globalAlpha = this.opacity;
441
+
442
+ //time seconds in text
443
+ ctx.font = "11px " + Timeline.FONT;//"11px Calibri";
444
+ ctx.textAlign = "center";
445
+ ctx.fillStyle = Timeline.FONT_COLOR//"#888";
446
+ for(var i = 0; i < times.length; ++i)
447
+ {
448
+ let time = times[i][1];
449
+ ctx.fillText( time == (time|0) ? time : time.toFixed(1), times[i][0], 20);
450
+ }
451
+
452
+ ctx.restore();
453
+ }
454
+
455
+ drawTracksBackground(w, h) {
456
+
457
+ let canvas = this.canvas;
458
+ let ctx = canvas.getContext("2d");
459
+ let duration = this.duration;
460
+ ctx.globalAlpha = this.opacity;
461
+ //content
462
+ let margin = this.session.left_margin;
463
+ let timeline_height = this.topMargin;
464
+ let line_height = this.trackHeight;
465
+
466
+ //fill track lines
467
+ w = w || canvas.width;
468
+ let max_tracks = Math.ceil( (h - timeline_height + this.currentScrollInPixels) / line_height );
469
+
470
+ ctx.save();
471
+ ctx.fillStyle = Timeline.BACKGROUND_COLOR;
472
+ for(let i = 0; i <= max_tracks; ++i)
473
+ {
474
+ ctx.fillStyle = i % 2 == 0 ? Timeline.TRACK_COLOR_PRIMARY: Timeline.BACKGROUND_COLOR;
475
+ ctx.fillRect(0, timeline_height + i * line_height - this.currentScrollInPixels, w, line_height );
476
+ }
477
+
478
+ //black bg
479
+ ctx.globalAlpha = 0.7;
480
+ ctx.fillStyle = Timeline.BACKGROUND_COLOR;
481
+ ctx.fillRect( margin, 0, canvas.width - margin, canvas.height);
482
+ ctx.globalAlpha = this.opacity;
483
+
484
+ //bg lines
485
+ ctx.strokeStyle = "#444";
486
+ ctx.beginPath();
487
+
488
+ let pos = this.timeToX( 0 );
489
+ if(pos < margin)
490
+ pos = margin;
491
+ ctx.moveTo( pos + 0.5, timeline_height);
492
+ ctx.lineTo( pos + 0.5, canvas.height);
493
+ ctx.moveTo( Math.round( this.timeToX( duration ) ) + 0.5, timeline_height);
494
+ ctx.lineTo( Math.round( this.timeToX( duration ) ) + 0.5, canvas.height);
495
+ ctx.stroke();
496
+
497
+ ctx.restore();
498
+ }
499
+
500
+ /**
501
+ * @method draw
502
+ * @param {*} currentTime
503
+ * @param {*} rect
504
+ * TODO
505
+ */
506
+
507
+ draw( currentTime = this.currentTime, rect ) {
508
+
509
+ let ctx = this.canvas.getContext("2d");
510
+ if(!rect)
511
+ rect = [0, ctx.canvas.height - ctx.canvas.height , ctx.canvas.width, ctx.canvas.height ];
512
+
513
+ // this.canvas = ctx.canvas;
514
+ this.position[0] = rect[0];
515
+ this.position[1] = rect[1];
516
+ let w = rect[2];
517
+ let h = rect[3];
518
+ this.currentTime = currentTime;
519
+ // this.updateHeader();
520
+ this.currentScrollInPixels = this.scrollableHeight <= h ? 0 : (this.currentScroll * (this.scrollableHeight - h));
521
+
522
+ //zoom
523
+ if(this.duration > 0)
524
+ {
525
+ this.startTime = -50 / this.secondsToPixels;
526
+ // this.startTime = Math.floor( this.session.start_time ); //seconds
527
+ this.startTime = this.session.start_time ; //seconds
528
+ if(this.startTime < 0)
529
+ this.startTime = 0;
530
+ // this.endTime = Math.ceil( this.startTime + (w - this.session.left_margin) * this.pixelsToSeconds );
531
+ this.endTime = this.startTime + (w - this.session.left_margin) * this.pixelsToSeconds ;
532
+ if(this.endTime > this.duration)
533
+ this.endTime = this.duration;
534
+ if(this.startTime > this.endTime) //avoids weird bug
535
+ this.endTime = this.startTime + 1;
536
+ }
537
+
538
+ this.tracksDrawn.length = 0;
539
+
540
+ // Background
541
+ ctx.globalAlpha = this.opacity;
542
+ ctx.fillStyle = Timeline.TRACK_COLOR_SECONDARY;
543
+ ctx.clearRect(0,0, this.canvas.width, this.canvas.height );
544
+
545
+ this.drawTracksBackground(w, h);
546
+
547
+ if(this.onDrawContent && this.animationClip) {
548
+
549
+ ctx.translate( this.position[0], this.position[1] + this.topMargin ); //20 is the top margin area
550
+
551
+ this.onDrawContent( ctx, this.timeStart, this.timeEnd, this );
552
+
553
+ ctx.translate( -this.position[0], -(this.position[1] + this.topMargin) ); //20 is the top margin area
554
+ }
555
+
556
+ //scrollbar
557
+ if( h < this.scrollableHeight )
558
+ {
559
+ ctx.fillStyle = "#222";
560
+ ctx.fillRect( w - this.session.left_margin - 10, 0, 10, h );
561
+ var scrollh = (h - this.topMargin)*h/ this.scrollableHeight;
562
+ ctx.fillStyle = this.grabbingScroll ? Timeline.FONT_COLOR : Timeline.TRACK_COLOR_SECONDARY;
563
+ ctx.roundRect( w - 8, this.currentScroll * (h - scrollh) + this.topMargin, 4, scrollh - this.topMargin, 5, true );
564
+ }
565
+ this.drawTimeInfo(w);
566
+
567
+ // Current time marker vertical line
568
+ let true_pos = Math.round( this.timeToX( this.currentTime ) ) + 0.5;
569
+ let quant_current_time = Math.round( this.currentTime * this.framerate ) / this.framerate;
570
+ let pos = Math.round( this.timeToX( quant_current_time ) ) + 0.5; //current_time is quantized
571
+ if(pos >= this.session.left_margin)
572
+ {
573
+ // ctx.strokeStyle = "#ABA";
574
+ // ctx.beginPath();
575
+ // ctx.globalAlpha = 0.3;
576
+ // ctx.moveTo(pos, 0); ctx.lineTo( pos, h );
577
+ // ctx.stroke();
578
+
579
+ ctx.strokeStyle = ctx.fillStyle = "#ADF";
580
+ ctx.globalAlpha = this.opacity;
581
+ ctx.beginPath();
582
+ ctx.moveTo(true_pos, 0); ctx.lineTo(true_pos, this.canvas.height);//line
583
+ ctx.stroke();
584
+ ctx.beginPath();
585
+ ctx.moveTo(true_pos - 4, 0); ctx.lineTo(true_pos + 4, 0); ctx.lineTo(true_pos, 6);//triangle
586
+ ctx.closePath();
587
+ ctx.fill();
588
+ }
589
+
590
+ // Selections
591
+ ctx.translate( this.position[0], this.position[1] + this.topMargin )
592
+ if(this.boxSelection && this.boxSelectionStart && this.boxSelectionEnd) {
593
+ ctx.globalAlpha = 0.5;
594
+ ctx.fillStyle = "#AAA";
595
+ ctx.strokeRect( this.boxSelectionStart[0], this.boxSelectionStart[1], this.boxSelectionEnd[0] - this.boxSelectionStart[0], this.boxSelectionEnd[1] - this.boxSelectionStart[1]);
596
+ ctx.stroke();
597
+ ctx.globalAlpha = this.opacity;
598
+ }
599
+ ctx.translate( -this.position[0], -(this.position[1] + this.topMargin) ); //20 is the top margin area
600
+
601
+ }
602
+
603
+ /**
604
+ * @method drawMarkers
605
+ * @param {*} ctx
606
+ * @param {*} markers
607
+ * TODO
608
+ */
609
+
610
+ drawMarkers( ctx, markers ) {
611
+
612
+ //render markers
613
+ ctx.fillStyle = "white";
614
+ ctx.textAlign = "left";
615
+ let markersPos = [];
616
+ for (let i = 0; i < markers.length; ++i) {
617
+ let marker = markers[i];
618
+ if (marker.time < this.startTime - this.pixelsToSeconds * 100 ||
619
+ marker.time > this.endTime)
620
+ continue;
621
+ var x = this.timeToX(marker.time);
622
+ markersPos.push(x);
623
+ ctx.save();
624
+ ctx.translate(x, 0);
625
+ ctx.rotate(Math.PI * -0.25);
626
+ ctx.fillText(marker.title, 20, 4);
627
+ ctx.restore();
628
+ }
629
+
630
+ if (markersPos.length) {
631
+ ctx.beginPath();
632
+ for (var i = 0; i < markersPos.length; ++i) {
633
+ ctx.moveTo(markersPos[i] - 5, 0);
634
+ ctx.lineTo(markersPos[i], -5);
635
+ ctx.lineTo(markersPos[i] + 5, 0);
636
+ ctx.lineTo(markersPos[i], 5);
637
+ ctx.lineTo(markersPos[i] - 5, 0);
638
+ }
639
+ ctx.fill();
640
+ }
641
+ }
642
+
643
+ /**
644
+ * @method clearState
645
+ */
646
+
647
+ clearState() {
648
+ this.trackState = [];
649
+ }
650
+
651
+ /**
652
+ * @method setDuration
653
+ * @param {Number} t
654
+ */
655
+
656
+ setDuration( t, updateHeader = true ) {
657
+ let v = this.validateDuration(t);
658
+ let decimals = t.toString().split('.')[1] ? t.toString().split('.')[1].length : 0;
659
+ updateHeader = (updateHeader || +v.toFixed(decimals) != t);
660
+ this.duration = this.animationClip.duration = v;
661
+
662
+ if(updateHeader) {
663
+ LX.emit( "@on_set_duration", +v.toFixed(3));
664
+ }
665
+
666
+ if( this.onSetDuration )
667
+ this.onSetDuration( v );
668
+ }
669
+
670
+ /**
671
+ * @method validateDuration
672
+ * @param {Number} t
673
+ * @returns minimum available duration
674
+ */
675
+ validateDuration(t) {
676
+ return t;
677
+ }
678
+
679
+ // Converts distance in pixels to time
680
+ xToTime( x ) {
681
+ return (x - this.session.left_margin) / this.secondsToPixels + this.session.start_time;
682
+ }
683
+
684
+ // Converts time to disance in pixels
685
+ timeToX( t ) {
686
+ return this.session.left_margin + (t - this.session.start_time) * this.secondsToPixels;
687
+ }
688
+
689
+ getCurrentFrame( framerate ) {
690
+ return Math.floor(this.currentTime * framerate);
691
+ }
692
+
693
+ /**
694
+ * @method setScale
695
+ * @param {*} v
696
+ * TODO
697
+ */
698
+
699
+ setScale( v ) {
700
+
701
+ if(!this.session)
702
+ return;
703
+
704
+ var centerx = this.canvas.width * 0.5;
705
+ var x = this.xToTime( centerx );
706
+ this.secondsToPixels *= v;
707
+ this.pixelsToSeconds = 1 / this.secondsToPixels;
708
+ this.session.start_time += x - this.xToTime( centerx );
709
+ this.draw();
710
+ }
711
+
712
+ /**
713
+ * @method setFramerate
714
+ * @param {*} v
715
+ */
716
+
717
+ setFramerate( v ) {
718
+ this.framerate = v;
719
+ }
720
+
721
+ /**
722
+ * @method processMouse
723
+ * @param {*} e
724
+ */
725
+
726
+ processMouse( e ) {
727
+
728
+ if(!this.canvas)
729
+ return;
730
+ // e.preventDefault();
731
+ // e.stopPropagation();
732
+ e.multipleSelection = false;
733
+
734
+ let h = this.canvas.height;
735
+ let w = this.canvas.width;
736
+ // Process mouse
737
+ let x = e.offsetX;
738
+ let y = e.offsetY;
739
+ e.deltax = x - this.lastMouse[0];
740
+ e.deltay = y - this.lastMouse[1];
741
+ let localX = e.offsetX - this.position[0];
742
+ let localY = e.offsetY - this.position[1];
743
+
744
+ // if(!this.grabbing_timeline && !this.movingKeys)
745
+ // this.canvas.style.cursor = "default";
746
+
747
+
748
+ let timeX = this.timeToX( this.currentTime );
749
+ let current_grabbing_timeline = localY < this.topMargin && localX > this.session.left_margin &&
750
+ localX > (timeX - 6) && localX < (timeX + 6);
751
+
752
+ if( current_grabbing_timeline )
753
+ this.canvas.style.cursor = "col-resize";
754
+ else if(this.movingKeys) {
755
+ this.canvas.style.cursor = "grabbing";
756
+ }
757
+ else
758
+ this.canvas.style.cursor = "default";
759
+
760
+ if( e.type == "wheel" ) {
761
+ if(e.shiftKey)
762
+ {
763
+ this.setScale( e.wheelDelta < 0 ? 0.95 : 1.05 );
764
+ }
765
+ else if( h < this.scrollableHeight)
766
+ {
767
+ this.currentScroll = LX.UTILS.clamp( this.currentScroll + (e.wheelDelta < 0 ? 0.1 : -0.1), 0, 1);
768
+ this.leftPanel.root.children[1].scrollTop = this.currentScroll* (this.scrollableHeight - h);
769
+ }
770
+
771
+ return;
772
+ }
773
+
774
+ var time = this.xToTime(x, true);
775
+
776
+ var is_inside = x >= this.position[0] && x <= (this.position[0] + this.size[0]) &&
777
+ y >= this.position[1] && y <= (this.position[1] + this.size[1]);
778
+
779
+ var track = null;
780
+ for(var i = this.tracksDrawn.length - 1; i >= 0; --i)
781
+ {
782
+ var t = this.tracksDrawn[i];
783
+ if( localY >= t[1] && localY < (t[1] + t[2]) )
784
+ {
785
+ track = t[0];
786
+ break;
787
+ }
788
+ }
789
+
790
+ e.track = track;
791
+ e.localX = localX;
792
+ e.localY = localY;
793
+
794
+
795
+
796
+ const innerSetTime = (t) => {
797
+ LX.emit( "@on_current_time_" + this.constructor.name, t);
798
+ if( this.onSetTime )
799
+ this.onSetTime( t );
800
+ }
801
+
802
+ if( e.type == "mouseup" )
803
+ {
804
+ if(!this.active) {
805
+ this.grabbing_timeline = false;
806
+ this.grabbing = false;
807
+ this.grabbingScroll = false;
808
+ this.movingKeys = false;
809
+ this.timeBeforeMove = null;
810
+ return;
811
+ }
812
+ // this.canvas.style.cursor = "default";
813
+ const discard = this.movingKeys || (LX.UTILS.getTime() - this.clickTime) > 420; // ms
814
+ this.movingKeys ? innerSetTime( this.currentTime ) : 0;
815
+
816
+ if(e.button == 0 && this.grabbing && this.onClipMoved && this.lastClipsSelected.length){
817
+ this.onClipMoved(this.lastClipsSelected);
818
+ }
819
+
820
+ this.grabbing_timeline = false;
821
+ this.grabbing = false;
822
+ this.grabbingScroll = false;
823
+ this.movingKeys = false;
824
+ this.timeBeforeMove = null;
825
+ e.discard = discard;
826
+
827
+ if(e.localY <= this.topMargin && !e.shiftKey) {
828
+ this.currentTime = Math.max(0, time);
829
+ innerSetTime(this.currentTime);
830
+ return;
831
+ }
832
+
833
+ if( e.button == 0 && this.onMouseUp )
834
+ this.onMouseUp(e, time);
835
+ }
836
+
837
+ if( !is_inside && !this.grabbing && !(e.metaKey || e.altKey ) )
838
+ return true;
839
+
840
+ if( this.onMouse && this.onMouse( e, time, this ) )
841
+ return;
842
+
843
+ if( e.type == "mousedown") {
844
+
845
+ this.clickTime = LX.UTILS.getTime();
846
+
847
+ if(this.trackBulletCallback && e.track)
848
+ this.trackBulletCallback(e.track,e,this,[localX,localY]);
849
+
850
+
851
+ if( h < this.scrollableHeight && x > w - 10 )
852
+ {
853
+ this.grabbingScroll = true;
854
+ this.grabbing = true;
855
+ }
856
+ else
857
+ {
858
+ if(!track || track && this.getCurrentContent(track, time, 0.001) == undefined) {
859
+
860
+ this.grabbing = true;
861
+ this.grabTime = time - this.currentTime;
862
+
863
+ this.grabbing_timeline = current_grabbing_timeline;
864
+ }
865
+
866
+ if(this.onMouseDown && this.active )
867
+ this.onMouseDown(e, time);
868
+ }
869
+ }
870
+ else if( e.type == "mousemove" ) {
871
+
872
+ if(e.shiftKey && this.active) {
873
+ if(this.boxSelection) {
874
+ this.boxSelectionEnd = [localX, localY - this.topMargin ];
875
+ return; // Handled
876
+ }
877
+ }
878
+ else if(this.grabbing && e.button !=2 && !this.movingKeys ) {
879
+ this.canvas.style.cursor = "grabbing";
880
+ if(this.grabbing_timeline && this.active)
881
+ {
882
+ let time = this.xToTime( localX );
883
+ time = Math.max(0, time);
884
+ this.currentTime = Math.min(this.duration, time);
885
+ this.draw();
886
+ LX.emit( "@on_current_time_" + this.constructor.name, this.currentTime );
887
+ }
888
+ else if(this.grabbingScroll )
889
+ {
890
+ this.currentScroll = LX.UTILS.clamp( this.currentScroll + (e.movementY / h), 0, 1);
891
+ }
892
+ else
893
+ {
894
+ // Move timeline in X (independent of current time)
895
+ var old = this.xToTime( this.lastMouse[0] );
896
+ var now = this.xToTime( e.offsetX );
897
+ this.session.start_time += (old - now);
898
+ }
899
+ }
900
+ if(this.onMouseMove)
901
+ this.onMouseMove(e, time);
902
+ }
903
+ else if (e.type == "dblclick" && this.onDblClick) {
904
+ this.onDblClick(e);
905
+ }
906
+ else if (e.type == "contextmenu" && this.showContextMenu && this.active)
907
+ this.showContextMenu(e);
908
+
909
+ this.lastMouse[0] = x;
910
+ this.lastMouse[1] = y;
911
+
912
+ return true;
913
+ }
914
+
915
+ /**
916
+ * @method processKeys
917
+ * @param {*} e
918
+ */
919
+ processKeys(e) {
920
+
921
+ if( e.type == 'keydown' ) {
922
+ switch(e.key) {
923
+ case 'Delete': case 'Backspace':
924
+ this.deleteContent();
925
+ break;
926
+ case 'c': case 'C':
927
+ if(e.ctrlKey)
928
+ this.copyContent();
929
+ break;
930
+ case 'v': case 'V':
931
+ if(e.ctrlKey)
932
+ this.pasteContent();
933
+ break;
934
+ }
935
+ }
936
+ }
937
+
938
+ /**
939
+ * @method drawTrackWithKeyframes
940
+ * @param {*} ctx
941
+ * ...
942
+ * @description helper function, you can call it from onDrawContent to render all the keyframes
943
+ * TODO
944
+ */
945
+
946
+ drawTrackWithKeyframes( ctx, y, trackHeight, title, track, trackInfo ) {
947
+
948
+ if(trackInfo.enabled === false)
949
+ ctx.globalAlpha = 0.4;
950
+
951
+ ctx.font = Math.floor( trackHeight * 0.8) + "px Arial";
952
+ ctx.textAlign = "left";
953
+ ctx.fillStyle = "rgba(255,255,255,0.8)";
954
+
955
+ // if(title != null)
956
+ // {
957
+ // // var info = ctx.measureText( title );
958
+ // ctx.fillStyle = this.active ? "rgba(255,255,255,0.9)" : "rgba(250,250,250,0.7)";
959
+ // ctx.fillText( title, 25, y + trackHeight * 0.75 );
960
+ // }
961
+ ctx.globalAlpha = 0.2;
962
+ ctx.fillStyle = Timeline.TRACK_SELECTED//"#2c303570";
963
+ if(trackInfo.isSelected)
964
+ ctx.fillRect(0, y, ctx.canvas.width, trackHeight );
965
+ ctx.globalAlpha = 1;
966
+ ctx.fillStyle = "#5e9fdd"//"rgba(10,200,200,1)";
967
+ var keyframes = track.times;
968
+
969
+ if(keyframes) {
970
+
971
+ this.tracksDrawn.push([track,y+this.topMargin,trackHeight]);
972
+ for(var j = 0; j < keyframes.length; ++j)
973
+ {
974
+ let time = keyframes[j];
975
+ let selected = trackInfo.selected[j];
976
+ if( time < this.startTime || time > this.endTime )
977
+ continue;
978
+ var keyframePosX = this.timeToX( time );
979
+ if( keyframePosX > this.sidebarWidth ){
980
+ ctx.save();
981
+
982
+ let margin = -1;
983
+ let size = trackHeight * 0.3;
984
+ if(trackInfo.edited[j])
985
+ ctx.fillStyle = Timeline.COLOR_EDITED;
986
+ if(selected) {
987
+ ctx.fillStyle = Timeline.COLOR_SELECTED;
988
+ size = trackHeight * 0.35;
989
+ margin = 0;
990
+ }
991
+ if(trackInfo.hovered[j]) {
992
+ size = trackHeight * 0.35;
993
+ ctx.fillStyle = Timeline.COLOR_HOVERED;
994
+ margin = 0;
995
+ }
996
+ if(trackInfo.locked)
997
+ ctx.fillStyle = Timeline.COLOR_LOCK;
998
+
999
+ if(!this.active || trackInfo.active == false)
1000
+ ctx.fillStyle = Timeline.COLOR_UNACTIVE;
1001
+
1002
+ ctx.translate(keyframePosX, y + this.trackHeight * 0.75 + margin);
1003
+ ctx.rotate(45 * Math.PI / 180);
1004
+ ctx.fillRect( -size, -size, size, size);
1005
+ if(selected) {
1006
+ ctx.globalAlpha = 0.3;
1007
+ ctx.fillRect( -size*1.5, -size*1.5, size*2, size*2);
1008
+ }
1009
+
1010
+ ctx.restore();
1011
+ }
1012
+ }
1013
+ }
1014
+
1015
+ ctx.globalAlpha = this.opacity;
1016
+ }
1017
+
1018
+ /**
1019
+ * @method drawTrackWithBoxes
1020
+ * @param {*} ctx
1021
+ * ...
1022
+ * TODO
1023
+ */
1024
+
1025
+ drawTrackWithBoxes( ctx, y, trackHeight, title, track ) {
1026
+
1027
+ let offset = (trackHeight - trackHeight *0.6)*0.5;
1028
+ this.tracksDrawn.push([track,y+this.topMargin,trackHeight]);
1029
+ trackHeight *= 0.6;
1030
+ let selectedClipArea = null;
1031
+
1032
+ if(track.enabled === false)
1033
+ ctx.globalAlpha = 0.4;
1034
+ this.canvas = this.canvas || ctx.canvas;
1035
+ ctx.font = Math.floor( trackHeight * 0.8) + "px Arial";
1036
+ ctx.textAlign = "left";
1037
+ ctx.fillStyle = "rgba(255,255,255,0.8)";
1038
+
1039
+ // if(title != null)
1040
+ // {
1041
+ // // var info = ctx.measureText( title );
1042
+ // ctx.fillStyle = "rgba(255,255,255,0.9)";
1043
+ // ctx.fillText( title, 25, y + trackHeight * 0.8 );
1044
+ // }
1045
+
1046
+ ctx.fillStyle = "rgba(10,200,200,1)";
1047
+ var clips = track.clips;
1048
+ let trackAlpha = 1;
1049
+
1050
+ if(clips) {
1051
+
1052
+ for(var j = 0; j < clips.length; ++j)
1053
+ {
1054
+ let clip = clips[j];
1055
+ //let selected = track.selected[j];
1056
+ var x = Math.floor( this.timeToX(clip.start) ) + 0.5;
1057
+ var x2 = Math.floor( this.timeToX( clip.start + clip.duration ) ) + 0.5;
1058
+ var w = x2-x;
1059
+
1060
+ if( x2 < 0 || x > this.canvas.width )
1061
+ continue;
1062
+
1063
+ //background rect
1064
+ ctx.globalAlpha = trackAlpha;
1065
+ ctx.fillStyle = clip.clipColor || "#5e9fdd"//#333";
1066
+ //ctx.fillRect(x,y,w,trackHeight);
1067
+ ctx.roundRect( x, y + offset, w, trackHeight , 5, true);
1068
+
1069
+ let fadeinX = this.secondsToPixels * ((clip.fadein || 0) - clip.start);
1070
+ let fadeoutX = this.secondsToPixels * (clip.start + clip.duration - (clip.fadeout || (clip.start + clip.duration)));
1071
+
1072
+ let color = LX.UTILS.HexToRgb(ctx.fillStyle);
1073
+ color = color.map(x => x*=0.8);
1074
+ ctx.fillStyle = 'rgba(' + color.join(',') + ', 0.8)';
1075
+
1076
+ if(fadeinX>0)
1077
+ ctx.roundRect(x, y + offset, fadeinX, trackHeight, {tl: 5, bl: 5, tr:0, br:0}, true);
1078
+ if(fadeoutX)
1079
+ ctx.roundRect( x + w - fadeoutX, y + offset, fadeoutX, trackHeight, {tl: 0, bl: 0, tr:5, br:5}, true);
1080
+
1081
+ // //draw clip content
1082
+ // if( clip.drawClip )
1083
+ // {
1084
+ // ctx.save();
1085
+ // ctx.translate(x,y);
1086
+ // ctx.strokeStyle = "#AAA";
1087
+ // ctx.fillStyle = "#AAA";
1088
+ // clip.drawClip( ctx, x2-x, trackHeight, this.selectedClip == clip || track.selected[j], this );
1089
+ // ctx.restore();
1090
+ // }
1091
+ //draw clip outline
1092
+ if(clip.hidden)
1093
+ ctx.globalAlpha = trackAlpha * 0.5;
1094
+
1095
+ var safex = Math.max(-2, x );
1096
+ var safex2 = Math.min( this.canvas.width + 2, x2 );
1097
+ // ctx.lineWidth = 0.5;
1098
+ // ctx.strokeStyle = clip.constructor.color || "black";
1099
+ // ctx.strokeRect( safex, y, safex2-safex, trackHeight );
1100
+ ctx.globalAlpha = trackAlpha;
1101
+ if(this.selectedClip == clip || track.selected[j])
1102
+ selectedClipArea = [x, y + offset, x2-x, trackHeight];
1103
+
1104
+ ctx.font = "12px Rubik";
1105
+ //render clip selection area
1106
+ if(selectedClipArea)
1107
+ {
1108
+ ctx.strokeStyle = track.clips[j].clipColor;
1109
+ ctx.globalAlpha = 0.5;
1110
+ ctx.lineWidth = 2.5;
1111
+ ctx.roundRect(selectedClipArea[0]-1,selectedClipArea[1]-1,selectedClipArea[2]+2,selectedClipArea[3]+2, 5, false, true);
1112
+ ctx.strokeStyle = "#888";
1113
+ ctx.lineWidth = 0.5;
1114
+ ctx.globalAlpha = this.opacity;
1115
+ ctx.font = "bold 13px Rubik";
1116
+ }
1117
+
1118
+ let text = clip.id.replaceAll("_", " ").replaceAll("-", " ");
1119
+ let textInfo = ctx.measureText( text );
1120
+ ctx.fillStyle = clip.color || Timeline.FONT_COLOR;
1121
+
1122
+ if( textInfo.width < (w - 24) )
1123
+ ctx.fillText( text, x + (w - textInfo.width)*0.5, y + offset + 12 );
1124
+ ctx.font = "12px Rubik";
1125
+ }
1126
+ }
1127
+
1128
+ //ctx.restore();
1129
+ }
1130
+
1131
+ /**
1132
+ * @method selectTrack
1133
+ * @param {id, parent, children, visible} trackInfo
1134
+ */
1135
+
1136
+ selectTrack( trackInfo) {
1137
+ this.unSelectAllTracks();
1138
+
1139
+ let [name, type] = trackInfo.id.split(" (");
1140
+
1141
+ if(type)
1142
+ type = type.replaceAll(")", "").replaceAll(" ", "");
1143
+ else {
1144
+ type = name;
1145
+ name = trackInfo.parent ? trackInfo.parent.id : trackInfo.id;
1146
+ }
1147
+ let tracks = this.tracksPerItem[name];
1148
+
1149
+ for(let i = 0; i < tracks.length; i++) {
1150
+ if(tracks[i].type != type && tracks.length > 1)
1151
+ continue;
1152
+ this.tracksPerItem[name][i].isSelected = true;
1153
+ trackInfo = this.tracksPerItem[name][i];
1154
+ }
1155
+
1156
+ if(this.onSelectTrack)
1157
+ this.onSelectTrack(trackInfo);
1158
+ }
1159
+
1160
+ unSelectAllTracks() {
1161
+ for(let i = 0; i < this.selectedItems.length; i++) {
1162
+ let item = this.selectedItems[i];
1163
+ let tracks = this.tracksPerItem[item];
1164
+ for(let t = 0; t < tracks.length; t++) {
1165
+ tracks[t].isSelected = false;
1166
+ }
1167
+ }
1168
+ }
1169
+
1170
+ /**
1171
+ * @method changeTrackVisibility
1172
+ * @param {id, parent, children, visible} trackInfo
1173
+ */
1174
+
1175
+ changeTrackVisibility(trackInfo, visible) {
1176
+ let [name, type] = trackInfo.id.split(" (");
1177
+ if(type)
1178
+ type = type.replaceAll(")", "").replaceAll(" ", "");
1179
+ else {
1180
+ type = name;
1181
+ name = trackInfo.parent ? trackInfo.parent.id : trackInfo.id;
1182
+ }
1183
+ trackInfo = {name, type};
1184
+ let tracks = this.tracksPerItem[name];
1185
+
1186
+ for(let i = 0; i < tracks.length; i++) {
1187
+ if(tracks[i].type != type && tracks.length > 1)
1188
+ continue;
1189
+ this.tracksPerItem[name][i].active = visible;
1190
+ trackInfo = this.tracksPerItem[name][i];
1191
+ }
1192
+ this.draw();
1193
+ if(this.onChangeTrackVisibility)
1194
+ this.onChangeTrackVisibility(trackInfo, visible);
1195
+ }
1196
+
1197
+ /**
1198
+ * @method changeTrackDisplay
1199
+ * @param {id, parent, children, display} trackInfo
1200
+ */
1201
+
1202
+ changeTrackDisplay(trackInfo, hide) {
1203
+
1204
+ for(let idx = 0; idx < trackInfo.children.length; idx++) {
1205
+ let [name, type] = trackInfo.children[idx].id.split(" (");
1206
+ if(type)
1207
+ type = type.replaceAll(")", "").replaceAll(" ", "");
1208
+ else {
1209
+ type = name;
1210
+ name = trackInfo.parent ? trackInfo.parent.id : trackInfo.id;
1211
+ }
1212
+ //trackInfo = {name, type};
1213
+ let tracks = this.tracksPerItem[name];
1214
+
1215
+ for(let i = 0; i < tracks.length; i++) {
1216
+ if(tracks[i].type != type && tracks.length > 1)
1217
+ continue;
1218
+ this.tracksPerItem[name][i].hide = hide;
1219
+ // trackInfo = this.tracksPerItem[name][i];
1220
+ }
1221
+ }
1222
+
1223
+ this.draw();
1224
+
1225
+ if(this.onChangeTrackDisplay)
1226
+ this.onChangeTrackDisplay(trackInfo, hide)
1227
+ }
1228
+
1229
+ /**
1230
+ * @method resize
1231
+ * @param {*} size
1232
+ *
1233
+ *
1234
+ */
1235
+ resize( size = [this.root.parent.root.clientWidth, this.root.parent.root.clientHeight]) {
1236
+
1237
+ this.root.root.style.width = size[0] + "px";
1238
+ this.root.root.style.height = size[1] + "px";
1239
+
1240
+ this.size = size;
1241
+ this.content_area.setSize([size[0], size[1] - this.header_offset]);
1242
+
1243
+ let w = size[0] - this.leftPanel.root.clientWidth - 8;
1244
+ this.resizeCanvas([w , size[1]]);
1245
+ }
1246
+
1247
+ resizeCanvas( size ) {
1248
+ if( size[0] <= 0 && size[1] <=0 )
1249
+ return;
1250
+
1251
+ size[1] -= this.header_offset;
1252
+ this.canvasArea.setSize(size);
1253
+ this.canvas.width = size[0];
1254
+ this.canvas.height = size[1];
1255
+ var w = Math.max(300, this.canvas.width);
1256
+ this.secondsToPixels = ( w - this.session.left_margin ) / this.duration;
1257
+ this.pixelsToSeconds = 1 / this.secondsToPixels;
1258
+ this.draw(this.currentTime);
1259
+
1260
+ }
1261
+
1262
+ /**
1263
+ * @method hide
1264
+ * Hide timeline area
1265
+ */
1266
+ hide() {
1267
+ this.root.hide();
1268
+ }
1269
+
1270
+ /**
1271
+ * @method show
1272
+ * Show timeline area if it is hidden
1273
+ */
1274
+ show() {
1275
+
1276
+ this.root.show();
1277
+ this.resize();
1278
+
1279
+ }
1280
+ };
1281
+
1282
+ Timeline.BACKGROUND_COLOR = LX.getThemeColor("global-color-primary");
1283
+ Timeline.TRACK_COLOR_PRIMARY = LX.getThemeColor("global-color-secondary");
1284
+ Timeline.TRACK_COLOR_SECONDARY = LX.getThemeColor("global-color-terciary");
1285
+ Timeline.TRACK_SELECTED = LX.getThemeColor("global-selected");
1286
+ Timeline.FONT = LX.getThemeColor("global-font");
1287
+ Timeline.FONT_COLOR = LX.getThemeColor("global-text");
1288
+ Timeline.COLOR = "#5e9fdd";
1289
+ Timeline.COLOR_HOVERED = "rgba(250,250,250,0.7)";
1290
+ Timeline.COLOR_SELECTED = "rgba(250,250,20,1)"///"rgba(250,250,20,1)";
1291
+ Timeline.COLOR_UNACTIVE = "rgba(250,250,250,0.7)";
1292
+ Timeline.COLOR_LOCK = "rgba(255,125,125,0.7)";
1293
+ Timeline.COLOR_EDITED = "white"//"rgba(125,250,250, 1)";
1294
+
1295
+ LX.Timeline = Timeline;
1296
+
1297
+ /**
1298
+ * @class KeyFramesTimeline
1299
+ */
1300
+
1301
+ class KeyFramesTimeline extends Timeline {
1302
+
1303
+ /**
1304
+ * @param {string} name
1305
+ * @param {object} options = {animationClip, selectedItems, x, y, width, height, canvas, trackHeight}
1306
+ */
1307
+ constructor(name, options = {}) {
1308
+
1309
+ super(name, options);
1310
+
1311
+ this.tracksPerItem = {};
1312
+
1313
+ // this.selectedItems = selectedItems;
1314
+ this.snappedKeyFrameIndex = -1;
1315
+ this.autoKeyEnabled = false;
1316
+
1317
+
1318
+ if(this.animationClip && this.animationClip.tracks.length)
1319
+ this.processTracks(this.animationClip);
1320
+
1321
+ // Add button data
1322
+ let offset = 25;
1323
+ if(this.active)
1324
+ {
1325
+
1326
+ }
1327
+ }
1328
+
1329
+ onMouseUp( e, time ) {
1330
+
1331
+ let track = e.track;
1332
+ let localX = e.localX;
1333
+ let discard = e.discard;
1334
+
1335
+ if(e.shiftKey) {
1336
+ e.multipleSelection = true;
1337
+ // Multiple selection
1338
+ if(!discard && track) {
1339
+ this.processCurrentKeyFrame( e, null, track, localX, true );
1340
+ }
1341
+ // Box selection
1342
+ else if(this.boxSelection) {
1343
+
1344
+ this.unSelectAllKeyFrames();
1345
+
1346
+ let tracks = this.getTracksInRange(this.boxSelectionStart[1], this.boxSelectionEnd[1], this.pixelsToSeconds * 5);
1347
+
1348
+ for(let t of tracks) {
1349
+ let keyFrameIndices = this.getKeyFramesInRange(t,
1350
+ this.xToTime( this.boxSelectionStart[0] ),
1351
+ this.xToTime( this.boxSelectionEnd[0] ),
1352
+ this.pixelsToSeconds * 5);
1353
+
1354
+ if(keyFrameIndices) {
1355
+ for(let index of keyFrameIndices)
1356
+ this.processCurrentKeyFrame( e, index, t, null, true );
1357
+ }
1358
+ }
1359
+ }
1360
+
1361
+ }else {
1362
+ let boundingBox = this.canvas.getBoundingClientRect()
1363
+ if(e.y < boundingBox.top || e.y > boundingBox.bottom)
1364
+ return;
1365
+ // Check exact track keyframe
1366
+ if(!discard && track) {
1367
+ this.processCurrentKeyFrame( e, null, track, localX );
1368
+
1369
+ }
1370
+ else {
1371
+ this.unSelectAllKeyFrames();
1372
+ let x = e.offsetX;
1373
+ let y = e.offsetY - this.topMargin;
1374
+ for( const b of this.buttonsDrawn ) {
1375
+ b.pressed = false;
1376
+ const bActive = x >= b[2] && x <= (b[2] + b[4]) && y >= b[3] && y <= (b[3] + b[5]);
1377
+ if(bActive) {
1378
+ const callback = b[6];
1379
+ if(callback) callback(e);
1380
+ else this[ b[1] ] = !this[ b[1] ];
1381
+ break;
1382
+ }
1383
+ }
1384
+ }
1385
+ }
1386
+
1387
+ this.canvas.classList.remove('grabbing');
1388
+ this.boxSelection = false;
1389
+ this.boxSelectionStart = null;
1390
+ this.boxSelectionEnd = null;
1391
+ }
1392
+
1393
+ onMouseDown( e, time ) {
1394
+ let localX = e.localX;
1395
+ let localY = e.localY;
1396
+ let track = e.track;
1397
+
1398
+ if(e.shiftKey) {
1399
+
1400
+ this.boxSelection = true;
1401
+ this.boxSelectionStart = [localX, localY - this.topMargin];
1402
+ e.multipleSelection = true;
1403
+ }
1404
+ else if(track && !track.locked) {
1405
+ const keyFrameIndex = this.getCurrentKeyFrame( track, this.xToTime( localX ), this.pixelsToSeconds * 5 );
1406
+ if( keyFrameIndex != undefined ) {
1407
+ this.processCurrentKeyFrame( e, keyFrameIndex, track, null, e.multipleSelection ); // Settings this as multiple so time is not being set
1408
+ if(e.ctrlKey ) {
1409
+
1410
+ this.movingKeys = true;
1411
+ this.canvas.style.cursor = "grab";
1412
+ this.canvas.classList.add('grabbing');
1413
+ }
1414
+ // Set pre-move state
1415
+ for(let selectedKey of this.lastKeyFramesSelected) {
1416
+ let [name, idx, keyIndex] = selectedKey;
1417
+ let trackInfo = this.tracksPerItem[name][idx];
1418
+ selectedKey[3] = this.animationClip.tracks[ trackInfo.clipIdx ].times[ keyIndex ];
1419
+ }
1420
+
1421
+ this.timeBeforeMove = track.times[ keyFrameIndex ];
1422
+
1423
+
1424
+ }
1425
+ } else if(!track) {
1426
+ let x = e.offsetX;
1427
+ let y = e.offsetY - this.topMargin;
1428
+ for( const b of this.buttonsDrawn ) {
1429
+ const bActive = x >= b[2] && x <= (b[2] + b[4]) && y >= b[3] && y <= (b[3] + b[5]);
1430
+ b.pressed = bActive;
1431
+ }
1432
+ }
1433
+ }
1434
+
1435
+ onMouseMove( e, time ) {
1436
+
1437
+ let localX = e.localX;
1438
+ let localY = e.localY;
1439
+ let track = e.track;
1440
+
1441
+ const innerSetTime = (t) => {
1442
+ LX.emit( "@on_current_time_" + this.constructor.name, t);
1443
+ if( this.onSetTime )
1444
+ this.onSetTime( t );
1445
+ }
1446
+ // Manage keyframe movement
1447
+ if(this.movingKeys) {
1448
+
1449
+ this.clearState();
1450
+ const newTime = this.xToTime( localX );
1451
+
1452
+ for(let [name, idx, keyIndex, keyTime] of this.lastKeyFramesSelected) {
1453
+ track = this.tracksPerItem[name][idx];
1454
+ if(track && track.locked)
1455
+ return;
1456
+
1457
+ this.canvas.style.cursor = "grabbing";
1458
+
1459
+ const delta = this.timeBeforeMove - keyTime;
1460
+ this.animationClip.tracks[ track.clipIdx ].times[ keyIndex ] = Math.min( this.animationClip.duration, Math.max(0, newTime - delta) );
1461
+ }
1462
+
1463
+ return;
1464
+ }
1465
+
1466
+ const removeHover = () => {
1467
+ if(this.lastHovered)
1468
+ this.tracksPerItem[ this.lastHovered[0] ][ this.lastHovered[1] ].hovered[ this.lastHovered[2] ] = undefined;
1469
+ };
1470
+
1471
+ if( this.grabbing && e.button != 2) {
1472
+
1473
+ // fix this
1474
+ if(e.shiftKey && track) {
1475
+
1476
+ let keyFrameIndex = this.getNearestKeyFrame( track, this.currentTime);
1477
+
1478
+ if(keyFrameIndex != this.snappedKeyFrameIndex){
1479
+ this.snappedKeyFrameIndex = keyFrameIndex;
1480
+ this.currentTime = track.times[ keyFrameIndex ];
1481
+ innerSetTime( this.currentTime );
1482
+ }
1483
+ }
1484
+ else{
1485
+ innerSetTime( this.currentTime );
1486
+ }
1487
+
1488
+ }
1489
+ else if(track) {
1490
+
1491
+ let keyFrameIndex = this.getCurrentKeyFrame( track, this.xToTime( localX ), this.pixelsToSeconds * 5 );
1492
+ if(keyFrameIndex != undefined) {
1493
+
1494
+ const name = this.tracksDictionary[track.fullname];
1495
+ let t = this.tracksPerItem[ name ][track.idx];
1496
+ if(t && t.locked)
1497
+ return;
1498
+ removeHover();
1499
+
1500
+ this.lastHovered = [name, track.idx, keyFrameIndex];
1501
+ t.hovered[keyFrameIndex] = true;
1502
+
1503
+ }else {
1504
+ removeHover();
1505
+ }
1506
+ }
1507
+ else {
1508
+ removeHover();
1509
+ }
1510
+ }
1511
+
1512
+ showContextMenu( e ) {
1513
+
1514
+ e.preventDefault();
1515
+ e.stopPropagation();
1516
+
1517
+ let actions = [];
1518
+ //let track = this.NMFtimeline.clip.tracks[0];
1519
+ if(this.lastKeyFramesSelected && this.lastKeyFramesSelected.length) {
1520
+ if(this.lastKeyFramesSelected.length == 1 && this.clipboard && this.clipboard.value)
1521
+ {
1522
+ actions.push(
1523
+ {
1524
+ title: "Paste",// + " <i class='bi bi-clipboard-fill float-right'></i>",
1525
+ callback: () => {
1526
+ this.pasteContent();
1527
+ }
1528
+ }
1529
+ )
1530
+ }
1531
+ actions.push(
1532
+ {
1533
+ title: "Copy",// + " <i class='bi bi-clipboard-fill float-right'></i>",
1534
+ callback: () => {
1535
+ this.copyContent();
1536
+
1537
+ }
1538
+ }
1539
+ )
1540
+ actions.push(
1541
+ {
1542
+ title: "Delete",// + " <i class='bi bi-trash float-right'></i>",
1543
+ callback: () => {
1544
+ this.deleteKeyFrame({});
1545
+ }
1546
+ }
1547
+ )
1548
+ }
1549
+ else{
1550
+
1551
+ actions.push(
1552
+ {
1553
+ title: "Add",
1554
+ callback: () => this.addKeyFrame( e.track )
1555
+ }
1556
+ )
1557
+
1558
+ if(this.clipboard && this.clipboard.keyframes)
1559
+ {
1560
+ actions.push(
1561
+ {
1562
+ title: "Paste",// + " <i class='bi bi-clipboard-fill float-right'></i>",
1563
+ callback: () => {
1564
+ this.pasteContent();
1565
+
1566
+ }
1567
+ }
1568
+ )
1569
+ }
1570
+ }
1571
+
1572
+ LX.addContextMenu("Options", e, (m) => {
1573
+ for(let i = 0; i < actions.length; i++) {
1574
+ m.add(actions[i].title, actions[i].callback )
1575
+ }
1576
+ });
1577
+
1578
+ }
1579
+
1580
+ onDrawContent( ctx, timeStart, timeEnd ) {
1581
+
1582
+ if(this.selectedItems == null || !this.tracksPerItem)
1583
+ return;
1584
+
1585
+ ctx.save();
1586
+ this.scrollableHeight = this.topMargin;
1587
+
1588
+ let offset = this.trackHeight;
1589
+ for(let t = 0; t < this.selectedItems.length; t++) {
1590
+ let tracks = this.tracksPerItem[this.selectedItems[t]] ? this.tracksPerItem[this.selectedItems[t]] : [{name: this.selectedItems[t]}];
1591
+ if(!tracks) continue;
1592
+
1593
+ const height = this.trackHeight;
1594
+ this.scrollableHeight += (tracks.length+1)*height;
1595
+ let scroll_y = - this.currentScrollInPixels;
1596
+
1597
+ let offsetI = 0;
1598
+ for(let i = 0; i < tracks.length; i++) {
1599
+ let track = tracks[i];
1600
+ if(track.hide) {
1601
+ continue;
1602
+ }
1603
+ this.drawTrackWithKeyframes(ctx, offsetI * height + offset + scroll_y, height, track.name + " (" + track.type + ")", this.animationClip.tracks[track.clipIdx], track);
1604
+ offsetI++;
1605
+ }
1606
+ offset += offsetI * height + height;
1607
+ }
1608
+
1609
+ ctx.restore();
1610
+ };
1611
+
1612
+ onUpdateTracks ( keyType ) {
1613
+
1614
+ if(this.selectedItems == null || this.lastKeyFramesSelected.length || !this.autoKeyEnabled)
1615
+ return;
1616
+
1617
+ for(let i = 0; i < this.selectedItems.length; i++) {
1618
+ let tracks = this.tracksPerItem[this.selectedItems[i]];
1619
+ if(!tracks) continue;
1620
+
1621
+ // Get current track
1622
+ const selectedTrackIdx = tracks.findIndex( t => t.type === keyType );
1623
+ if(selectedTrackIdx < 0)
1624
+ return;
1625
+ let track = tracks[ selectedTrackIdx ];
1626
+
1627
+ // Add new keyframe
1628
+ const newIdx = this.addKeyFrame( track );
1629
+ if(newIdx === null)
1630
+ continue;
1631
+
1632
+ // Select it
1633
+ this.lastKeyFramesSelected.push( [track.name, track.idx, newIdx] );
1634
+ track.selected[newIdx] = true;
1635
+
1636
+ }
1637
+ LX.emit( "@on_current_time_" + this.constructor.name, this.currentTime );
1638
+ // Update time
1639
+ if(this.onSetTime)
1640
+ this.onSetTime(this.currentTime);
1641
+
1642
+ return true; // Handled
1643
+ }
1644
+
1645
+ // Creates a map for each item -> tracks
1646
+ processTracks(animation) {
1647
+
1648
+ this.tracksPerItem = {};
1649
+ this.tracksDictionary = {};
1650
+ this.animationClip = {
1651
+ name: animation.name,
1652
+ duration: animation.duration,
1653
+ speed: animation.speed ?? 1,
1654
+ tracks: []
1655
+ };
1656
+
1657
+ for( let i = 0; i < animation.tracks.length; ++i ) {
1658
+
1659
+ let track = animation.tracks[i];
1660
+
1661
+ const [name, type] = this.getTrackName(track.name);
1662
+
1663
+ let trackInfo = {
1664
+ fullname: track.name,
1665
+ name: name, type: type,
1666
+ dim: track.values.length/track.times.length,
1667
+ selected: [], edited: [], hovered: [], active: true,
1668
+ times: track.times,
1669
+ values: track.values
1670
+ };
1671
+
1672
+ if(!this.tracksPerItem[name]) {
1673
+ this.tracksPerItem[name] = [trackInfo];
1674
+ }else {
1675
+ this.tracksPerItem[name].push( trackInfo );
1676
+ }
1677
+
1678
+
1679
+ const trackIndex = this.tracksPerItem[name].length - 1;
1680
+ this.tracksPerItem[name][trackIndex].idx = trackIndex;
1681
+ this.tracksPerItem[name][trackIndex].clipIdx = i;
1682
+
1683
+ // Save index also in original track
1684
+ track.idx = trackIndex;
1685
+ this.tracksDictionary[track.name] = name;
1686
+
1687
+ this.animationClip.tracks.push(trackInfo);
1688
+ }
1689
+ this.resize();
1690
+ }
1691
+
1692
+
1693
+ optimizeTrack(trackIdx) {
1694
+ const track = this.animationClip.tracks[trackIdx];
1695
+ if(track.optimize) {
1696
+
1697
+ track.optimize( this.optimizeThreshold );
1698
+ if(this.onOptimizeTracks)
1699
+ this.onOptimizeTracks(trackIdx);
1700
+ }
1701
+ }
1702
+
1703
+ optimizeTracks() {
1704
+
1705
+ if(!this.animationClip)
1706
+ return;
1707
+
1708
+ for( let i = 0; i < this.animationClip.tracks.length; ++i ) {
1709
+ const track = this.animationClip.tracks[i];
1710
+ if(track.optimize) {
1711
+
1712
+ track.optimize( this.optimizeThreshold );
1713
+ if(this.onOptimizeTracks)
1714
+ this.onOptimizeTracks(i);
1715
+ }
1716
+ }
1717
+ }
1718
+
1719
+
1720
+ getNumTracks( item ) {
1721
+ if(!item || !this.tracksPerItem)
1722
+ return;
1723
+ const tracks = this.tracksPerItem[item.name];
1724
+ return tracks ? tracks.length : null;
1725
+ }
1726
+
1727
+
1728
+ onShowOptimizeMenu( e ) {
1729
+
1730
+ if(this.selectedItems == null)
1731
+ return;
1732
+
1733
+ let tracks = [];
1734
+ for(let i = 0; i < this.selectedItems.length; i++) {
1735
+
1736
+ tracks = [...tracks, ...this.tracksPerItem[this.selectedItems[i]]];
1737
+ if(!tracks) continue;
1738
+
1739
+ }
1740
+ if(!tracks.length) return;
1741
+
1742
+ const threshold = this.onGetOptimizeThreshold ? this.onGetOptimizeThreshold() : 0.025;
1743
+ LX.addContextMenu("Optimize", e, m => {
1744
+ for( let t of tracks ) {
1745
+ m.add( t.name + (t.type ? "@" + t.type : ""), () => {
1746
+ if(this.optimizeTrack) {
1747
+ this.optimizeTrack(t.clipIdx, threshold)
1748
+ // this.animationClip.tracks[t.clipIdx].optimize( threshold );
1749
+ t.edited = [];
1750
+ }
1751
+ })
1752
+ }
1753
+ });
1754
+ }
1755
+
1756
+ onPreProcessTrack( track ) {
1757
+ const name = this.tracksDictionary[track.fullname];
1758
+ let trackInfo = this.tracksPerItem[name][track.idx];
1759
+ trackInfo.selected = [];
1760
+ trackInfo.edited = [];
1761
+ trackInfo.hovered = [];
1762
+ }
1763
+
1764
+ isKeyFrameSelected( track, index ) {
1765
+ return track.selected[ index ];
1766
+ }
1767
+
1768
+ saveState( clipIdx ) {
1769
+
1770
+ const localIdx = this.animationClip.tracks[clipIdx].idx;
1771
+ const name = this.getTrackName(this.animationClip.tracks[clipIdx].name)[0];
1772
+ const trackInfo = this.tracksPerItem[name][localIdx];
1773
+
1774
+ this.trackState.push({
1775
+ idx: clipIdx,
1776
+ t: this.animationClip.tracks[clipIdx].times.slice(),
1777
+ v: this.animationClip.tracks[clipIdx].values.slice(),
1778
+ editedTracks: [].concat(trackInfo.edited)
1779
+ });
1780
+ }
1781
+
1782
+ undo() {
1783
+
1784
+ if(!this.trackState.length)
1785
+ return;
1786
+
1787
+ const state = this.trackState.pop();
1788
+ this.animationClip.tracks[state.idx].times = state.t;
1789
+ this.animationClip.tracks[state.idx].values = state.v;
1790
+
1791
+ const localIdx = this.animationClip.tracks[state.idx].idx;
1792
+ const name = this.getTrackName(this.animationClip.tracks[state.idx].name)[0];
1793
+ this.tracksPerItem[name][localIdx].edited = state.editedTracks;
1794
+
1795
+ // Update animation action interpolation info
1796
+ if(this.onUpdateTrack)
1797
+ this.onUpdateTrack( state.idx );
1798
+ }
1799
+
1800
+ selectKeyFrame( track, selectionInfo, index ) {
1801
+
1802
+ if(index == undefined || !track)
1803
+ return;
1804
+
1805
+ this.unSelectAllKeyFrames();
1806
+
1807
+ this.lastKeyFramesSelected.push( selectionInfo );
1808
+ if(track.locked)
1809
+ return;
1810
+ track.selected[index] = true;
1811
+ this.currentTime = this.animationClip.tracks[track.clipIdx].times[ index ];
1812
+ LX.emit( "@on_current_time_" + this.constructor.name, this.currentTime );
1813
+ if( this.onSetTime )
1814
+ this.onSetTime( this.currentTime);
1815
+ }
1816
+
1817
+ copyContent() {
1818
+ let toCopy = {};
1819
+ for(let i = 0; i < this.lastKeyFramesSelected.length; i++){
1820
+ let [id, trackIdx, keyIdx] = this.lastKeyFramesSelected[i];
1821
+ if(toCopy[this.tracksPerItem[id][trackIdx].clipIdx]) {
1822
+ toCopy[this.tracksPerItem[id][trackIdx].clipIdx].idxs.push(keyIdx);
1823
+ } else {
1824
+ toCopy[this.tracksPerItem[id][trackIdx].clipIdx] = {idxs : [keyIdx]};
1825
+ toCopy[this.tracksPerItem[id][trackIdx].clipIdx].track = this.tracksPerItem[id][trackIdx]
1826
+ }
1827
+ if(i == 0) {
1828
+ this.copyKeyFrameValue(this.tracksPerItem[id][trackIdx], keyIdx)
1829
+ }
1830
+ }
1831
+ for(let clipIdx in toCopy) {
1832
+
1833
+ this.copyKeyFrames(toCopy[clipIdx].track, toCopy[clipIdx].idxs);
1834
+ }
1835
+ }
1836
+
1837
+ copyKeyFrameValue( track, index ) {
1838
+
1839
+ // 1 element clipboard by now
1840
+
1841
+ let values = [];
1842
+ let start = index * track.dim;
1843
+ for(let i = start; i < start + track.dim; ++i)
1844
+ values.push( this.animationClip.tracks[ track.clipIdx ].values[i] );
1845
+
1846
+ if(!this.clipboard)
1847
+ this.clipboard = {};
1848
+
1849
+ this.clipboard.value = {
1850
+ type: track.type,
1851
+ values: values
1852
+ };
1853
+ }
1854
+
1855
+ copyKeyFrames( track, indices ) {
1856
+
1857
+ let clipIdx = track.clipIdx;
1858
+ if(!this.clipboard)
1859
+ this.clipboard = {};
1860
+ if(!this.clipboard.keyframes) {
1861
+ this.clipboard.keyframes = {};
1862
+ }
1863
+
1864
+ this.clipboard.keyframes[clipIdx] = { track: track, values:{} };
1865
+ // 1 element clipboard by now
1866
+ for(let idx = 0; idx < indices.length; idx++ ){
1867
+ let keyIdx = indices[idx] ;
1868
+ let values = [];
1869
+ let start = keyIdx * track.dim;
1870
+ for(let i = start; i < start + track.dim; ++i)
1871
+ values.push( this.animationClip.tracks[ clipIdx ].values[i] );
1872
+
1873
+ this.clipboard.keyframes[clipIdx].values[indices[idx]] = values;
1874
+ };
1875
+ }
1876
+
1877
+ pasteContent() {
1878
+ if(!this.clipboard)
1879
+ return;
1880
+
1881
+ if(this.clipboard.value && this.lastKeyFramesSelected.length == 1) {
1882
+
1883
+ let [id, trackIdx, keyIdx] = this.lastKeyFramesSelected[0];
1884
+ this.pasteKeyFrameValue({}, this.tracksPerItem[id][trackIdx], keyIdx);
1885
+ }
1886
+ if(this.clipboard.keyframes) {
1887
+ let currentTime = this.currentTime;
1888
+ for(let clipIdx in this.clipboard.keyframes) {
1889
+ let indices = Object.keys( this.clipboard.keyframes[clipIdx].values)
1890
+ this.pasteKeyFrames({multipleSelection: this.clipboard.keyframes.length}, clipIdx, indices);
1891
+ this.currentTime = currentTime;
1892
+ }
1893
+ }
1894
+ }
1895
+
1896
+ canPasteKeyFrame () {
1897
+ return this.clipboard != null;
1898
+ }
1899
+
1900
+ _paste( track, index ) {
1901
+
1902
+ let clipboardInfo = this.clipboard.value;
1903
+
1904
+ if(clipboardInfo.type != track.type){
1905
+ return;
1906
+ }
1907
+
1908
+ let start = index * track.dim;
1909
+ let j = 0;
1910
+ for(let i = start; i < start + track.dim; ++i) {
1911
+ this.animationClip.tracks[ track.clipIdx ].values[i] = clipboardInfo.values[j];
1912
+ ++j;
1913
+ }
1914
+ LX.emit( "@on_current_time_" + this.constructor.name, this.currentTime);
1915
+ if(this.onSetTime)
1916
+ this.onSetTime(this.currentTime);
1917
+
1918
+ track.edited[ index ] = true;
1919
+ }
1920
+
1921
+ pasteKeyFrameValue( e, track, index ) {
1922
+
1923
+ this.saveState(track.clipIdx);
1924
+
1925
+ // Copy to current key
1926
+ this._paste( track, index );
1927
+
1928
+ if(!e.multipleSelection)
1929
+ return;
1930
+
1931
+ // Don't want anything after this
1932
+ this.clearState();
1933
+
1934
+ // Copy to every selected key
1935
+ for(let [name, idx, keyIndex] of this.lastKeyFramesSelected) {
1936
+ this._paste( this.tracksPerItem[name][idx], keyIndex );
1937
+ }
1938
+ }
1939
+
1940
+ pasteKeyFrames( e, clipIdx, indices ) {
1941
+
1942
+ this.saveState(clipIdx);
1943
+
1944
+ // Copy to current key
1945
+ for(let i = 0; i < indices.length; i++) {
1946
+ let value = this.clipboard.keyframes[clipIdx].values[indices[i]];
1947
+ if(typeof value == 'number')
1948
+ value = [value];
1949
+ if(i > 0) {
1950
+ let delta = this.animationClip.tracks[clipIdx].times[indices[i]] - this.animationClip.tracks[clipIdx].times[indices[i-1]];
1951
+ this.currentTime += delta;
1952
+
1953
+ }
1954
+ this.addKeyFrame( this.clipboard.keyframes[clipIdx].track, value);
1955
+ }
1956
+
1957
+ if(!e.multipleSelection)
1958
+ return;
1959
+
1960
+ // Don't want anything after this
1961
+ this.clearState();
1962
+
1963
+ // Copy to every selected key
1964
+ for(let [name, idx, keyIndex] of this.lastKeyFramesSelected) {
1965
+ this._paste( this.tracksPerItem[name][idx], keyIndex );
1966
+ }
1967
+ }
1968
+
1969
+ addKeyFrame( track, value = undefined, time = this.currentTime ) {
1970
+
1971
+ // Update animationClip information
1972
+ let clipIdx = track.clipIdx;
1973
+
1974
+ let [name, keyType] = this.getTrackName(track.name)
1975
+ let tracks = this.tracksPerItem[name];
1976
+ if(!tracks) return;
1977
+
1978
+ // Get current track
1979
+ const selectedTrackIdx = tracks.findIndex( t => t.type === keyType );
1980
+ if(selectedTrackIdx >= 0)
1981
+ track = tracks[ selectedTrackIdx ];
1982
+ if(clipIdx == undefined) {
1983
+ if(selectedTrackIdx < 0)
1984
+ return;
1985
+ clipIdx = tracks[ selectedTrackIdx ].clipIdx;
1986
+ }
1987
+
1988
+ // Time slot with other key?
1989
+ const keyInCurrentSlot = this.animationClip.tracks[clipIdx].times.find( t => { return !LX.UTILS.compareThreshold(time, t, t, 0.001 ); });
1990
+ if( keyInCurrentSlot ) {
1991
+ console.warn("There is already a keyframe stored in time slot ", keyInCurrentSlot)
1992
+ return;
1993
+ }
1994
+
1995
+ this.saveState(clipIdx);
1996
+
1997
+ // Find new index
1998
+ let newIdx = this.animationClip.tracks[clipIdx].times.findIndex( t => t > time );
1999
+
2000
+ // Add as last index
2001
+ let lastIndex = false;
2002
+ if(newIdx < 0) {
2003
+ newIdx = this.animationClip.tracks[clipIdx].times.length;
2004
+ lastIndex = true;
2005
+ }
2006
+
2007
+ // Add time key
2008
+ const timesArray = [];
2009
+ this.animationClip.tracks[clipIdx].times.forEach( (a, b) => {
2010
+ b == newIdx ? timesArray.push(time, a) : timesArray.push(a);
2011
+ } );
2012
+
2013
+ if(lastIndex) {
2014
+ timesArray.push(time);
2015
+ }
2016
+
2017
+ this.animationClip.tracks[clipIdx].times = new Float32Array( timesArray );
2018
+
2019
+ // Add values
2020
+ let valuesArray = [];
2021
+ let dim = value.length;
2022
+ this.animationClip.tracks[clipIdx].values.forEach( (a, b) => {
2023
+ if(b == newIdx * dim) {
2024
+ for( let i = 0; i < dim; ++i )
2025
+ valuesArray.push(value[i]);
2026
+ }
2027
+ valuesArray.push(a);
2028
+ } );
2029
+
2030
+ if(lastIndex) {
2031
+ for( let i = 0; i < dim; ++i )
2032
+ valuesArray.push(value[i]);
2033
+ }
2034
+
2035
+ this.animationClip.tracks[clipIdx].values = new Float32Array( valuesArray );
2036
+
2037
+
2038
+ // Move the other's key properties
2039
+ for(let i = (this.animationClip.tracks[clipIdx].times.length - 1); i > newIdx; --i) {
2040
+ track.edited[i - 1] ? track.edited[i] = track.edited[i - 1] : 0;
2041
+ }
2042
+
2043
+ // Reset this key's properties
2044
+ track.hovered[newIdx] = undefined;
2045
+ track.selected[newIdx] = undefined;
2046
+ track.edited[newIdx] = true;
2047
+
2048
+
2049
+ // Update animation action interpolation info
2050
+ if(this.onUpdateTrack)
2051
+ this.onUpdateTrack( clipIdx );
2052
+
2053
+ LX.emit( "@on_current_time_" + this.constructor.name, time);
2054
+ if(this.onSetTime)
2055
+ this.onSetTime(time);
2056
+ this.draw();
2057
+ return newIdx;
2058
+ }
2059
+
2060
+ deleteContent() {
2061
+
2062
+ this.deleteKeyFrame({ multipleSelection: this.lastKeyFramesSelected.length > 1});
2063
+ }
2064
+
2065
+ /** Delete a keyframe given the track and the its index
2066
+ * @track: track that keyframe belongs to
2067
+ * @index: index of the keyframe on the track
2068
+ */
2069
+ _delete( track, index ) {
2070
+
2071
+ // Don't remove by now the first key
2072
+ if(index == 0) {
2073
+ console.warn("Operation not supported! [remove first keyframe track]");
2074
+ return 0;
2075
+ }
2076
+
2077
+ // Update clip information
2078
+ const clipIdx = track.clipIdx;
2079
+
2080
+ // Don't remove by now the last key
2081
+ // if(index == this.animationClip.tracks[clipIdx].times.length - 1) {
2082
+ // console.warn("Operation not supported! [remove last keyframe track]");
2083
+ // return;
2084
+ // }
2085
+
2086
+ // Reset this key's properties
2087
+ track.hovered[index] = undefined;
2088
+ track.selected[index] = undefined;
2089
+ track.edited[index] = undefined;
2090
+
2091
+ // Delete time key
2092
+ this.animationClip.tracks[clipIdx].times = this.animationClip.tracks[clipIdx].times.filter( (v, i) => i != index);
2093
+
2094
+ // Delete values
2095
+ const indexDim = track.dim * index;
2096
+ const slice1 = this.animationClip.tracks[clipIdx].values.slice(0, indexDim);
2097
+ const slice2 = this.animationClip.tracks[clipIdx].values.slice(indexDim + track.dim);
2098
+
2099
+ this.animationClip.tracks[clipIdx].values = LX.UTILS.concatTypedArray([slice1, slice2], Float32Array);
2100
+
2101
+ // Move the other's key properties
2102
+ for(let i = index; i < this.animationClip.tracks[clipIdx].times.length; ++i) {
2103
+ track.edited[i] = track.edited[i + 1];
2104
+ }
2105
+
2106
+ // Update animation action interpolation info
2107
+ if(this.onDeleteKeyFrame)
2108
+ this.onDeleteKeyFrame( clipIdx, index );
2109
+
2110
+ return 1;
2111
+ }
2112
+
2113
+ /** Delete one or more keyframes given the triggered event
2114
+ * @e: event
2115
+ * @track:
2116
+ * @index: index of the keyframe on the track
2117
+ */
2118
+ deleteKeyFrame(e, track, index) {
2119
+
2120
+ if(e.multipleSelection) {
2121
+
2122
+ // Split in tracks
2123
+ const perTrack = [];
2124
+ this.lastKeyFramesSelected.forEach( e => perTrack[e[1]] ? perTrack[e[1]].push(e) : perTrack[e[1]] = [e] );
2125
+
2126
+ for(let pts of perTrack) {
2127
+
2128
+ if(!pts) continue;
2129
+
2130
+ pts = pts.sort( (a,b) => a[2] - b[2] );
2131
+
2132
+ let deletedIndices = 0;
2133
+
2134
+ // Delete every selected key
2135
+ for(let [name, idx, keyIndex] of pts) {
2136
+ this.saveState(this.tracksPerItem[name][idx].clipIdx);
2137
+ deletedIndices += this._delete( this.tracksPerItem[name][idx], keyIndex - deletedIndices );
2138
+ }
2139
+ }
2140
+ }
2141
+ else{
2142
+
2143
+ // Key pressed
2144
+ if(!track && this.lastKeyFramesSelected.length > 0) {
2145
+ const [itemName, trackIndex, keyIndex] = this.lastKeyFramesSelected[0];
2146
+ track = this.tracksPerItem[itemName][trackIndex];
2147
+ index = keyIndex;
2148
+ }
2149
+
2150
+ if ( track ){
2151
+ this.saveState(track.clipIdx);
2152
+ this._delete( track, index );
2153
+ }
2154
+ }
2155
+
2156
+ this.unSelectAllKeyFrames();
2157
+ }
2158
+
2159
+ getNumKeyFramesSelected() {
2160
+ return this.lastKeyFramesSelected.length;
2161
+ }
2162
+
2163
+
2164
+ unSelect() {
2165
+
2166
+ if(!this.unSelectAllKeyFrames()) {
2167
+ this.selectedItems = null;
2168
+ if(this.onItemUnselected)
2169
+ this.onItemUnselected();
2170
+ }
2171
+ }
2172
+
2173
+ setSelectedItems( itemsName ) {
2174
+
2175
+ if(itemsName.constructor !== Array)
2176
+ throw("Item name has to be an array!");
2177
+
2178
+ this.selectedItems = itemsName;
2179
+ this.unSelectAllKeyFrames();
2180
+ this.updateLeftPanel();
2181
+ this.resize();
2182
+ }
2183
+
2184
+ getTrack( trackInfo ) {
2185
+ const [name, trackIndex] = trackInfo;
2186
+ return this.tracksPerItem[ name ][trackIndex];
2187
+ }
2188
+
2189
+ getTrackName( uglyName ) {
2190
+
2191
+ let name, type;
2192
+
2193
+ // Support other versions
2194
+ if(uglyName.includes("[")) {
2195
+ const nameIndex = uglyName.indexOf('['),
2196
+ trackNameInfo = uglyName.substr(nameIndex+1).split("].");
2197
+ name = trackNameInfo[0];
2198
+ type = trackNameInfo[1];
2199
+ }else {
2200
+ const trackNameInfo = uglyName.split(".");
2201
+ name = trackNameInfo[0];
2202
+ type = trackNameInfo[1];
2203
+ }
2204
+
2205
+ return [name, type];
2206
+ }
2207
+
2208
+ getCurrentKeyFrame( track, time, threshold ) {
2209
+
2210
+ if(!track || !track.times.length)
2211
+ return;
2212
+
2213
+ // Avoid iterating through all timestamps
2214
+ if((time + threshold) < track.times[0])
2215
+ return;
2216
+
2217
+ for(let i = 0; i < track.times.length; ++i) {
2218
+ let t = track.times[i];
2219
+ if(t >= (time - threshold) &&
2220
+ t <= (time + threshold)) {
2221
+ return i;
2222
+ }
2223
+ }
2224
+
2225
+ return;
2226
+ }
2227
+
2228
+ getKeyFramesInRange( track, minTime, maxTime, threshold ) {
2229
+
2230
+ if(!track || !track.times.length)
2231
+ return;
2232
+
2233
+ // Manage negative selection
2234
+ if(minTime > maxTime) {
2235
+ let aux = minTime;
2236
+ minTime = maxTime;
2237
+ maxTime = aux;
2238
+ }
2239
+
2240
+ // Avoid iterating through all timestamps
2241
+ if((maxTime + threshold) < track.times[0])
2242
+ return;
2243
+
2244
+ let indices = [];
2245
+
2246
+ for(let i = 0; i < track.times.length; ++i) {
2247
+ let t = track.times[i];
2248
+ if(t >= (minTime - threshold) &&
2249
+ t <= (maxTime + threshold)) {
2250
+ indices.push(i);
2251
+ }
2252
+ }
2253
+
2254
+ return indices;
2255
+ }
2256
+
2257
+ getNearestKeyFrame( track, time ) {
2258
+
2259
+ if(!track || !track.times.length)
2260
+ return;
2261
+
2262
+ return track.times.reduce((a, b) => {
2263
+ return Math.abs(b - time) < Math.abs(a - time) ? b : a;
2264
+ });
2265
+ }
2266
+
2267
+ unSelectAllKeyFrames() {
2268
+
2269
+ for(let [name, idx, keyIndex] of this.lastKeyFramesSelected) {
2270
+ this.tracksPerItem[name][idx].selected[keyIndex] = false;
2271
+ }
2272
+
2273
+ // Something has been unselected
2274
+ const unselected = this.lastKeyFramesSelected.length > 0;
2275
+ this.lastKeyFramesSelected.length = 0;
2276
+ return unselected;
2277
+ }
2278
+
2279
+ processCurrentKeyFrame( e, keyFrameIndex, track, localX, multiple ) {
2280
+
2281
+ if(track.locked)
2282
+ return;
2283
+
2284
+ e.multipleSelection = multiple;
2285
+ keyFrameIndex = keyFrameIndex ?? this.getCurrentKeyFrame( track, this.xToTime( localX ), this.pixelsToSeconds * 5 );
2286
+
2287
+ if(!multiple && e.button != 2) {
2288
+ this.unSelectAllKeyFrames();
2289
+ }
2290
+
2291
+ const name = this.tracksDictionary[track.fullname];
2292
+ let t = this.tracksPerItem[ name ][track.idx];
2293
+ let currentSelection = [name, track.idx, keyFrameIndex];
2294
+ if(this.lastKeyFramesSelected.indexOf(currentSelection) > -1)
2295
+ return;
2296
+
2297
+ if(!multiple)
2298
+ this.selectKeyFrame(t, currentSelection, keyFrameIndex);
2299
+ else
2300
+ this.lastKeyFramesSelected.push( currentSelection );
2301
+ if( this.onSelectKeyFrame && this.onSelectKeyFrame(e, currentSelection, keyFrameIndex)) {
2302
+ // Event handled
2303
+ return;
2304
+ }
2305
+
2306
+ if(keyFrameIndex == undefined)
2307
+ return;
2308
+
2309
+ // Select if not handled
2310
+
2311
+ t.selected[keyFrameIndex] = true;
2312
+
2313
+ if( !multiple ) {
2314
+ LX.emit( "@on_current_time_" + this.constructor.name, track.times[ keyFrameIndex ]);
2315
+
2316
+ if(this.onSetTime )
2317
+ this.onSetTime( track.times[ keyFrameIndex ] );
2318
+ }
2319
+ }
2320
+
2321
+ /**
2322
+ * @method addNewTrack
2323
+ */
2324
+
2325
+ addNewTrack() {
2326
+
2327
+ if(!this.animationClip)
2328
+ this.animationClip = {tracks:[]};
2329
+
2330
+ let trackInfo = {
2331
+ idx: this.animationClip.tracks.length,
2332
+ values: [], times: [],
2333
+ selected: [], edited: [], hovered: []
2334
+ };
2335
+
2336
+ this.animationClip.tracks.push(trackInfo);
2337
+ return trackInfo.idx;
2338
+ }
2339
+
2340
+ /**
2341
+ * @method clearTrack
2342
+ */
2343
+
2344
+ clearTrack(idx, defaultValue) {
2345
+
2346
+ let track = this.animationClip.tracks[idx];
2347
+
2348
+ if(track.locked )
2349
+ {
2350
+ return;
2351
+ }
2352
+
2353
+ const count = track.times.length;
2354
+ for(let i = count - 1; i >= 0; i--)
2355
+ {
2356
+ this.saveState(track.clipIdx);
2357
+ this._delete(track, i );
2358
+ }
2359
+ if(defaultValue != undefined) {
2360
+ if(typeof(defaultValue) == 'number') {
2361
+ track.values[0] = defaultValue;
2362
+ }
2363
+ else {
2364
+ for(let i = 0; i < defaultValue.length; i++) {
2365
+ track.values[i] = defaultValue[i];
2366
+ }
2367
+ }
2368
+
2369
+ }
2370
+ return idx;
2371
+ }
2372
+ }
2373
+
2374
+ LX.KeyFramesTimeline = KeyFramesTimeline;
2375
+
2376
+ /**
2377
+ * @class ClipsTimeline
2378
+ */
2379
+
2380
+ class ClipsTimeline extends Timeline {
2381
+
2382
+ /**
2383
+ * @param {string} name
2384
+ * @param {object} options = {animationClip, selectedItems, x, y, width, height, canvas, trackHeight}
2385
+ */
2386
+ constructor(name, options = {}) {
2387
+
2388
+ super(name, options);
2389
+
2390
+ this.addNewTrack();
2391
+ this.selectedClip = null;
2392
+ this.lastClipsSelected = [];
2393
+ }
2394
+
2395
+ resizeCanvas( size ) {
2396
+ if( size[0] <= 0 && size[1] <=0 )
2397
+ return;
2398
+
2399
+ size[1] -= this.header_offset;
2400
+ this.canvasArea.setSize(size);
2401
+ this.canvas.width = size[0];
2402
+ this.canvas.height = size[1];
2403
+ var w = Math.max(300, this.canvas.width);
2404
+ this.secondsToPixels = ( w - this.session.left_margin ) / this.duration;
2405
+ this.pixelsToSeconds = 1 / this.secondsToPixels;
2406
+
2407
+ let timeline_height = this.topMargin;
2408
+ let line_height = this.trackHeight;
2409
+ let max_tracks = Math.ceil( (size[1] - timeline_height) / line_height );
2410
+ while(this.animationClip.tracks.length < max_tracks - 1) {
2411
+ this.addNewTrack();
2412
+ }
2413
+
2414
+ this.draw(this.currentTime);
2415
+
2416
+ }
2417
+
2418
+ onMouseUp( e ) {
2419
+
2420
+ let track = e.track;
2421
+ let localX = e.localX;
2422
+
2423
+ let discard = e.discard;
2424
+
2425
+ if(e.shiftKey) {
2426
+
2427
+ // Multiple selection
2428
+ if(!discard && track) {
2429
+ this.processCurrentClip( e, null, track, localX, true );
2430
+ }
2431
+ // Box selection
2432
+ else if (this.boxSelection){
2433
+
2434
+ let tracks = this.getTracksInRange(this.boxSelectionStart[1], this.boxSelectionEnd[1], this.pixelsToSeconds * 5);
2435
+
2436
+ for(let t of tracks) {
2437
+ let clipsIndices = this.getClipsInRange(t,
2438
+ this.xToTime( this.boxSelectionStart[0] ),
2439
+ this.xToTime( this.boxSelectionEnd[0] ),
2440
+ this.pixelsToSeconds * 5);
2441
+
2442
+ if(clipsIndices) {
2443
+ for(let index of clipsIndices)
2444
+ this.processCurrentClip( e, index, t, null, true );
2445
+ }
2446
+ }
2447
+ }
2448
+
2449
+ }
2450
+ else {
2451
+
2452
+ let boundingBox = this.canvas.getBoundingClientRect()
2453
+ if(e.y < boundingBox.top || e.y > boundingBox.bottom)
2454
+ return;
2455
+
2456
+ // Check exact track clip
2457
+ if(!discard && track) {
2458
+ if(e.button!=2){
2459
+ this.processCurrentClip( e, null, track, localX );
2460
+ }
2461
+ }
2462
+
2463
+ }
2464
+ this.movingKeys = false;
2465
+ this.boxSelection = false;
2466
+ this.boxSelectionStart = null;
2467
+ this.boxSelectionEnd = null;
2468
+
2469
+ }
2470
+
2471
+ onMouseDown( e, time ) {
2472
+
2473
+ let localX = e.localX;
2474
+ let localY = e.localY;
2475
+ let track = e.track;
2476
+
2477
+ if(e.shiftKey) {
2478
+
2479
+ this.boxSelection = true;
2480
+ this.boxSelectionStart = [localX, localY - this.topMargin];
2481
+
2482
+ }
2483
+ else if(e.ctrlKey && track) {
2484
+
2485
+ let x = e.offsetX;
2486
+ let selectedClips = [];
2487
+ if(this.lastClipsSelected.length){
2488
+ selectedClips = this.lastClipsSelected;
2489
+ }
2490
+ else{
2491
+ let clipIndex = this.getCurrentClip( track, this.xToTime( localX ), this.pixelsToSeconds * 5 );
2492
+ if(clipIndex != undefined)
2493
+ {
2494
+ this.lastClipsSelected = selectedClips = [[track.idx, clipIndex]];
2495
+ }
2496
+
2497
+ }
2498
+ this.canvas.style.cursor = "grab";
2499
+ for(let i = 0; i< selectedClips.length; i++)
2500
+ {
2501
+ this.movingKeys = false
2502
+ let [trackIndex, clipIndex] = selectedClips[i];
2503
+ var clip = this.animationClip.tracks[trackIndex].clips[clipIndex];
2504
+
2505
+ if(!this.timelineClickedClips)
2506
+ this.timelineClickedClips = [];
2507
+ if(this.timelineClickedClips.indexOf(clip) < 0) {
2508
+ this.timelineClickedClips.push(clip);
2509
+
2510
+ if(!this.timelineClickedClipsTime)
2511
+ this.timelineClickedClipsTime = [];
2512
+ this.timelineClickedClipsTime.push(this.xToTime( localX ));
2513
+ }
2514
+
2515
+
2516
+ var endingX = this.timeToX( clip.start + clip.duration );
2517
+ var distToStart = Math.abs( this.timeToX( clip.start ) - x );
2518
+ var distToEnd = Math.abs( this.timeToX( clip.start + clip.duration ) - e.offsetX );
2519
+
2520
+ if(this.duration < clip.start + clip.duration ){
2521
+ this.setDuration(clip.start + clip.duration);
2522
+ }
2523
+ //this.addUndoStep( "clip_modified", clip );
2524
+ if( (e.ctrlKey && distToStart < 5) || (clip.fadein && Math.abs( this.timeToX( clip.start + clip.fadein ) - e.offsetX ) < 5) )
2525
+ this.dragClipMode = "fadein";
2526
+ else if( (e.ctrlKey && distToEnd < 5) || (clip.fadeout && Math.abs( this.timeToX( clip.start + clip.duration - clip.fadeout ) - e.offsetX ) < 5) )
2527
+ this.dragClipMode = "fadeout";
2528
+ else if( Math.abs( endingX - x ) < 10 )
2529
+ this.dragClipMode = "duration";
2530
+ else
2531
+ this.dragClipMode = "move";
2532
+ }
2533
+
2534
+ }
2535
+ else if(!track) {
2536
+
2537
+ if( this.timelineClickedClips )
2538
+ {
2539
+ for(let i = 0; i < this.timelineClickedClips.length; i++){
2540
+
2541
+ if( this.timelineClickedClips[i].fadein && this.timelineClickedClips[i].fadein < 0 )
2542
+ this.timelineClickedClips[i].fadein = 0;
2543
+ if( this.timelineClickedClips[i].fadeout && this.timelineClickedClips[i].fadeout < 0 )
2544
+ this.timelineClickedClips[i].fadeout = 0;
2545
+ }
2546
+ }
2547
+ this.timelineClickedClips = null;
2548
+ this.selectedClip = null;
2549
+ this.unSelectAllClips();
2550
+ if(this.onSelectClip)
2551
+ this.onSelectClip(null);
2552
+ }
2553
+ }
2554
+
2555
+ onMouseMove( e, time ) {
2556
+
2557
+ const innerSetTime = (t) => {
2558
+ LX.emit( "@on_current_time_" + this.constructor.name, t);
2559
+ if( this.onSetTime )
2560
+ this.onSetTime( t );
2561
+ }
2562
+
2563
+ if(e.shiftKey) {
2564
+ if(this.boxSelection) {
2565
+ this.boxSelectionEnd = [localX,localY - this.topMargin];
2566
+ return; // Handled
2567
+ }
2568
+ }
2569
+
2570
+ if(this.grabbing && e.button != 2) {
2571
+
2572
+ var curr = time - this.currentTime;
2573
+ var delta = curr - this.grabTime;
2574
+ this.grabTime = curr;
2575
+
2576
+ var ct = Math.max(0,this.currentTime - delta);
2577
+ if( this.timelineClickedClips != undefined) {
2578
+ this.movingKeys = true;
2579
+ for(let i = 0; i < this.timelineClickedClips.length; i++){
2580
+
2581
+ let trackIdx = this.lastClipsSelected[i][0];
2582
+ let clipIdx = this.lastClipsSelected[i][1];
2583
+ var clip = this.timelineClickedClips[i] ;
2584
+ var diff = clip.start + delta < 0 ? - clip.start : delta;//this.currentTime - this.timelineClickedClipsTime[i];//delta;
2585
+ if( this.dragClipMode == "move" ) {
2586
+ let clipsInRange = this.getClipsInRange(this.animationClip.tracks[trackIdx], clip.start+diff, clip.start + clip.duration + diff, 0.01)
2587
+ if(clipsInRange && clipsInRange[0] != clipIdx)
2588
+ return;
2589
+ clip.start += diff;
2590
+ if(clip.fadein != undefined)
2591
+ clip.fadein += diff;
2592
+ if(clip.fadeout != undefined)
2593
+ clip.fadeout += diff;
2594
+ this.canvas.style.cursor = "grabbing";
2595
+
2596
+ if( this.timelineClickedClips.length == 1 && e.track && e.movementY != 0) {
2597
+
2598
+ //let tracks = this.getTracksInRange(e.localY, e.localY + this.trackHeight, this.pixelsToSeconds*5);
2599
+ // for(let i = 0; i < tracks.length; i++) {
2600
+ let clips = this.getClipsInRange(e.track, clip.start, clip.start + clip.duration, 0.01)
2601
+ if(clips == undefined || !clips.length) {
2602
+ // let newClip = Object.assign({}, clip);
2603
+ let clipIndex = this.addClipInTrack(clip, e.track.idx);
2604
+ this.deleteClip(clip);
2605
+ e.track.selected[clipIndex] = true;
2606
+ this.lastClipsSelected = [[e.track.idx, clipIndex]];
2607
+ this.timelineClickedClips = [clip];
2608
+ return true;
2609
+ }
2610
+ // }
2611
+ }
2612
+ }
2613
+ else if( this.dragClipMode == "fadein" )
2614
+ clip.fadein = Math.min(Math.max((clip.fadein || 0) + diff, clip.start), clip.start+clip.duration);
2615
+ else if( this.dragClipMode == "fadeout" )
2616
+ clip.fadeout = Math.max(Math.min((clip.fadeout || clip.start+clip.duration) + diff, clip.start+clip.duration), clip.start);
2617
+ else if( this.dragClipMode == "duration" )
2618
+ clip.duration += diff;
2619
+
2620
+ if(this.duration < clip.start + clip.duration )
2621
+ {
2622
+ this.setDuration(clip.start + clip.duration);
2623
+ }
2624
+ }
2625
+ return true;
2626
+ }
2627
+ else{
2628
+ innerSetTime( this.currentTime );
2629
+ }
2630
+ }
2631
+ }
2632
+
2633
+ onDblClick( e ) {
2634
+
2635
+ let track = e.track;
2636
+ let localX = e.localX;
2637
+
2638
+ let clipIndex = this.getCurrentClip( track, this.xToTime( localX ), this.pixelsToSeconds * 5 );
2639
+ if(clipIndex != undefined) {
2640
+ this.lastClipsSelected = [[track.idx, clipIndex]];
2641
+
2642
+ if( this.onSelectClip )
2643
+ this.onSelectClip(track.clips[clipIndex]);
2644
+ }
2645
+ }
2646
+
2647
+ showContextMenu( e ) {
2648
+
2649
+ e.preventDefault();
2650
+ e.stopPropagation();
2651
+
2652
+ let actions = [];
2653
+ if(this.lastClipsSelected.length) {
2654
+ actions.push(
2655
+ {
2656
+ title: "Copy",// + " <i class='bi bi-clipboard-fill float-right'></i>",
2657
+ callback: () => { this.copyContent();}
2658
+ }
2659
+ )
2660
+ actions.push(
2661
+ {
2662
+ title: "Delete",// + " <i class='bi bi-trash float-right'></i>",
2663
+ callback: () => {
2664
+ this.deleteContent({});
2665
+ // this.optimizeTracks();
2666
+ }
2667
+ }
2668
+ )
2669
+ }
2670
+ else{
2671
+
2672
+ if(this.clipsToCopy)
2673
+ {
2674
+ actions.push(
2675
+ {
2676
+ title: "Paste",// + " <i class='bi bi-clipboard-fill float-right'></i>",
2677
+ callback: () => {
2678
+ this.pasteContent();
2679
+ }
2680
+ }
2681
+ )
2682
+ }
2683
+ }
2684
+
2685
+ LX.addContextMenu("Options", e, (m) => {
2686
+ for(let i = 0; i < actions.length; i++) {
2687
+ m.add(actions[i].title, actions[i].callback )
2688
+ }
2689
+ });
2690
+
2691
+ }
2692
+
2693
+ onDrawContent( ctx, timeStart, timeEnd ) {
2694
+
2695
+ if(!this.animationClip)
2696
+ return;
2697
+ let tracks = this.animationClip.tracks|| [{name: "NMF", clips: []}];
2698
+ if(!tracks)
2699
+ return;
2700
+
2701
+ const height = this.trackHeight;
2702
+
2703
+ this.scrollableHeight = (tracks.length)*height + this.topMargin;
2704
+ let scroll_y = - this.currentScrollInPixels;
2705
+
2706
+ ctx.save();
2707
+ for(let i = 0; i < tracks.length; i++) {
2708
+ let track = tracks[i];
2709
+ this.drawTrackWithBoxes(ctx, (i) * height + scroll_y, height, track.name || "", track);
2710
+ }
2711
+
2712
+ ctx.restore();
2713
+
2714
+ }
2715
+
2716
+ // Creates a map for each item -> tracks
2717
+ processTracks(animation) {
2718
+
2719
+ this.tracksPerItem = {};
2720
+ this.tracksDictionary = {};
2721
+ this.animationClip = {
2722
+ name: animation.name,
2723
+ duration: animation.duration,
2724
+ speed: animation.speed ?? 1,
2725
+ tracks: []
2726
+ };
2727
+
2728
+ for( let i = 0; i < animation.tracks.length; ++i ) {
2729
+
2730
+ let track = animation.tracks[i];
2731
+
2732
+ const name = track.name;
2733
+ const type = track.type;
2734
+
2735
+ let trackInfo = {
2736
+ fullname: track.name,
2737
+ clips: track.clips,
2738
+ name: name, type: type,
2739
+ selected: [], edited: [], hovered: [], active: true,
2740
+ times: track.times,
2741
+ };
2742
+
2743
+ this.tracksDictionary[track.name] = name;
2744
+
2745
+ this.animationClip.tracks.push(trackInfo);
2746
+ }
2747
+ }
2748
+
2749
+ /**
2750
+ * @method optimizeTrack
2751
+ */
2752
+
2753
+ optimizeTrack(trackIdx) {
2754
+ if(this.animationClip.tracks[trackIdx].clips.length) {
2755
+ this.animationClip.tracks[trackIdx].idx = tracks.length;
2756
+ for(let j = 0; j < this.animationClip.tracks[trackIdx].clips.length; j++)
2757
+ {
2758
+ this.animationClip.tracks[trackIdx].clips[j].trackIdx = tracks.length;
2759
+ }
2760
+ let selectedIdx = 0;
2761
+ for(let l = 0; l < this.lastClipsSelected.length; l++)
2762
+ {
2763
+ let [t,c] = this.lastClipsSelected[l];
2764
+
2765
+ if(t > trackIdx)
2766
+ this.lastClipsSelected[l][1] = t - 1;
2767
+ if(t == trackIdx)
2768
+ selectedIdx = l;
2769
+ }
2770
+ this.lastClipsSelected = [...this.lastClipsSelected.slice(0, selectedIdx), ...this.lastClipsSelected.slice(selectedIdx + 1, this.lastClipsSelected.length)];
2771
+ tracks.push(this.animationClip.tracks[i]);
2772
+ }
2773
+ }
2774
+
2775
+ /**
2776
+ * @method optimizeTracks
2777
+ */
2778
+
2779
+ optimizeTracks() {
2780
+
2781
+ let tracks = [];
2782
+ for(let i = 0; i < this.animationClip.tracks.length; i++)
2783
+ {
2784
+ if(this.animationClip.tracks[i].clips.length) {
2785
+ this.animationClip.tracks[i].idx = tracks.length;
2786
+ for(let j = 0; j < this.animationClip.tracks[i].clips.length; j++)
2787
+ {
2788
+ this.animationClip.tracks[i].clips[j].trackIdx = tracks.length;
2789
+ }
2790
+ let selectedIdx = 0;
2791
+ for(let l = 0; l < this.lastClipsSelected.length; l++)
2792
+ {
2793
+ let [t,c] = this.lastClipsSelected[l];
2794
+
2795
+ if(t > i)
2796
+ this.lastClipsSelected[l][1] = t - 1;
2797
+ if(t == i)
2798
+ selectedIdx = l;
2799
+ }
2800
+ this.lastClipsSelected = [...this.lastClipsSelected.slice(0, selectedIdx), ...this.lastClipsSelected.slice(selectedIdx + 1, this.lastClipsSelected.length)];
2801
+ tracks.push(this.animationClip.tracks[i]);
2802
+ }
2803
+ }
2804
+ }
2805
+
2806
+ /** Add a clip to the timeline in a free track slot at the current time
2807
+ * @clip: clip to be added
2808
+ * @offsetTime: (optional) offset time of current time
2809
+ * @callback: (optional) function to call after adding the clip
2810
+ */
2811
+ addClip( clip, offsetTime = 0, callback = null ) {
2812
+
2813
+ // Update clip information
2814
+ let trackIdx = null;
2815
+ let newStart = this.currentTime + offsetTime + clip.start;
2816
+ if(clip.fadein != undefined)
2817
+ clip.fadein += (newStart - clip.start);
2818
+ if(clip.fadeout != undefined)
2819
+ clip.fadeout += (newStart - clip.start);
2820
+ clip.start = newStart;
2821
+
2822
+ // Time slot with other clip?
2823
+ let clipInCurrentSlot = null;
2824
+ if(!this.animationClip)
2825
+ this.addNewTrack();
2826
+
2827
+ for(let i = 0; i < this.animationClip.tracks.length; i++) {
2828
+ clipInCurrentSlot = this.animationClip.tracks[i].clips.find( t => {
2829
+ return LX.UTILS.compareThresholdRange(newStart, clip.start + clip.duration, t.start, t.start+t.duration);
2830
+ });
2831
+
2832
+ if(!clipInCurrentSlot)
2833
+ {
2834
+ trackIdx = i;
2835
+ break;
2836
+ }
2837
+ console.warn("There is already a clip stored in time slot ", clipInCurrentSlot)
2838
+ }
2839
+ if(trackIdx == undefined)
2840
+ {
2841
+ // clipIdx = this.animationClip.tracks.length;
2842
+ // this.animationClip.tracks.push({clipIdx: clipIdx, clips: []} );
2843
+ trackIdx = this.addNewTrack();
2844
+ }
2845
+ //this.saveState(clipIdx);
2846
+
2847
+ // Find new index
2848
+ let newIdx = this.animationClip.tracks[trackIdx].clips.findIndex( t => t.start > newStart );
2849
+
2850
+ // Add as last index
2851
+ let lastIndex = false;
2852
+ if(newIdx < 0) {
2853
+ newIdx = this.animationClip.tracks[trackIdx].clips.length;
2854
+ lastIndex = true;
2855
+ }
2856
+
2857
+ // Add clip
2858
+ const clipsArray = [];
2859
+ this.animationClip.tracks[trackIdx].clips.forEach( (a, b) => {
2860
+ b == newIdx ? clipsArray.push(clip, a) : clipsArray.push(a);
2861
+ } );
2862
+
2863
+ if(lastIndex) {
2864
+ clipsArray.push(clip);
2865
+ }
2866
+
2867
+ //Save track state before add the new clip
2868
+
2869
+ this.saveState(trackIdx, newIdx);
2870
+ this.animationClip.tracks[trackIdx].clips = clipsArray;
2871
+ // Move the other's clips properties
2872
+ let track = this.animationClip.tracks[trackIdx];
2873
+ for(let i = (track.clips.length - 1); i > newIdx; --i) {
2874
+ track.edited[i - 1] ? track.edited[i] = track.edited[i - 1] : 0;
2875
+ }
2876
+
2877
+ // Reset this clip's properties
2878
+ track.hovered[newIdx] = undefined;
2879
+ track.selected[newIdx] = true;
2880
+ track.edited[newIdx] = undefined;
2881
+
2882
+ this.lastClipsSelected.push( [track.idx, newIdx] );
2883
+
2884
+ let end = clip.start + clip.duration;
2885
+
2886
+ if( end > this.duration || !this.animationClip.duration)
2887
+ {
2888
+ this.setDuration(end);
2889
+ }
2890
+
2891
+ // // Update animation action interpolation info
2892
+ if(this.onUpdateTrack)
2893
+ this.onUpdateTrack( trackIdx );
2894
+
2895
+ LX.emit( "@on_current_time_" + this.constructor.name, this.currentTime);
2896
+ if(this.onSetTime)
2897
+ this.onSetTime(this.currentTime);
2898
+
2899
+ if(this.onSelectClip)
2900
+ this.onSelectClip(clip);
2901
+
2902
+ if(callback)
2903
+ callback();
2904
+
2905
+ this.resize();
2906
+ return newIdx;
2907
+ }
2908
+
2909
+ /** Add a clip to the timeline in a free track slot at the current time
2910
+ * @clip: clip to be added
2911
+ * @trackIdx: track index where to add the track
2912
+ * @offsetTime: (optional) offset time of current time
2913
+ * @callback: (optional) function to call after adding the clip
2914
+ */
2915
+ addClipInTrack( clip, trackIdx, offsetTime = 0, callback = null ) {
2916
+
2917
+ // Time slot with other clip?
2918
+ if(!this.animationClip)
2919
+ return;
2920
+
2921
+ // Find new index
2922
+ let newIdx = this.animationClip.tracks[trackIdx].clips.findIndex( t => t.start > clip.start );
2923
+
2924
+ // Add as last index
2925
+ let lastIndex = false;
2926
+ if(newIdx < 0) {
2927
+ newIdx = this.animationClip.tracks[trackIdx].clips.length;
2928
+ lastIndex = true;
2929
+ }
2930
+
2931
+ // Add clip
2932
+ const clipsArray = [];
2933
+ this.animationClip.tracks[trackIdx].clips.forEach( (a, b) => {
2934
+ b == newIdx ? clipsArray.push(clip, a) : clipsArray.push(a);
2935
+ } );
2936
+
2937
+ if(lastIndex) {
2938
+ clipsArray.push(clip);
2939
+ }
2940
+
2941
+ this.animationClip.tracks[trackIdx].clips = clipsArray;
2942
+ // Move the other's clips properties
2943
+ let track = this.animationClip.tracks[trackIdx];
2944
+ for(let i = (track.clips.length - 1); i > newIdx; --i) {
2945
+ track.edited[i - 1] ? track.edited[i] = track.edited[i - 1] : 0;
2946
+ }
2947
+
2948
+ // Reset this clip's properties
2949
+ track.hovered[newIdx] = undefined;
2950
+ track.selected[newIdx] = undefined;
2951
+ track.edited[newIdx] = undefined;
2952
+
2953
+
2954
+ let end = clip.start + clip.duration;
2955
+
2956
+ if( end > this.duration || !this.animationClip.duration)
2957
+ {
2958
+ this.setDuration(end);
2959
+ }
2960
+
2961
+ // // Update animation action interpolation info
2962
+ if(this.onUpdateTrack)
2963
+ this.onUpdateTrack( trackIdx );
2964
+
2965
+ LX.emit( "@on_current_time_" + this.constructor.name, this.currentTime);
2966
+ if(this.onSetTime)
2967
+ this.onSetTime(this.currentTime);
2968
+
2969
+ if(callback)
2970
+ callback();
2971
+ return newIdx;
2972
+ }
2973
+
2974
+ /** Add an array of clips to the timeline in the first free track at the current time
2975
+ * @clips: clips to be added
2976
+ * @offsetTime: (optional) offset time of current time
2977
+ * @callback: (optional) function to call after adding the clip
2978
+ */
2979
+ addClips( clips, offsetTime = 0, callback = null ) {
2980
+
2981
+ if(!this.animationClip)
2982
+ this.addNewTrack();
2983
+
2984
+ //Search track where to place each new clip
2985
+ let trackIdxs = {};
2986
+ for(let i = 0; i < this.animationClip.tracks.length; i++) {
2987
+ trackIdxs = {}
2988
+
2989
+ for(let c = 0; c < clips.length; c++) {
2990
+ let clip = clips[c];
2991
+ // Update clip information
2992
+ let newStart = this.currentTime + offsetTime + clip.start;
2993
+
2994
+ // Time slot with other clip?
2995
+ let clipInCurrentSlot = null;
2996
+
2997
+ if( c == 0 ) {
2998
+ clipInCurrentSlot = this.animationClip.tracks[i].clips.find( t => {
2999
+ return LX.UTILS.compareThresholdRange(newStart, newStart + clip.duration, t.start, t.start+t.duration);
3000
+ });
3001
+
3002
+ if(!clipInCurrentSlot)
3003
+ {
3004
+ trackIdxs[c] = {trackIdx:i , start: newStart, end: newStart + clip.duration};
3005
+ } else {
3006
+ console.warn("There is already a clip stored in time slot ", clipInCurrentSlot)
3007
+ if(!this.animationClip.tracks[i+1]) {
3008
+ this.addNewTrack();
3009
+
3010
+ trackIdxs[c] = {trackIdx: i+1, stat: newStart, end: newStart + clip.duration};
3011
+ }
3012
+ else {
3013
+
3014
+ break;
3015
+ }
3016
+ }
3017
+ }
3018
+ else {
3019
+
3020
+ for(let t in trackIdxs) {
3021
+ if(trackIdxs[t].trackIdx == trackIdxs[c -1].trackIdx) {
3022
+ clipInCurrentSlot = LX.UTILS.compareThresholdRange(newStart, newStart + clip.duration, trackIdxs[t].start, trackIdxs[t].end);
3023
+ if(clipInCurrentSlot)
3024
+ break;
3025
+ }
3026
+ }
3027
+ if(!clipInCurrentSlot) {
3028
+ clipInCurrentSlot = this.animationClip.tracks[trackIdxs[c-1].trackIdx].clips.find( t => {
3029
+ return LX.UTILS.compareThresholdRange(newStart, newStart + clip.duration, t.start, t.start+t.duration);
3030
+ });
3031
+ }
3032
+ if(!clipInCurrentSlot) {
3033
+
3034
+ trackIdxs[c] = {trackIdx: trackIdxs[c-1].trackIdx, start: newStart, end: newStart + clip.duration};
3035
+ }
3036
+ else{
3037
+
3038
+ let j = trackIdxs[c-1].trackIdx + 1;
3039
+ if(this.animationClip.tracks[j]) {
3040
+
3041
+ clipInCurrentSlot = this.animationClip.tracks[j].clips.find( t => {
3042
+ return LX.UTILS.compareThresholdRange(newStart, newStart + clip.duration, t.start, t.start+t.duration);
3043
+ });
3044
+
3045
+ if(!clipInCurrentSlot) {
3046
+
3047
+ trackIdxs[c] = {trackIdx: j, start: newStart, end: newStart + clip.duration};
3048
+ }
3049
+ else {
3050
+ break;
3051
+ }
3052
+ }
3053
+ else {
3054
+
3055
+ this.addNewTrack();
3056
+ trackIdxs[c] = {trackIdx: j, start: newStart, end: newStart + clip.duration};
3057
+ }
3058
+ }
3059
+
3060
+ if(trackIdxs[c] == null) {
3061
+ c = 0;
3062
+ trackIdxs = {}
3063
+ }
3064
+ }
3065
+
3066
+ }
3067
+
3068
+ if(Object.keys(trackIdxs).length == clips.length) {
3069
+ break;
3070
+ }
3071
+ }
3072
+
3073
+ //Add each clip in the assigned free slot track
3074
+ for(let i = 0; i < clips.length; i++) {
3075
+ let clip = clips[i];
3076
+ let newStart = trackIdxs[i].start;
3077
+ if(clip.fadein != undefined)
3078
+ clip.fadein += (newStart - clip.start);
3079
+ if(clip.fadeout != undefined)
3080
+ clip.fadeout += (newStart - clip.start);
3081
+ clip.start = newStart;
3082
+ clip.end = clip.start + clip.duration;
3083
+
3084
+ // Find new index
3085
+ let trackIdx = trackIdxs[i].trackIdx;
3086
+ let newIdx = this.animationClip.tracks[trackIdx].clips.findIndex( t => t.start > trackIdxs[i].start );
3087
+
3088
+ // Add as last index
3089
+ let lastIndex = false;
3090
+ if(newIdx < 0) {
3091
+ newIdx = this.animationClip.tracks[trackIdx].clips.length;
3092
+ lastIndex = true;
3093
+ }
3094
+
3095
+ // Add clip
3096
+ const clipsArray = [];
3097
+ this.animationClip.tracks[trackIdx].clips.forEach( (a, b) => {
3098
+ b == newIdx ? clipsArray.push(clip, a) : clipsArray.push(a);
3099
+ } );
3100
+
3101
+ if(lastIndex) {
3102
+ clipsArray.push(clip);
3103
+ }
3104
+
3105
+ //Save track state before add the new clip
3106
+ this.saveState(trackIdx, newIdx);
3107
+ this.animationClip.tracks[trackIdx].clips = clipsArray;
3108
+ // Move the other's clips properties
3109
+ let track = this.animationClip.tracks[trackIdx];
3110
+ for(let i = (track.clips.length - 1); i > newIdx; --i) {
3111
+ track.edited[i - 1] ? track.edited[i] = track.edited[i - 1] : 0;
3112
+ }
3113
+
3114
+ // Reset this clip's properties
3115
+ track.hovered[newIdx] = undefined;
3116
+ track.selected[newIdx] = true;
3117
+ track.edited[newIdx] = undefined;
3118
+ this.lastClipsSelected.push( [track.idx, newIdx] );
3119
+
3120
+ let end = clip.start + clip.duration;
3121
+
3122
+ if( end > this.duration || !this.animationClip.duration)
3123
+ {
3124
+ this.setDuration(end);
3125
+ }
3126
+
3127
+ // Update animation action interpolation info
3128
+ if(this.onUpdateTrack)
3129
+ this.onUpdateTrack( trackIdx );
3130
+ }
3131
+
3132
+ LX.emit( "@on_current_time_" + this.constructor.name, this.currentTime);
3133
+ if(this.onSetTime)
3134
+ this.onSetTime(this.currentTime);
3135
+
3136
+ if(callback)
3137
+ callback();
3138
+
3139
+ this.resize();
3140
+ return true;
3141
+ }
3142
+
3143
+ deleteContent() {
3144
+ this.deleteClip({});
3145
+ }
3146
+
3147
+ /** Delete clip from the timeline
3148
+ * @clip: clip to be delete
3149
+ * @callback: (optional) function to call after deleting the clip
3150
+ */
3151
+ deleteClip( e, clip, callback ) {
3152
+
3153
+ let index = -1;
3154
+ // Key pressed
3155
+ if(!clip && this.selectedClip) {
3156
+ clip = this.selectedClip;
3157
+ }
3158
+
3159
+ if(e.multipleSelection || !clip) {
3160
+
3161
+ // Split in tracks
3162
+ const perTrack = [];
3163
+ this.lastClipsSelected.forEach( e => perTrack[e[0]] ? perTrack[e[0]].push(e) : perTrack[e[0]] = [e] );
3164
+
3165
+ for(let pts of perTrack) {
3166
+
3167
+ if(!pts) continue;
3168
+
3169
+ pts = pts.sort( (a,b) => a[2] - b[2] );
3170
+
3171
+ let deletedIndices = 0;
3172
+
3173
+ // Delete every selected clip
3174
+ for(let [trackIdx, clipIdx] of pts) {
3175
+ this.saveState(trackIdx, clipIdx);
3176
+ this._delete(trackIdx, clipIdx );
3177
+ deletedIndices++;
3178
+ }
3179
+ }
3180
+ }
3181
+ else if ( clip ){
3182
+ const [trackIdx, clipIdx] = clip;
3183
+
3184
+ this.saveState(trackIdx, clipIdx);
3185
+ this._delete( trackIdx, clipIdx );
3186
+ }
3187
+
3188
+
3189
+ if(callback)
3190
+ callback();
3191
+
3192
+ this.timelineClickedClips = [];
3193
+ this.selectedClip = null;
3194
+ //this.unSelectAllClips();
3195
+ // // Update animation action interpolation info
3196
+
3197
+ }
3198
+
3199
+ _delete( trackIdx, clipIdx) {
3200
+
3201
+ let clips = this.animationClip.tracks[trackIdx].clips;
3202
+ if(clipIdx >= 0)
3203
+ {
3204
+ clips = [...clips.slice(0, clipIdx), ...clips.slice(clipIdx + 1, clips.length)];
3205
+ this.animationClip.tracks[trackIdx].clips = clips;
3206
+ if(clips.length)
3207
+ {
3208
+ let selectedIdx = 0;
3209
+ for(let i = 0; i < this.lastClipsSelected.length; i++)
3210
+ {
3211
+ let [t,c] = this.lastClipsSelected[i];
3212
+
3213
+ if( t == trackIdx && c > clipIdx)
3214
+ this.lastClipsSelected[i][1] = c - 1;
3215
+ if(t == trackIdx && c == clipIdx)
3216
+ selectedIdx = i;
3217
+ }
3218
+ this.lastClipsSelected = [...this.lastClipsSelected.slice(0, selectedIdx), ...this.lastClipsSelected.slice(selectedIdx + 1, this.lastClipsSelected.length)];
3219
+ }
3220
+ else {
3221
+ let selectedIdx = this.lastClipsSelected.findIndex( c=> c[0] == trackIdx && c[1] == clipIdx);
3222
+ this.lastClipsSelected = [...this.lastClipsSelected.slice(0, selectedIdx), ...this.lastClipsSelected.slice(selectedIdx + 1, this.lastClipsSelected.length)];
3223
+ }
3224
+ }
3225
+
3226
+ }
3227
+
3228
+ copyContent() {
3229
+ this.clipsToCopy = [...this.lastClipsSelected];
3230
+ }
3231
+
3232
+ pasteContent() {
3233
+ if(!this.clipsToCopy)
3234
+ return;
3235
+
3236
+ this.clipsToCopy.sort((a,b) => {
3237
+ if(a[0]<b[0])
3238
+ return -1;
3239
+ return 1;
3240
+ });
3241
+
3242
+ for(let i = 0; i < this.clipsToCopy.length; i++){
3243
+ let [trackIdx, clipIdx] = this.clipsToCopy[i];
3244
+ let clipToCopy = Object.assign({}, this.animationClip.tracks[trackIdx].clips[clipIdx]);
3245
+ this.addClip(clipToCopy, this.clipsToCopy.length > 1 ? clipToCopy.start : 0);
3246
+ }
3247
+ this.clipsToCopy = null;
3248
+ }
3249
+
3250
+ /**
3251
+ * @method addNewTrack
3252
+ */
3253
+
3254
+ addNewTrack() {
3255
+
3256
+ if(!this.animationClip)
3257
+ this.animationClip = {tracks:[]};
3258
+
3259
+ let trackInfo = {
3260
+ idx: this.animationClip.tracks.length,
3261
+ clips: [],
3262
+ selected: [], edited: [], hovered: []
3263
+ };
3264
+
3265
+ this.animationClip.tracks.push(trackInfo);
3266
+ return trackInfo.idx;
3267
+ }
3268
+
3269
+ /**
3270
+ * @method clearTrack
3271
+ */
3272
+
3273
+ clearTrack(idx) {
3274
+
3275
+ if(!this.animationClip) {
3276
+ this.animationClip = {tracks:[]};
3277
+ return;
3278
+ }
3279
+ this.saveState(idx);
3280
+
3281
+ if(this.animationClip.tracks[idx].locked )
3282
+ {
3283
+ return;
3284
+ }
3285
+ let trackInfo = {
3286
+ idx: idx,
3287
+ clips: [],
3288
+ selected: [], edited: [], hovered: []
3289
+ };
3290
+
3291
+ this.animationClip.tracks[idx] = trackInfo;
3292
+ return trackInfo.idx;
3293
+ }
3294
+
3295
+ saveState( trackIdx, clipIdx ) {
3296
+
3297
+ let track = this.animationClip.tracks[trackIdx];
3298
+ let clips = Array.from(track.clips);
3299
+ let trackInfo = Object.assign({}, track);
3300
+ trackInfo.clips = clips;
3301
+ this.trackState.push({
3302
+ idx: clipIdx,
3303
+ t: trackInfo,
3304
+ editedTracks: [].concat(trackInfo.edited)
3305
+ });
3306
+ }
3307
+
3308
+ undo() {
3309
+
3310
+ if(!this.trackState.length)
3311
+ return;
3312
+
3313
+ const state = this.trackState.pop();
3314
+ this.animationClip.tracks[state.t.idx].clips = state.t.clips;
3315
+
3316
+ // Update animation action interpolation info
3317
+ if(this.onUpdateTrack)
3318
+ this.onUpdateTrack( state.t.idx );
3319
+ }
3320
+
3321
+ getCurrentClip( track, time, threshold ) {
3322
+
3323
+ if(!track || !track.clips.length)
3324
+ return;
3325
+
3326
+ // Avoid iterating through all timestamps
3327
+ if((time + threshold) < track.clips[0])
3328
+ return;
3329
+
3330
+ for(let i = 0; i < track.clips.length; ++i) {
3331
+ let t = track.clips[i];
3332
+ if(t.start + t.duration >= (time - threshold) &&
3333
+ t.start <= (time + threshold)) {
3334
+ return i;
3335
+ }
3336
+ }
3337
+
3338
+ return;
3339
+ };
3340
+
3341
+ unSelectAllClips() {
3342
+
3343
+ for(let [ idx, keyIndex] of this.lastClipsSelected) {
3344
+ this.animationClip.tracks[idx].selected[keyIndex]= false;
3345
+ }
3346
+ // Something has been unselected
3347
+ const unselected = this.lastClipsSelected.length > 0;
3348
+ this.lastClipsSelected.length = 0;
3349
+ this.timelineClickedClips = null;
3350
+ this.timelineClickedClipsTime = null;
3351
+ this.selectedClip = false;
3352
+ return unselected;
3353
+ }
3354
+
3355
+ processCurrentClip( e, clipIndex, track, localX, multiple ) {
3356
+
3357
+ e.multipleSelection = multiple;
3358
+ clipIndex = clipIndex ?? this.getCurrentClip( track, this.xToTime( localX ), this.pixelsToSeconds * 5 );
3359
+
3360
+ if(!multiple && e.button != 2) {
3361
+ this.unSelectAllClips();
3362
+ }
3363
+
3364
+ if(clipIndex == undefined)
3365
+ return;
3366
+
3367
+ if(track.selected[clipIndex])
3368
+ return;
3369
+
3370
+ let currentSelection = [ track.idx, clipIndex];
3371
+ // Select if not handled
3372
+ this.lastClipsSelected.push( currentSelection );
3373
+ track.selected[clipIndex] = true;
3374
+
3375
+ // if( !multiple && this.onSetTime )
3376
+ // this.onSetTime( track.clips[ clipIndex ] );
3377
+
3378
+ if( this.onSelectClip && this.onSelectClip(track.clips[ clipIndex ])) {
3379
+ // Event handled
3380
+ return;
3381
+ }
3382
+ }
3383
+
3384
+ getClipsInRange( track, minTime, maxTime, threshold ) {
3385
+
3386
+ if(!track || !track.clips.length)
3387
+ return;
3388
+
3389
+ // Manage negative selection
3390
+ if(minTime > maxTime) {
3391
+ let aux = minTime;
3392
+ minTime = maxTime;
3393
+ maxTime = aux;
3394
+ }
3395
+
3396
+ // Avoid iterating through all timestamps
3397
+
3398
+ if((maxTime + threshold) < track.clips[0].start)
3399
+ return;
3400
+
3401
+ let indices = [];
3402
+
3403
+ for(let i = 0; i < track.clips.length; ++i) {
3404
+ let t = track.clips[i];
3405
+ if((t.start + t.duration <= (maxTime + threshold) || t.start <= (maxTime + threshold)) &&
3406
+ (t.start + t.duration >= (minTime - threshold) || t.start >= (minTime - threshold)) )
3407
+ {
3408
+ indices.push(i);
3409
+ }
3410
+ }
3411
+
3412
+ return indices;
3413
+ }
3414
+
3415
+ validateDuration(t) {
3416
+ for(let i = 0; i < this.animationClip.tracks.length; i++) {
3417
+ const track = this.animationClip.tracks[i];
3418
+ const clipsIdxs = this.getClipsInRange( track, t , this.animationClip.duration, 0 );
3419
+ if(!clipsIdxs || !clipsIdxs.length)
3420
+ continue;
3421
+ const clip = track.clips[clipsIdxs[clipsIdxs.length - 1]];
3422
+ t = Math.max(t, clip.start + clip.duration);
3423
+ }
3424
+ return t;
3425
+ }
3426
+ }
3427
+
3428
+ LX.ClipsTimeline = ClipsTimeline;
3429
+
3430
+
3431
+ /**
3432
+ * @class CurvesTimeline
3433
+ */
3434
+
3435
+ class CurvesTimeline extends Timeline {
3436
+
3437
+ /**
3438
+ * @param {string} name
3439
+ * @param {object} options = {animationClip, selectedItems, x, y, width, height, canvas, trackHeight, range}
3440
+ */
3441
+ constructor(name, options = {}) {
3442
+
3443
+ super(name, options);
3444
+
3445
+ this.tracksPerItem = {};
3446
+
3447
+ // this.selectedItems = selectedItems;
3448
+ this.snappedKeyFrameIndex = -1;
3449
+ this.autoKeyEnabled = false;
3450
+ this.valueBeforeMove = 0;
3451
+ this.range = options.range || [0, 1];
3452
+
3453
+ if(this.animationClip && this.animationClip.tracks.length)
3454
+ this.processTracks(animation);
3455
+
3456
+ // Add button data
3457
+ let offset = 25;
3458
+ if(this.active)
3459
+ {
3460
+
3461
+ }
3462
+ }
3463
+
3464
+ onMouseUp( e, time) {
3465
+
3466
+ let track = e.track;
3467
+ let localX = e.localX;
3468
+ let discard = e.discard;
3469
+
3470
+ if(e.shiftKey) {
3471
+ e.multipleSelection = true;
3472
+ // Multiple selection
3473
+ if(!discard && track) {
3474
+ this.processCurrentKeyFrame( e, null, track, localX, true );
3475
+ }
3476
+ // Box selection
3477
+ else if(this.boxSelectionEnd){
3478
+
3479
+ this.unSelectAllKeyFrames();
3480
+
3481
+ let tracks = this.getTracksInRange(this.boxSelectionStart[1], this.boxSelectionEnd[1], this.pixelsToSeconds * 5);
3482
+
3483
+ for(let t of tracks) {
3484
+ let keyFrameIndices = this.getKeyFramesInRange(t,
3485
+ this.xToTime( this.boxSelectionStart[0] ),
3486
+ this.xToTime( this.boxSelectionEnd[0] ),
3487
+ this.pixelsToSeconds * 5);
3488
+
3489
+ if(keyFrameIndices) {
3490
+ for(let index of keyFrameIndices)
3491
+ this.processCurrentKeyFrame( e, index, t, null, true );
3492
+ }
3493
+ }
3494
+ }
3495
+
3496
+ }else {
3497
+
3498
+ let boundingBox = this.canvas.getBoundingClientRect()
3499
+ if(e.y < boundingBox.top || e.y > boundingBox.bottom)
3500
+ return;
3501
+ // Check exact track keyframe
3502
+ if(!discard && track) {
3503
+ this.processCurrentKeyFrame( e, null, track, localX );
3504
+
3505
+ }
3506
+ else {
3507
+ let x = e.offsetX;
3508
+ let y = e.offsetY - this.topMargin;
3509
+ for( const b of this.buttonsDrawn ) {
3510
+ b.pressed = false;
3511
+ const bActive = x >= b[2] && x <= (b[2] + b[4]) && y >= b[3] && y <= (b[3] + b[5]);
3512
+ if(bActive) {
3513
+ const callback = b[6];
3514
+ if(callback) callback(e);
3515
+ else this[ b[1] ] = !this[ b[1] ];
3516
+ break;
3517
+ }
3518
+ }
3519
+ }
3520
+
3521
+ }
3522
+
3523
+ this.boxSelection = false;
3524
+ this.boxSelectionStart = null;
3525
+ this.boxSelectionEnd = null;
3526
+
3527
+ }
3528
+
3529
+ onMouseDown( e, time ) {
3530
+
3531
+ let localX = e.localX;
3532
+ let localY = e.localY;
3533
+ let track = e.track;
3534
+
3535
+ if(e.shiftKey) {
3536
+
3537
+ this.boxSelection = true;
3538
+ this.boxSelectionStart = [localX, localY - this.topMargin];
3539
+ e.multipleSelection = true;
3540
+
3541
+ }
3542
+ else if(track && !track.locked) {
3543
+
3544
+ const keyFrameIndex = this.getCurrentKeyFrame( track, this.xToTime( localX ), this.pixelsToSeconds * 5 );
3545
+ if( keyFrameIndex != undefined ) {
3546
+ this.processCurrentKeyFrame( e, keyFrameIndex, track, null, e.multipleSelection ); // Settings this as multiple so time is not being set
3547
+ if(e.ctrlKey || e.altKey) {
3548
+ this.movingKeys = true;
3549
+ this.canvas.style.cursor = "grab";
3550
+
3551
+ }
3552
+ // Set pre-move state
3553
+ for(let selectedKey of this.lastKeyFramesSelected) {
3554
+ let [name, idx, keyIndex] = selectedKey;
3555
+ let trackInfo = this.tracksPerItem[name][idx];
3556
+ selectedKey[3] = this.animationClip.tracks[ trackInfo.clipIdx ].times[ keyIndex ];
3557
+ }
3558
+
3559
+ this.timeBeforeMove = track.times[ keyFrameIndex ];
3560
+ this.valueBeforeMove = localY;
3561
+ }
3562
+ }
3563
+ else if(!track) {
3564
+ this.unSelectAllKeyFrames()
3565
+ let x = e.offsetX;
3566
+ let y = e.offsetY - this.topMargin;
3567
+ for( const b of this.buttonsDrawn ) {
3568
+ const bActive = x >= b[2] && x <= (b[2] + b[4]) && y >= b[3] && y <= (b[3] + b[5]);
3569
+ b.pressed = bActive;
3570
+ }
3571
+ }
3572
+ }
3573
+
3574
+ onMouseMove( e, time ) {
3575
+
3576
+ let localX = e.localX;
3577
+ let localY = e.localY;
3578
+ let track = e.track;
3579
+
3580
+ const innerSetTime = (t) => {
3581
+ LX.emit( "@on_current_time_" + this.constructor.name, t);
3582
+ if( this.onSetTime )
3583
+ this.onSetTime( t );
3584
+ }
3585
+ // Manage keyframe movement
3586
+ if(this.movingKeys) {
3587
+ this.clearState();
3588
+ const newTime = this.xToTime( localX );
3589
+
3590
+ for(let [name, idx, keyIndex, keyTime] of this.lastKeyFramesSelected) {
3591
+ track = this.tracksPerItem[name][idx];
3592
+ if(track && track.locked)
3593
+ return;
3594
+
3595
+ this.canvas.style.cursor = "grabbing";
3596
+
3597
+ if(e.ctrlKey) {
3598
+ const delta = this.timeBeforeMove - keyTime;
3599
+ this.animationClip.tracks[ track.clipIdx ].times[ keyIndex ] = Math.min( this.animationClip.duration, Math.max(0, newTime - delta) );
3600
+ }
3601
+
3602
+ if(e.altKey) {
3603
+ let trackRange = [this.tracksDrawn[track.idx][1], this.tracksDrawn[track.idx][1] + this.trackHeight];
3604
+ localY = Math.min( trackRange[1], Math.max(trackRange[0], localY) );
3605
+
3606
+ //convert to range track values
3607
+ let value = (((localY - trackRange[1]) * (this.range[1] - this.range[0])) / (trackRange[0] - trackRange[1])) + this.range[0];
3608
+ track.edited[keyIndex] = true;
3609
+ this.animationClip.tracks[ track.clipIdx ].values[ keyIndex ] = value;
3610
+ LX.emit( "@on_change_" + this.tracksDrawn[track.idx][0].type, value );
3611
+ }
3612
+ }
3613
+ return
3614
+
3615
+
3616
+ }
3617
+
3618
+ const removeHover = () => {
3619
+ if(this.lastHovered)
3620
+ this.tracksPerItem[ this.lastHovered[0] ][ this.lastHovered[1] ].hovered[ this.lastHovered[2] ] = undefined;
3621
+ };
3622
+
3623
+ if( this.grabbing && e.button != 2) {
3624
+
3625
+ var curr = time - this.currentTime;
3626
+ var delta = curr - this.grabTime;
3627
+ this.grabTime = curr;
3628
+
3629
+ // fix this
3630
+ if(e.shiftKey && track) {
3631
+
3632
+ let keyFrameIndex = this.getNearestKeyFrame( track, this.currentTime);
3633
+
3634
+ if(keyFrameIndex != this.snappedKeyFrameIndex){
3635
+ this.snappedKeyFrameIndex = keyFrameIndex;
3636
+ innerSetTime( this.currentTime );
3637
+ }
3638
+ }
3639
+ else{
3640
+ innerSetTime( this.currentTime );
3641
+ }
3642
+
3643
+ }
3644
+ else if(track) {
3645
+
3646
+ let keyFrameIndex = this.getCurrentKeyFrame( track, this.xToTime( localX ), this.pixelsToSeconds * 5 );
3647
+ if(keyFrameIndex != undefined) {
3648
+
3649
+ const name = this.tracksDictionary[track.fullname];
3650
+ let t = this.tracksPerItem[ name ][track.idx];
3651
+ removeHover();
3652
+ if(t && t.locked)
3653
+ return;
3654
+
3655
+ this.lastHovered = [name, track.idx, keyFrameIndex];
3656
+ t.hovered[keyFrameIndex] = true;
3657
+
3658
+ }else {
3659
+ removeHover();
3660
+ }
3661
+ }
3662
+ else {
3663
+ removeHover();
3664
+ }
3665
+ }
3666
+
3667
+ showContextMenu( e ) {
3668
+
3669
+ e.preventDefault();
3670
+ e.stopPropagation();
3671
+
3672
+ let actions = [];
3673
+ //let track = this.NMFtimeline.clip.tracks[0];
3674
+ if(this.lastKeyFramesSelected && this.lastKeyFramesSelected.length) {
3675
+ if(this.lastKeyFramesSelected.length == 1 && this.clipboard && this.clipboard.value)
3676
+ {
3677
+ actions.push(
3678
+ {
3679
+ title: "Paste",// + " <i class='bi bi-clipboard-fill float-right'></i>",
3680
+ callback: () => {
3681
+ this.pasteContent();
3682
+ }
3683
+ }
3684
+ )
3685
+ }
3686
+ actions.push(
3687
+ {
3688
+ title: "Copy",// + " <i class='bi bi-clipboard-fill float-right'></i>",
3689
+ callback: () => {
3690
+ this.copyContent();
3691
+ }
3692
+ }
3693
+ )
3694
+ actions.push(
3695
+ {
3696
+ title: "Delete",// + " <i class='bi bi-trash float-right'></i>",
3697
+ callback: () => {
3698
+ this.deleteContent({});
3699
+
3700
+ }
3701
+ }
3702
+ )
3703
+ }
3704
+ else{
3705
+
3706
+ actions.push(
3707
+ {
3708
+ title: "Add",
3709
+ callback: () => this.addKeyFrame( e.track, 0 )
3710
+ }
3711
+ )
3712
+
3713
+ if(this.clipboard && this.clipboard.keyframes)
3714
+ {
3715
+ actions.push(
3716
+ {
3717
+ title: "Paste",// + " <i class='bi bi-clipboard-fill float-right'></i>",
3718
+ callback: () => {
3719
+ this.pasteContent();
3720
+ }
3721
+ }
3722
+ )
3723
+ }
3724
+ }
3725
+
3726
+ LX.addContextMenu("Options", e, (m) => {
3727
+ for(let i = 0; i < actions.length; i++) {
3728
+ m.add(actions[i].title, actions[i].callback )
3729
+ }
3730
+ });
3731
+
3732
+ }
3733
+
3734
+ onDrawContent( ctx, timeStart, timeEnd ) {
3735
+
3736
+ if(this.selectedItems == null || !this.tracksPerItem)
3737
+ return;
3738
+ ctx.save();
3739
+ // this.canvasArea.root.innerHtml = "";
3740
+ let offset = this.trackHeight;
3741
+ this.scrollableHeight = this.topMargin;
3742
+ for(let t = 0; t < this.selectedItems.length; t++) {
3743
+ let tracks = this.tracksPerItem[this.selectedItems[t]] ? this.tracksPerItem[this.selectedItems[t]] : [{name: this.selectedItems[t]}];
3744
+ if(!tracks) continue;
3745
+
3746
+ const height = this.trackHeight;
3747
+ this.scrollableHeight += (tracks.length+1)*height ;
3748
+ let scroll_y = - this.currentScrollInPixels;
3749
+
3750
+ let offsetI = 0;
3751
+ for(let i = 0; i < tracks.length; i++) {
3752
+ let track = tracks[i];
3753
+ if(track.hide) {
3754
+ continue;
3755
+ }
3756
+
3757
+ this.drawTrackWithCurves(ctx, offsetI * height + offset + scroll_y, height, track.name + " (" + track.type + ")", this.animationClip.tracks[track.clipIdx], track);
3758
+ offsetI++;
3759
+ }
3760
+ offset += offsetI * height + height;
3761
+ }
3762
+ ctx.restore();
3763
+
3764
+ };
3765
+
3766
+ drawTrackWithCurves (ctx, y, trackHeight, name, track, trackInfo) {
3767
+ let keyframes = track.times;
3768
+ let values = track.values;
3769
+
3770
+ if(keyframes) {
3771
+
3772
+ ctx.fillStyle = "#2c303570";
3773
+ if(trackInfo.isSelected)
3774
+ ctx.fillRect(0, y - 3, ctx.canvas.width, trackHeight );
3775
+
3776
+ this.tracksDrawn.push([track,y+this.topMargin,trackHeight]);
3777
+
3778
+ //draw lines
3779
+ ctx.strokeStyle = "white";
3780
+ ctx.beginPath();
3781
+ for(var j = 0; j < keyframes.length; ++j)
3782
+ {
3783
+
3784
+ let time = keyframes[j];
3785
+ let value = values[j];
3786
+
3787
+ //convert to timeline track range
3788
+ value = (((value - this.range[0]) * ( -this.trackHeight) ) / (this.range[1] - this.range[0])) + this.trackHeight;
3789
+
3790
+ if( time < this.startTime || time > this.endTime )
3791
+ continue;
3792
+ let keyframePosX = this.timeToX( time );
3793
+
3794
+ ctx.save();
3795
+ ctx.translate(keyframePosX, y );
3796
+
3797
+ if( keyframePosX <= this.sidebarWidth ){
3798
+ ctx.moveTo( 0, value );
3799
+ }
3800
+ else {
3801
+ ctx.lineTo( 0, value );
3802
+ }
3803
+ ctx.restore()
3804
+
3805
+ }
3806
+ ctx.stroke();
3807
+ ctx.closePath();
3808
+ ctx.fillStyle = Timeline.COLOR;
3809
+ //draw points
3810
+ for(var j = 0; j < keyframes.length; ++j)
3811
+ {
3812
+ let time = keyframes[j];
3813
+ let selected = trackInfo.selected[j];
3814
+ let margin = 0;
3815
+ let size = 5;
3816
+ if( time < this.startTime || time > this.endTime )
3817
+ continue;
3818
+ var keyframePosX = this.timeToX( time );
3819
+ if( keyframePosX > this.sidebarWidth ){
3820
+ ctx.save();
3821
+
3822
+
3823
+ if(trackInfo.edited[j])
3824
+ ctx.fillStyle = Timeline.COLOR_EDITED;
3825
+ if(selected) {
3826
+ ctx.fillStyle = Timeline.COLOR_SELECTED;
3827
+ //size = 7;
3828
+ margin = -2;
3829
+ }
3830
+ if(trackInfo.hovered[j]) {
3831
+ //size = 7;
3832
+ ctx.fillStyle = Timeline.COLOR_HOVERED;
3833
+ margin = -2;
3834
+ }
3835
+ if(trackInfo.locked)
3836
+ ctx.fillStyle = Timeline.COLOR_LOCK;
3837
+
3838
+ if(!this.active || trackInfo.active == false)
3839
+ ctx.fillStyle = Timeline.COLOR_UNACTIVE;
3840
+
3841
+ ctx.translate(keyframePosX, y);
3842
+
3843
+ let value = values[j];
3844
+ value = (((value - this.range[0]) * ( -this.trackHeight) ) / (this.range[1] - this.range[0])) + this.trackHeight;
3845
+
3846
+ ctx.beginPath();
3847
+ ctx.arc( 0, value, size, 0, Math.PI * 2);
3848
+ ctx.fill();
3849
+ ctx.closePath();
3850
+
3851
+ if(trackInfo.selected[j]) {
3852
+ ctx.fillStyle = Timeline.COLOR_SELECTED;
3853
+ ctx.beginPath();
3854
+ ctx.arc( 0, value, size - margin, 0, Math.PI * 2);
3855
+ ctx.fill();
3856
+ ctx.closePath();
3857
+ }
3858
+ ctx.restore();
3859
+ }
3860
+ }
3861
+ }
3862
+ }
3863
+
3864
+ onUpdateTracks ( keyType ) {
3865
+
3866
+ if(this.selectedItems == null || this.lastKeyFramesSelected.length || !this.autoKeyEnabled)
3867
+ return;
3868
+
3869
+ for(let i = 0; i < this.selectedItems.length; i++) {
3870
+ let tracks = this.tracksPerItem[this.selectedItems[i]];
3871
+ if(!tracks) continue;
3872
+
3873
+ // Get current track
3874
+ const selectedTrackIdx = tracks.findIndex( t => t.type === keyType );
3875
+ if(selectedTrackIdx < 0)
3876
+ return;
3877
+ let track = tracks[ selectedTrackIdx ];
3878
+
3879
+ // Add new keyframe
3880
+ const newIdx = this.addKeyFrame( track );
3881
+ if(newIdx === null)
3882
+ continue;
3883
+
3884
+ // Select it
3885
+ this.lastKeyFramesSelected.push( [track.name, track.idx, newIdx] );
3886
+ track.selected[newIdx] = true;
3887
+
3888
+ }
3889
+
3890
+ LX.emit( "@on_current_time_" + this.constructor.name, this.currentTime);
3891
+ // Update time
3892
+ if(this.onSetTime)
3893
+ this.onSetTime(this.currentTime);
3894
+
3895
+ return true; // Handled
3896
+ }
3897
+
3898
+ // Creates a map for each item -> tracks
3899
+ processTracks(animation) {
3900
+
3901
+ this.tracksPerItem = {};
3902
+ this.tracksDictionary = {};
3903
+ this.animationClip = {
3904
+ name: animation.name,
3905
+ duration: animation.duration,
3906
+ speed: animation.speed ?? 1,
3907
+ tracks: []
3908
+ };
3909
+ for( let i = 0; i < animation.tracks.length; ++i ) {
3910
+
3911
+ let track = animation.tracks[i];
3912
+
3913
+ const [name, type] = this.getTrackName(track.name);
3914
+
3915
+ let trackInfo = {
3916
+ fullname: track.name,
3917
+ name: name, type: type,
3918
+ dim: track.values.length/track.times.length,
3919
+ selected: [], edited: [], hovered: [], active: true,
3920
+ values: track.values,
3921
+ times: track.times
3922
+
3923
+ };
3924
+
3925
+ if(!this.tracksPerItem[name]) {
3926
+ this.tracksPerItem[name] = [trackInfo];
3927
+ }else {
3928
+ this.tracksPerItem[name].push( trackInfo );
3929
+ }
3930
+
3931
+
3932
+ const trackIndex = this.tracksPerItem[name].length - 1;
3933
+ this.tracksPerItem[name][trackIndex].idx = trackIndex;
3934
+ this.tracksPerItem[name][trackIndex].clipIdx = i;
3935
+
3936
+ // Save index also in original track
3937
+ trackInfo.idx = trackIndex;
3938
+ this.tracksDictionary[track.name] = name;
3939
+ this.animationClip.tracks.push(trackInfo);
3940
+
3941
+ }
3942
+ }
3943
+
3944
+ getNumTracks( item ) {
3945
+ if(!item || !this.tracksPerItem)
3946
+ return;
3947
+ const tracks = this.tracksPerItem[item.name];
3948
+ return tracks ? tracks.length : null;
3949
+ }
3950
+
3951
+
3952
+ onShowOptimizeMenu( e) {
3953
+
3954
+ if(this.selectedItems == null)
3955
+ return;
3956
+
3957
+ let tracks = [];
3958
+ for(let i = 0; i < this.selectedItems.length; i++) {
3959
+
3960
+ tracks = [...tracks, ...this.tracksPerItem[this.selectedItems[i]]];
3961
+ if(!tracks) continue;
3962
+
3963
+ }
3964
+ if(!tracks.length) return;
3965
+
3966
+ const threshold = this.onGetOptimizeThreshold ? this.onGetOptimizeThreshold() : 0.025;
3967
+ LX.addContextMenu("Optimize", e, m => {
3968
+ for( let t of tracks ) {
3969
+ m.add( t.name + (t.type ? "@" + t.type : ""), () => {
3970
+ if(!this.animationClip.tracks[t.clipIdx].optimize)
3971
+ return;
3972
+ this.animationClip.tracks[t.clipIdx].optimize( threshold );
3973
+ t.edited = [];
3974
+ if(this.onOptimizeTracks)
3975
+ this.onOptimizeTracks(t.clipIdx);
3976
+ })
3977
+ }
3978
+ });
3979
+ }
3980
+
3981
+ onPreProcessTrack( track, idx ) {
3982
+ const name = this.tracksDictionary[track.name];
3983
+ let trackInfo = this.tracksPerItem[track.name][idx];
3984
+ trackInfo.selected = [];
3985
+ trackInfo.edited = [];
3986
+ trackInfo.hovered = [];
3987
+ }
3988
+
3989
+ isKeyFrameSelected( track, index ) {
3990
+ return track.selected[ index ];
3991
+ }
3992
+
3993
+ saveState( clipIdx ) {
3994
+
3995
+ const localIdx = this.animationClip.tracks[clipIdx].idx;
3996
+ const name = this.getTrackName(this.animationClip.tracks[clipIdx].name)[0];
3997
+ const trackInfo = this.tracksPerItem[name][localIdx];
3998
+
3999
+ this.trackState.push({
4000
+ idx: clipIdx,
4001
+ t: this.animationClip.tracks[clipIdx].times.slice(),
4002
+ v: this.animationClip.tracks[clipIdx].values.slice(),
4003
+ editedTracks: [].concat(trackInfo.edited)
4004
+ });
4005
+ }
4006
+
4007
+ undo() {
4008
+
4009
+ if(!this.trackState.length)
4010
+ return;
4011
+
4012
+ const state = this.trackState.pop();
4013
+ this.animationClip.tracks[state.idx].times = state.t;
4014
+ this.animationClip.tracks[state.idx].values = state.v;
4015
+
4016
+ const localIdx = this.animationClip.tracks[state.idx].idx;
4017
+ const name = this.getTrackName(this.animationClip.tracks[state.idx].name)[0];
4018
+ this.tracksPerItem[name][localIdx].edited = state.editedTracks;
4019
+
4020
+ // Update animation action interpolation info
4021
+ if(this.onUpdateTrack)
4022
+ this.onUpdateTrack( state.idx );
4023
+ }
4024
+
4025
+ selectKeyFrame( track, selectionInfo, index ) {
4026
+
4027
+ if(index == undefined || !track)
4028
+ return;
4029
+
4030
+ this.unSelectAllKeyFrames();
4031
+
4032
+ this.lastKeyFramesSelected.push( selectionInfo );
4033
+ track.selected[index] = true;
4034
+ this.currentTime = this.animationClip.tracks[track.clipIdx].times[ index ];
4035
+
4036
+ LX.emit( "@on_current_time_" + this.constructor.name, this.currentTime );
4037
+ if( this.onSetTime )
4038
+ this.onSetTime( this.currentTime );
4039
+ }
4040
+
4041
+ copyContent() {
4042
+ let toCopy = {};
4043
+ for(let i = 0; i < this.lastKeyFramesSelected.length; i++){
4044
+ let [id, trackIdx, keyIdx] = this.lastKeyFramesSelected[i];
4045
+ if(toCopy[this.tracksPerItem[id][trackIdx].clipIdx]) {
4046
+ toCopy[this.tracksPerItem[id][trackIdx].clipIdx].idxs.push(keyIdx);
4047
+ } else {
4048
+ toCopy[this.tracksPerItem[id][trackIdx].clipIdx] = {idxs : [keyIdx]};
4049
+ toCopy[this.tracksPerItem[id][trackIdx].clipIdx].track = this.tracksPerItem[id][trackIdx]
4050
+ }
4051
+ if(i == 0) {
4052
+ this.copyKeyFrameValue(this.tracksPerItem[id][trackIdx], keyIdx)
4053
+ }
4054
+ }
4055
+ for(let clipIdx in toCopy) {
4056
+
4057
+ this.copyKeyFrames(toCopy[clipIdx].track, toCopy[clipIdx].idxs)
4058
+ }
4059
+ }
4060
+
4061
+ copyKeyFrame( track, index ) {
4062
+
4063
+ // 1 element clipboard by now
4064
+
4065
+ let values = [];
4066
+ let start = index * track.dim;
4067
+ for(let i = start; i < start + track.dim; ++i)
4068
+ values.push( this.animationClip.tracks[ track.clipIdx ].values[i] );
4069
+
4070
+ this.clipboard = {
4071
+ type: track.type,
4072
+ values: values
4073
+ };
4074
+ }
4075
+
4076
+ pasteContent() {
4077
+ if(!this.clipboard)
4078
+ return;
4079
+
4080
+ if(this.clipboard.value && this.lastKeyFramesSelected.length == 1) {
4081
+
4082
+ let [id, trackIdx, keyIdx] = this.lastKeyFramesSelected[0];
4083
+ this.pasteKeyFrameValue({}, this.tracksPerItem[id][trackIdx], keyIdx);
4084
+ }
4085
+ if(this.clipboard.keyframes) {
4086
+ let currentTime = this.currentTime;
4087
+ for(let clipIdx in this.clipboard.keyframes) {
4088
+ let indices = Object.keys( this.clipboard.keyframes[clipIdx].values)
4089
+ this.pasteKeyFrames({multipleSelection: this.clipboard.keyframes.length}, clipIdx, indices);
4090
+ this.currentTime = currentTime;
4091
+ }
4092
+ }
4093
+ }
4094
+
4095
+ canPasteKeyFrame () {
4096
+ return this.clipboard != null;
4097
+ }
4098
+
4099
+
4100
+ _paste( track, index ) {
4101
+
4102
+ let clipboardInfo = this.clipboard.value;
4103
+
4104
+ if(clipboardInfo.type != track.type){
4105
+ return;
4106
+ }
4107
+
4108
+ let start = index * track.dim;
4109
+ let j = 0;
4110
+ for(let i = start; i < start + track.dim; ++i) {
4111
+ this.animationClip.tracks[ track.clipIdx ].values[i] = clipboardInfo.values[j];
4112
+ ++j;
4113
+ }
4114
+
4115
+ LX.emit( "@on_current_time_" + this.constructor.name, this.currentTime);
4116
+ if(this.onSetTime)
4117
+ this.onSetTime(this.currentTime);
4118
+
4119
+ track.edited[ index ] = true;
4120
+ }
4121
+
4122
+ pasteKeyFrame( e, track, index ) {
4123
+
4124
+ this.saveState(track.clipIdx);
4125
+
4126
+ // Copy to current key
4127
+ this._paste( track, index );
4128
+
4129
+ if(!e.multipleSelection)
4130
+ return;
4131
+
4132
+ // Don't want anything after this
4133
+ this.clearState();
4134
+
4135
+ // Copy to every selected key
4136
+ for(let [name, idx, keyIndex] of this.lastKeyFramesSelected) {
4137
+ this._paste( this.tracksPerItem[name][idx], keyIndex );
4138
+ }
4139
+ }
4140
+
4141
+ pasteKeyFrameValue( e, track, index ) {
4142
+
4143
+ this.saveState(track.clipIdx);
4144
+
4145
+ // Copy to current key
4146
+ this._paste( track, index );
4147
+
4148
+ if(!e.multipleSelection)
4149
+ return;
4150
+
4151
+ // Don't want anything after this
4152
+ this.clearState();
4153
+
4154
+ // Copy to every selected key
4155
+ for(let [name, idx, keyIndex] of this.lastKeyFramesSelected) {
4156
+ this._paste( this.tracksPerItem[name][idx], keyIndex );
4157
+ }
4158
+ }
4159
+
4160
+ addKeyFrame( track, value = undefined, time = this.currentTime ) {
4161
+
4162
+ // Update animationClip information
4163
+ const clipIdx = track.clipIdx;
4164
+
4165
+ // Time slot with other key?
4166
+ const keyInCurrentSlot = this.animationClip.tracks[clipIdx].times.find( t => { return !LX.UTILS.compareThreshold(this.currentTime, t, t, 0.001 ); });
4167
+ if( keyInCurrentSlot ) {
4168
+ console.warn("There is already a keyframe stored in time slot ", keyInCurrentSlot)
4169
+ return;
4170
+ }
4171
+
4172
+ this.saveState(clipIdx);
4173
+
4174
+ // Find new index
4175
+ let newIdx = this.animationClip.tracks[clipIdx].times.findIndex( t => t > time );
4176
+
4177
+ // Add as last index
4178
+ let lastIndex = false;
4179
+ if(newIdx < 0) {
4180
+ newIdx = this.animationClip.tracks[clipIdx].times.length;
4181
+ lastIndex = true;
4182
+ }
4183
+
4184
+ // Add time key
4185
+ const timesArray = [];
4186
+ this.animationClip.tracks[clipIdx].times.forEach( (a, b) => {
4187
+ b == newIdx ? timesArray.push(time, a) : timesArray.push(a);
4188
+ } );
4189
+
4190
+ if(lastIndex) {
4191
+ timesArray.push(time);
4192
+ }
4193
+
4194
+ this.animationClip.tracks[clipIdx].times = new Float32Array( timesArray );
4195
+
4196
+ // Get mid values
4197
+ const values = value != undefined ? [value] : this.onGetSelectedItem();
4198
+
4199
+ // Add values
4200
+ let valuesArray = [];
4201
+
4202
+ let dim = values.length;
4203
+ this.animationClip.tracks[clipIdx].values.forEach( (a, b) => {
4204
+ if(b == newIdx * dim) {
4205
+ for( let i = 0; i < dim; ++i )
4206
+ valuesArray.push(values[i]);
4207
+ }
4208
+ valuesArray.push(a);
4209
+ } );
4210
+
4211
+ if(lastIndex) {
4212
+ for( let i = 0; i < dim; ++i )
4213
+ valuesArray.push(values[i]);
4214
+ }
4215
+
4216
+ this.animationClip.tracks[clipIdx].values = new Float32Array( valuesArray );
4217
+
4218
+
4219
+ // // Move the other's key properties
4220
+ // for(let i = (this.animationClip.tracks[clipIdx].times.length - 1); i > newIdx; --i) {
4221
+ // track.edited[i - 1] ? track.edited[i] = track.edited[i - 1] : 0;
4222
+ // }
4223
+
4224
+ // Reset this key's properties
4225
+ track.hovered[newIdx] = undefined;
4226
+ track.selected[newIdx] = undefined;
4227
+ track.edited[newIdx] = true;
4228
+
4229
+
4230
+ // Update animation action interpolation info
4231
+ if(this.onUpdateTrack)
4232
+ this.onUpdateTrack( clipIdx );
4233
+
4234
+ LX.emit( "@on_current_time_" + this.constructor.name, this.currentTime);
4235
+ if(this.onSetTime)
4236
+ this.onSetTime(this.currentTime);
4237
+
4238
+ return newIdx;
4239
+ }
4240
+
4241
+ deleteContent() {
4242
+
4243
+ this.deleteKeyFrame({ multipleSelection: this.lastKeyFramesSelected.length > 1});
4244
+ }
4245
+
4246
+ /** Delete a keyframe given the track and the its index
4247
+ * @track: track that keyframe belongs to
4248
+ * @index: index of the keyframe on the track
4249
+ */
4250
+ _delete( track, index ) {
4251
+
4252
+ // Don't remove by now the first key
4253
+ if(index == 0) {
4254
+ console.warn("Operation not supported! [remove first keyframe track]");
4255
+ return;
4256
+ }
4257
+
4258
+ // Update clip information
4259
+ const clipIdx = track.clipIdx;
4260
+
4261
+ // Don't remove by now the last key
4262
+ // if(index == this.animationClip.tracks[clipIdx].times.length - 1) {
4263
+ // console.warn("Operation not supported! [remove last keyframe track]");
4264
+ // return;
4265
+ // }
4266
+
4267
+ // Reset this key's properties
4268
+ track.hovered[index] = undefined;
4269
+ track.selected[index] = undefined;
4270
+ track.edited[index] = undefined;
4271
+
4272
+ // Delete time key
4273
+ this.animationClip.tracks[clipIdx].times = this.animationClip.tracks[clipIdx].times.filter( (v, i) => i != index);
4274
+
4275
+ // Delete values
4276
+ const indexDim = track.dim * index;
4277
+ const slice1 = this.animationClip.tracks[clipIdx].values.slice(0, indexDim);
4278
+ const slice2 = this.animationClip.tracks[clipIdx].values.slice(indexDim + track.dim);
4279
+
4280
+ this.animationClip.tracks[clipIdx].values = LX.UTILS.concatTypedArray([slice1, slice2], Float32Array);
4281
+
4282
+ // Move the other's key properties
4283
+ for(let i = index; i < this.animationClip.tracks[clipIdx].times.length; ++i) {
4284
+ track.edited[i] = track.edited[i + 1];
4285
+ }
4286
+
4287
+ // Update animation action interpolation info
4288
+ if(this.onDeleteKeyFrame)
4289
+ this.onDeleteKeyFrame( clipIdx, index );
4290
+
4291
+ return true;
4292
+ }
4293
+
4294
+ /** Delete one or more keyframes given the triggered event
4295
+ * @e: event
4296
+ * @track:
4297
+ * @index: index of the keyframe on the track
4298
+ */
4299
+ deleteKeyFrame(e, track, index) {
4300
+
4301
+ if(e.multipleSelection) {
4302
+
4303
+ // Split in tracks
4304
+ const perTrack = [];
4305
+ this.lastKeyFramesSelected.forEach( e => perTrack[e[1]] ? perTrack[e[1]].push(e) : perTrack[e[1]] = [e] );
4306
+
4307
+ for(let pts of perTrack) {
4308
+
4309
+ if(!pts) continue;
4310
+
4311
+ pts = pts.sort( (a,b) => a[2] - b[2] );
4312
+
4313
+ let deletedIndices = 0;
4314
+
4315
+ // Delete every selected key
4316
+ for(let [name, idx, keyIndex] of pts) {
4317
+ this.saveState(this.tracksPerItem[name][idx].clipIdx);
4318
+ let deleted = this._delete( this.tracksPerItem[name][idx], keyIndex - deletedIndices );
4319
+ deletedIndices += deleted ? 1 : 0;
4320
+ }
4321
+ }
4322
+ }
4323
+ else{
4324
+
4325
+ // Key pressed
4326
+ if(!track && this.lastKeyFramesSelected.length > 0) {
4327
+ const [itemName, trackIndex, keyIndex] = this.lastKeyFramesSelected[0];
4328
+ track = this.tracksPerItem[itemName][trackIndex];
4329
+ index = keyIndex;
4330
+ }
4331
+
4332
+ if ( track ){
4333
+ this.saveState(track.clipIdx);
4334
+ this._delete( track, index );
4335
+ }
4336
+ }
4337
+
4338
+ this.unSelectAllKeyFrames();
4339
+ }
4340
+
4341
+ /**
4342
+ * @method clearTrack
4343
+ */
4344
+
4345
+ clearTrack(idx, defaultValue) {
4346
+
4347
+ let track = this.animationClip.tracks[idx];
4348
+
4349
+ if(track.locked )
4350
+ {
4351
+ return;
4352
+ }
4353
+
4354
+ const count = track.times.length;
4355
+ for(let i = count - 1; i >= 0; i--)
4356
+ {
4357
+ this.saveState(track.clipIdx);
4358
+ this._delete(track, i );
4359
+ }
4360
+ if(defaultValue != undefined) {
4361
+ if(typeof(defaultValue) == 'number') {
4362
+ track.values[0] = defaultValue;
4363
+ }
4364
+ else {
4365
+ for(let i = 0; i < defaultValue.length; i++) {
4366
+ track.values[i] = defaultValue[i];
4367
+ }
4368
+ }
4369
+
4370
+ }
4371
+ return idx;
4372
+ }
4373
+
4374
+ getNumKeyFramesSelected() {
4375
+ return this.lastKeyFramesSelected.length;
4376
+ }
4377
+
4378
+ unSelect() {
4379
+
4380
+ if(!this.unSelectAllKeyFrames()) {
4381
+ this.selectedItems = null;
4382
+ if(this.onItemUnselected)
4383
+ this.onItemUnselected();
4384
+ }
4385
+ }
4386
+
4387
+ setSelectedItems( itemsName ) {
4388
+
4389
+ if(itemsName.constructor !== Array)
4390
+ throw("Item name has to be an array!");
4391
+
4392
+ this.selectedItems = itemsName;
4393
+ this.unSelectAllKeyFrames();
4394
+ this.updateLeftPanel();
4395
+ }
4396
+
4397
+ getTrack( trackInfo ) {
4398
+ const [name, trackIndex] = trackInfo;
4399
+ return this.tracksPerItem[ name ][trackIndex];
4400
+ }
4401
+
4402
+ getTrackName( uglyName ) {
4403
+
4404
+ let name, type;
4405
+
4406
+ // Support other versions
4407
+ if(uglyName.includes("[")) {
4408
+ const nameIndex = uglyName.indexOf('['),
4409
+ trackNameInfo = uglyName.substr(nameIndex+1).split("].");
4410
+ name = trackNameInfo[0].replaceAll(/[\[\]]/g,"");
4411
+ name = name.replaceAll("_", " ");
4412
+ type = trackNameInfo[1];
4413
+ }else {
4414
+ const trackNameInfo = uglyName.split(".");
4415
+ name = trackNameInfo[0].replaceAll(/[\[\]]/g,"");
4416
+ name = name.replaceAll("_", " ");
4417
+ type = trackNameInfo[1];
4418
+ }
4419
+
4420
+ return [name, type];
4421
+ }
4422
+
4423
+ getCurrentKeyFrame( track, time, threshold ) {
4424
+
4425
+ if(!track || !track.times.length)
4426
+ return;
4427
+
4428
+ // Avoid iterating through all timestamps
4429
+ if((time + threshold) < track.times[0])
4430
+ return;
4431
+
4432
+ for(let i = 0; i < track.times.length; ++i) {
4433
+ let t = track.times[i];
4434
+ if(t >= (time - threshold) &&
4435
+ t <= (time + threshold)) {
4436
+ return i;
4437
+ }
4438
+ }
4439
+
4440
+ return;
4441
+ }
4442
+
4443
+ getKeyFramesInRange( track, minTime, maxTime, threshold ) {
4444
+
4445
+ if(!track || !track.times.length)
4446
+ return;
4447
+
4448
+ // Manage negative selection
4449
+ if(minTime > maxTime) {
4450
+ let aux = minTime;
4451
+ minTime = maxTime;
4452
+ maxTime = aux;
4453
+ }
4454
+
4455
+ // Avoid iterating through all timestamps
4456
+ if((maxTime + threshold) < track.times[0])
4457
+ return;
4458
+
4459
+ let indices = [];
4460
+
4461
+ for(let i = 0; i < track.times.length; ++i) {
4462
+ let t = track.times[i];
4463
+ if(t >= (minTime - threshold) &&
4464
+ t <= (maxTime + threshold)) {
4465
+ indices.push(i);
4466
+ }
4467
+ }
4468
+
4469
+ return indices;
4470
+ }
4471
+
4472
+ getNearestKeyFrame( track, time ) {
4473
+
4474
+ if(!track || !track.times.length)
4475
+ return;
4476
+
4477
+ return track.times.reduce((a, b) => {
4478
+ return Math.abs(b - time) < Math.abs(a - time) ? b : a;
4479
+ });
4480
+ }
4481
+
4482
+ unSelectAllKeyFrames() {
4483
+
4484
+ for(let [name, idx, keyIndex] of this.lastKeyFramesSelected) {
4485
+ this.tracksPerItem[name][idx].selected[keyIndex] = false;
4486
+ }
4487
+
4488
+ // Something has been unselected
4489
+ const unselected = this.lastKeyFramesSelected.length > 0;
4490
+ this.lastKeyFramesSelected.length = 0;
4491
+ return unselected;
4492
+ }
4493
+
4494
+ processCurrentKeyFrame( e, keyFrameIndex, track, localX, multiple ) {
4495
+
4496
+ e.multipleSelection = multiple;
4497
+ keyFrameIndex = keyFrameIndex ?? this.getCurrentKeyFrame( track, this.xToTime( localX ), this.pixelsToSeconds * 5 );
4498
+
4499
+ if(!multiple && e.button != 2) {
4500
+ this.unSelectAllKeyFrames();
4501
+ }
4502
+
4503
+ const name = this.tracksDictionary[track.fullname];
4504
+ let t = this.tracksPerItem[ name ][track.idx];
4505
+ let currentSelection = [name, track.idx, keyFrameIndex];
4506
+ if(!multiple)
4507
+ this.selectKeyFrame(t, currentSelection, keyFrameIndex);
4508
+ else
4509
+ this.lastKeyFramesSelected.push( currentSelection );
4510
+
4511
+ if( this.onSelectKeyFrame && this.onSelectKeyFrame(e, currentSelection, keyFrameIndex)) {
4512
+ // Event handled
4513
+ return;
4514
+ }
4515
+
4516
+ if(keyFrameIndex == undefined)
4517
+ return;
4518
+
4519
+ // Select if not handled
4520
+ t.selected[keyFrameIndex] = true;
4521
+
4522
+ if( !multiple) {
4523
+
4524
+ LX.emit( "@on_current_time_" + this.constructor.name, track.times[ keyFrameIndex]);
4525
+ if(this.onSetTime)
4526
+ this.onSetTime( track.times[ keyFrameIndex ] );
4527
+ }
4528
+ }
4529
+ }
4530
+
4531
+ LX.CurvesTimeline = CurvesTimeline;
4532
+
4533
+ /**
4534
+ * Draws a rounded rectangle using the current state of the canvas.
4535
+ * If you omit the last three params, it will draw a rectangle
4536
+ * outline with a 5 pixel border radius
4537
+ * @param {Number} x The top left x coordinate
4538
+ * @param {Number} y The top left y coordinate
4539
+ * @param {Number} width The width of the rectangle
4540
+ * @param {Number} height The height of the rectangle
4541
+ * @param {Number} [radius = 5] The corner radius; It can also be an object
4542
+ * to specify different radii for corners
4543
+ * @param {Number} [radius.tl = 0] Top left
4544
+ * @param {Number} [radius.tr = 0] Top right
4545
+ * @param {Number} [radius.br = 0] Bottom right
4546
+ * @param {Number} [radius.bl = 0] Bottom left
4547
+ * @param {Boolean} [fill = false] Whether to fill the rectangle.
4548
+ * @param {Boolean} [stroke = true] Whether to stroke the rectangle.
4549
+ */
4550
+
4551
+ CanvasRenderingContext2D.prototype.roundRect = function(x, y, width, height, radius = 5, fill = false, stroke = false) {
4552
+
4553
+ if (typeof radius === 'number') {
4554
+ radius = {tl: radius, tr: radius, br: radius, bl: radius};
4555
+ } else {
4556
+ var defaultRadius = {tl: 0, tr: 0, br: 0, bl: 0};
4557
+ for (var side in defaultRadius) {
4558
+ radius[side] = radius[side] || defaultRadius[side];
4559
+ }
4560
+ }
4561
+
4562
+ this.beginPath();
4563
+ this.moveTo(x + radius.tl, y);
4564
+ this.lineTo(x + width - radius.tr, y);
4565
+ this.quadraticCurveTo(x + width, y, x + width, y + radius.tr);
4566
+ this.lineTo(x + width, y + height - radius.br);
4567
+ this.quadraticCurveTo(x + width, y + height, x + width - radius.br, y + height);
4568
+ this.lineTo(x + radius.bl, y + height);
4569
+ this.quadraticCurveTo(x, y + height, x, y + height - radius.bl);
4570
+ this.lineTo(x, y + radius.tl);
4571
+ this.quadraticCurveTo(x, y, x + radius.tl, y);
4572
+ this.closePath();
4573
+
4574
+ if (fill) {
4575
+ this.fill();
4576
+ }
4577
+ if (stroke) {
4578
+ this.stroke();
4579
+ }
4580
+ }
4581
+
4582
+ LX.UTILS.HexToRgb = (hex) => {
4583
+ var c;
4584
+ if(/^#([A-Fa-f0-9]{3}){1,2}$/.test(hex)){
4585
+ c= hex.substring(1).split('');
4586
+ if(c.length== 3){
4587
+ c= [c[0], c[0], c[1], c[1], c[2], c[2]];
4588
+ }
4589
+ c= '0x'+c.join('');
4590
+ return [(c>>16)&255, (c>>8)&255, c&255];
4591
+ }
4592
+ throw new Error('Bad Hex');
4593
+ }
4594
+
4595
+ LX.UTILS.concatTypedArray = (Arrays, ArrayType) => {
4596
+ return Arrays.reduce((acc, arr) => new ArrayType([...acc, ...arr]), []);
4597
+ }
4598
+
4599
+ export { Timeline, KeyFramesTimeline, ClipsTimeline, CurvesTimeline };