lexgui 0.1.32 → 0.1.34

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.
@@ -16,17 +16,13 @@ class Session {
16
16
  constructor() {
17
17
 
18
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;
19
+ this.left_margin = 0;
23
20
  this.scroll_y = 0;
24
- // this.offset_y = 0;
25
- // this.selection = null;
26
21
  }
27
22
  };
28
23
 
29
24
  /**
25
+ * Abstract class
30
26
  * @class Timeline
31
27
  * @description Agnostic timeline, do not impose any timeline content. Renders to a canvas
32
28
  */
@@ -43,9 +39,7 @@ class Timeline {
43
39
  this.currentTime = 0;
44
40
  this.framerate = 30;
45
41
  this.opacity = options.opacity || 1;
46
- this.sidebarWidth = 0// 200;
47
42
  this.topMargin = 40;
48
- this.renderOutFrames = false;
49
43
  this.lastMouse = [];
50
44
  this.lastKeyFramesSelected = [];
51
45
  this.tracksDrawn = [];
@@ -55,40 +49,46 @@ class Timeline {
55
49
  this.timeBeforeMove = 0;
56
50
  this.tracksPerItem = {};
57
51
  this.tracksDictionary = {};
58
- this._times = [];
59
-
60
- this.onBeforeCreateTopBar = options.onBeforeCreateTopBar;
61
- this.onAfterCreateTopBar = options.onAfterCreateTopBar;
62
- this.onChangePlayMode = options.onChangePlayMode;
63
52
 
64
53
  this.playing = false;
65
- this.loop = options.loop ?? true;
66
-
67
- this.session = new Session();
68
-
69
- this.canvas = options.canvas ?? document.createElement('canvas');
70
54
 
71
55
  this.duration = 1;
72
56
  this.speed = 1;
73
57
  this.position = [0, 0];
74
58
  this.size = [ options.width ?? 400, options.height ?? 100];
75
59
 
60
+ this.active = true;
61
+ this.loop = options.loop ?? true;
62
+ this.optimizeThreshold = 0.025;
63
+
64
+ this.session = new Session();
65
+
66
+ this.canvas = options.canvas ?? document.createElement('canvas');
67
+
76
68
  this.currentScroll = 0; //in percentage
77
69
  this.currentScrollInPixels = 0; //in pixels
78
70
  this.scrollableHeight = this.size[1]; //true height of the timeline content
79
-
71
+
80
72
  this.secondsToPixels = this.canvas.width/this.duration;
81
73
  this.pixelsToSeconds = 1 / this.secondsToPixels;
82
74
  this.selectedItems = options.selectedItems ?? null;
83
- this.animationClip = options.animationClip ?? null;
84
75
  this.trackHeight = options.trackHeight ?? 25;
85
76
 
86
- this.active = true;
87
77
  this.skipVisibility = options.skipVisibility ?? false;
88
78
  this.skipLock = options.skipLock ?? false;
89
79
  this.disableNewTracks = options.disableNewTracks ?? false;
90
-
91
- this.optimizeThreshold = 0.025;
80
+
81
+ this.onBeforeCreateTopBar = options.onBeforeCreateTopBar;
82
+ this.onAfterCreateTopBar = options.onAfterCreateTopBar;
83
+ this.onChangePlayMode = options.onChangePlayMode;
84
+ this.onChangeState = options.onChangeState;
85
+ this.onSetDuration = options.onSetDuration;
86
+ this.onSetSpeed = options.onSetSpeed;
87
+ this.onSelectTrack = options.onSelectTrack;
88
+ this.onChangeTrackVisibility = options.onChangeTrackVisibility;
89
+ this.onChangeTrackDisplay = options.onChangeTrackDisplay;
90
+ this.onSetTime = options.onSetTime;
91
+ this.onLockTrack = options.onLockTrack;
92
92
 
93
93
  this.root = new LX.Area({className : 'lextimeline'});
94
94
 
@@ -133,129 +133,487 @@ class Timeline {
133
133
  this.resizeCanvas( [ bounding.width, bounding.height + this.header_offset ] );
134
134
  }
135
135
  this.resize(this.size);
136
+
137
+ if(options.animationClip) {
138
+ this.setAnimationClip(options.animationClip);
139
+ }
136
140
  }
137
141
 
138
142
  /**
139
- * @method updateHeader
140
- * @param {*}
143
+ * @method setAnimationClip
144
+ * @param {*} animation
145
+ * @param {boolean} needsToProcess
146
+ * @param {obj} processOptions
147
+ * [KeyFrameTimeline] - each track should contain an attribute "dim" to indicate the value dimension (e.g. vector3 -> dim=3). Otherwise dimensions will be infered from track's values and times. Default is 1
141
148
  */
149
+ setAnimationClip( animation, needsToProcess = true ) {
150
+ if(!animation || !animation.tracks || needsToProcess) {
151
+ this.processTracks(animation); // generate default animationclip or process the user's one
152
+ }
153
+ else {
154
+ this.animationClip = animation;
155
+ }
156
+
157
+ this.duration = this.animationClip.duration;
158
+ this.speed = this.animationClip.speed ?? this.speed;
159
+ const w = Math.max(300, this.canvas.width);
160
+ this.secondsToPixels = ( w - this.session.left_margin ) / this.duration;
161
+ this.pixelsToSeconds = 1 / this.secondsToPixels;
142
162
 
143
- updateHeader() {
163
+ this.updateLeftPanel();
164
+ this.draw(this.currentTime);
165
+ return this.animationClip;
166
+ }
144
167
 
145
- if(this.header)
146
- this.header.clear();
147
- else {
148
- this.header = new LX.Panel({id:'lextimelineheader', height: this.header_offset+"px"});
149
- this.root.root.appendChild(this.header.root);
168
+ /**
169
+ * @method validateDuration
170
+ * @param {Number} t
171
+ * @returns minimum available duration
172
+ */
173
+ validateDuration(t) {
174
+ return t;
175
+ }
176
+
177
+ /**
178
+ * @method setDuration
179
+ * @param {Number} t
180
+ */
181
+
182
+ setDuration( t, updateHeader = true ) {
183
+ let v = this.validateDuration(t);
184
+ const decimals = t.toString().split('.')[1] ? t.toString().split('.')[1].length : 0;
185
+ updateHeader = (updateHeader || +v.toFixed(decimals) != t);
186
+ this.duration = this.animationClip.duration = v;
187
+
188
+ if(updateHeader) {
189
+ LX.emit( "@on_set_duration", +v.toFixed(3));
150
190
  }
151
191
 
152
- let header = this.header;
153
- LX.DEFAULT_NAME_WIDTH = "50%";
154
- header.sameLine();
192
+ if( this.onSetDuration ) {
193
+ this.onSetDuration( v );
194
+ }
195
+ }
155
196
 
156
- if(this.name) {
157
- header.addTitle(this.name);
197
+ /**
198
+ * @method setSpeed
199
+ * @param {Number} speed
200
+ */
201
+ setSpeed(speed) {
202
+ this.speed = speed;
203
+ LX.emit( "@on_set_speed", +speed.toFixed(3));
204
+
205
+ if( this.onSetSpeed ) {
206
+ this.onSetSpeed( speed );
158
207
  }
208
+ }
159
209
 
160
- header.addButton('', '<i class="fa-solid fa-'+ (this.playing ? 'pause' : 'play') +'"></i>', (value, event) => {
161
- this.changeState();
162
- }, {width: "40px", buttonClass: "accept"});
210
+ /**
211
+ * @method setScale
212
+ * @param {Number} v
213
+ */
214
+ setScale( v ) {
163
215
 
164
- header.addButton('', '<i class="fa-solid fa-rotate"></i>', (value, event) => {
165
- this.loop = !this.loop;
166
- if(this.onChangePlayMode) {
167
- this.onChangePlayMode(this.loop);
168
- }
216
+ if(!this.session)
217
+ return;
169
218
 
170
- }, {width: "40px", selectable: true, selected: this.loop});
171
-
172
- if(this.onBeforeCreateTopBar)
173
- this.onBeforeCreateTopBar(header);
219
+ if( this.secondsToPixels * v < 100)
220
+ return;
174
221
 
175
- header.addNumber("Current Time", this.currentTime, (value, event) => {
176
- if(value > this.duration) {
177
- value = this.duration;
178
- LX.emit( "@on_current_time_" + this.constructor.name, value);
179
- }
180
-
181
- this.currentTime = value;
182
- if(this.onSetTime)
183
- this.onSetTime(this.currentTime);
184
- this.draw();
185
-
186
- }, {signal: "@on_current_time_" + this.constructor.name, step: 0.01, min: 0, precision: 3, skipSlider: true});
222
+ const xCurrentTime = this.timeToX(this.currentTime);
223
+ this.secondsToPixels *= v;
224
+ this.pixelsToSeconds = 1 / this.secondsToPixels;
225
+ this.session.start_time += this.currentTime - this.xToTime(xCurrentTime);
226
+ this.draw();
227
+ }
187
228
 
188
- header.addNumber("Duration", +this.duration.toFixed(3), (value, event) => {
189
- this.setDuration(value, false)}, {step: 0.01, min: 0, signal: "@on_set_duration"
190
- });
229
+ /**
230
+ * @method setFramerate
231
+ * @param {Number} v
232
+ */
233
+ setFramerate( v ) {
234
+ this.framerate = v;
235
+ }
236
+
237
+ /** DEPRACATED??
238
+ * @method getCurrentFrame
239
+ * @param {Number} framerate
240
+ */
241
+ getCurrentFrame( framerate = this.framerate ) {
242
+ return Math.floor(this.currentTime * framerate);
243
+ }
191
244
 
192
- header.addNumber("Speed", +this.speed.toFixed(3), (value, event) => {
193
- this.setSpeed(value)}, {step: 0.01});
194
-
195
- if(this.onAfterCreateTopBar)
196
- this.onAfterCreateTopBar(header);
245
+ /**
246
+ * @method getContent
247
+ * @param {Track} track
248
+ * @param {Number} time
249
+ * @param {Number} threshold
250
+ */
251
+ getContent(track, time, threshold) {
197
252
 
198
- if(this.onShowOptimizeMenu)
199
- header.addButton("", '<i class="fa-solid fa-filter"></i>', (value, event) => {this.onShowOptimizeMenu(event)}, {width: "40px"});
253
+ if(this.getKeyFrame) {
254
+ return this.getKeyFrame(track, time, threshold);
255
+ }
200
256
 
201
- header.addButton("", '<i class="fa-solid fa-gear"></i>', (value, event) => {
202
- if(this.dialog)
203
- return;
204
- this.dialog = new LX.Dialog("Configuration", d => {
205
- d.addNumber("Framerate", this.framerate, (v) => {
206
- this.framerate = v;
207
- }, {min: 0, disabled: false});
208
- d.addNumber("Num items", Object.keys(this.tracksPerItem).length, null, {disabled: true});
209
- d.addNumber("Num tracks", this.animationClip ? this.animationClip.tracks.length : 0, null, {disabled: true});
210
- if(this.onShowOptimizeMenu)
211
- d.addNumber("Optimize Threshold", this.optimizeThreshold, v => {
212
- this.optimizeThreshold = v;
213
- }, {min: 0, max: 0.25, step: 0.001, precision: 4});
214
-
215
- }, {
216
- onclose: (root) => {
217
-
218
- root.remove();
219
- this.dialog = null;
220
- }
221
- })
222
- }, {width: "40px"})
257
+ if(this.getClip) {
258
+ return this.getClip(track, time, threshold);
259
+ }
260
+ }
223
261
 
224
- header.endLine();
225
- LX.DEFAULT_NAME_WIDTH = "30%";
262
+ /**
263
+ * @method getTracksInRange
264
+ * @param {Number} minY
265
+ * @param {Number} maxY
266
+ * @param {Number} threshold
267
+ */
268
+ getTracksInRange( minY, maxY, threshold ) {
269
+
270
+ let tracks = [];
271
+
272
+ // Manage negative selection
273
+ if(minY > maxY) {
274
+ let aux = minY;
275
+ minY = maxY;
276
+ maxY = aux;
277
+ }
278
+
279
+ for(let i = this.tracksDrawn.length - 1; i >= 0; --i) {
280
+ let t = this.tracksDrawn[i];
281
+ let pos = t[1] - this.topMargin, size = t[2];
282
+ if( pos + threshold >= minY && (pos + size - threshold) <= maxY ) {
283
+ tracks.push( t[0] );
284
+ }
285
+ }
286
+
287
+ return tracks;
226
288
  }
227
289
 
228
290
  /**
229
- * @method updateLeftPanel
230
- *
231
- */
232
- updateLeftPanel(area) {
291
+ * @method addNewTrack
292
+ * @description Add empty new track
293
+ */
294
+ addNewTrack() {
233
295
 
296
+ if(!this.animationClip)
297
+ this.animationClip = {tracks:[]};
234
298
 
235
- if(this.leftPanel)
236
- this.leftPanel.clear();
299
+ let trackInfo = {
300
+ idx: this.animationClip.tracks.length,
301
+ values: [], times: [],
302
+ selected: [], edited: [], hovered: []
303
+ };
304
+
305
+ this.animationClip.tracks.push(trackInfo);
306
+ this.updateLeftPanel();
307
+ return trackInfo.idx;
308
+ }
309
+
310
+ /**
311
+ * @method selectTrack
312
+ * @param {Object} trackInfo = {id, parent, children, visible}
313
+ */
314
+ selectTrack(trackInfo) {
315
+ this.unSelectAllTracks();
316
+
317
+ let [name, type] = trackInfo.id.split(" (");
318
+
319
+ if(type)
320
+ type = type.replaceAll(")", "").replaceAll(" ", "");
237
321
  else {
238
- this.leftPanel = area.addPanel({className: 'lextimelinepanel', width: "100%", height: "100%"});
322
+ type = name;
323
+ name = trackInfo.parent ? trackInfo.parent.id : trackInfo.id;
239
324
  }
325
+ let tracks = this.tracksPerItem[name];
240
326
 
241
- let panel = this.leftPanel;
242
- panel.sameLine(2);
243
- let title = panel.addTitle("Tracks");
327
+ for(let i = 0; i < tracks.length; i++) {
328
+ if(tracks[i].type != type && tracks.length > 1)
329
+ continue;
330
+ this.tracksPerItem[name][i].isSelected = true;
331
+ trackInfo = this.tracksPerItem[name][i];
332
+ }
244
333
 
245
- if(!this.disableNewTracks)
246
- {
247
- panel.addButton('', '<i class = "fa-solid fa-plus"></i>', (value, event) => {
248
- this.addNewTrack();
249
- }, {width: "40px", height: "40px"});
334
+ if(this.onSelectTrack) {
335
+ this.onSelectTrack(trackInfo);
250
336
  }
251
- panel.endLine();
337
+ }
252
338
 
253
- const styles = window.getComputedStyle(title);
254
- const titleHeight = title.clientHeight + parseFloat(styles['marginTop']) + parseFloat(styles['marginBottom']);
255
-
256
- let p = new LX.Panel({height: "calc(100% - " + titleHeight + "px)"});
257
- if(this.animationClip && this.selectedItems) {
258
- let items = {'id': '', 'children': []};
339
+ /**
340
+ * @method unSelectAllTracks
341
+ */
342
+ unSelectAllTracks() {
343
+ for(let i = 0; i < this.selectedItems.length; i++) {
344
+ let item = this.selectedItems[i];
345
+ let tracks = this.tracksPerItem[item];
346
+ for(let t = 0; t < tracks.length; t++) {
347
+ tracks[t].isSelected = false;
348
+ }
349
+ }
350
+ }
351
+
352
+ /**
353
+ * @method changeTrackVisibility
354
+ * @param {Object} trackInfo = {id, parent, children}
355
+ * @param {Boolean} visible
356
+ */
357
+ changeTrackVisibility(trackInfo, visible) {
358
+ let [name, type] = trackInfo.id.split(" (");
359
+ if(type)
360
+ type = type.replaceAll(")", "").replaceAll(" ", "");
361
+ else {
362
+ type = name;
363
+ name = trackInfo.parent ? trackInfo.parent.id : trackInfo.id;
364
+ }
365
+ trackInfo = {name, type};
366
+ let tracks = this.tracksPerItem[name];
367
+
368
+ for(let i = 0; i < tracks.length; i++) {
369
+ if(tracks[i].type != type && tracks.length > 1)
370
+ continue;
371
+ this.tracksPerItem[name][i].active = visible;
372
+ trackInfo = this.tracksPerItem[name][i];
373
+ }
374
+ this.draw();
375
+ if(this.onChangeTrackVisibility) {
376
+ this.onChangeTrackVisibility(trackInfo, visible);
377
+ }
378
+ }
379
+
380
+ /**
381
+ * @method changeTrackDisplay
382
+ * @param {Object} trackInfo = {id, parent, children}
383
+ * @param {Boolean} hide
384
+ */
385
+ changeTrackDisplay(trackInfo, hide) {
386
+
387
+ for(let idx = 0; idx < trackInfo.children.length; idx++) {
388
+ let [name, type] = trackInfo.children[idx].id.split(" (");
389
+ if(type)
390
+ type = type.replaceAll(")", "").replaceAll(" ", "");
391
+ else {
392
+ type = name;
393
+ name = trackInfo.parent ? trackInfo.parent.id : trackInfo.id;
394
+ }
395
+ //trackInfo = {name, type};
396
+ let tracks = this.tracksPerItem[name];
397
+
398
+ for(let i = 0; i < tracks.length; i++) {
399
+ if(tracks[i].type != type && tracks.length > 1)
400
+ continue;
401
+ this.tracksPerItem[name][i].hide = hide;
402
+ // trackInfo = this.tracksPerItem[name][i];
403
+ }
404
+ }
405
+
406
+ this.draw();
407
+
408
+ if(this.onChangeTrackDisplay) {
409
+ this.onChangeTrackDisplay(trackInfo, hide)
410
+ }
411
+ }
412
+
413
+ /**
414
+ * @method clearState
415
+ */
416
+ clearState() {
417
+ this.trackState = [];
418
+ }
419
+
420
+ /**
421
+ * @method show
422
+ * @description Show timeline area if it is hidden
423
+ */
424
+ show() {
425
+
426
+ this.root.show();
427
+ this.updateLeftPanel();
428
+ this.resize();
429
+ }
430
+
431
+ /**
432
+ * @method hide
433
+ * @description Hide timeline area
434
+ */
435
+ hide() {
436
+ this.root.hide();
437
+ }
438
+
439
+ /**
440
+ * @method resize
441
+ * @param {Array} size = [width, height]
442
+ */
443
+ resize( size = [this.root.parent.root.clientWidth, this.root.parent.root.clientHeight]) {
444
+
445
+ // this.root.root.style.width = size[0] + "px";
446
+ // this.root.root.style.height = size[1] + "px";
447
+
448
+ this.size = size;
449
+ //this.content_area.setSize([size[0], size[1] - this.header_offset]);
450
+ this.content_area.root.style.height = "calc(100% - "+ this.header_offset + "px)";
451
+
452
+ let w = size[0] - this.leftPanel.root.clientWidth - 8;
453
+ this.resizeCanvas([w , size[1]]);
454
+ }
455
+
456
+ /**
457
+ * @method resizeCanvas
458
+ * @param {*Array} size = [width, height]
459
+ */
460
+ resizeCanvas( size ) {
461
+ if( size[0] <= 0 && size[1] <=0 )
462
+ return;
463
+ if(Math.abs(this.canvas.width - size[0]) > 1) {
464
+
465
+ var w = Math.max(300, size[0] );
466
+ this.secondsToPixels = ( w- this.session.left_margin ) / this.duration;
467
+ this.pixelsToSeconds = 1 / this.secondsToPixels;
468
+ }
469
+ size[1] -= this.header_offset;
470
+ //this.canvasArea.setSize(size);
471
+ //this.canvasArea.root.style.height = "calc(100% - "+ this.header_offset + "px)";
472
+ this.canvas.width = this.canvasArea.root.clientWidth;
473
+ this.canvas.height = this.canvasArea.root.clientHeight;
474
+ this.draw(this.currentTime);
475
+ }
476
+
477
+ /**
478
+ * @method xToTime
479
+ * @description Converts distance in pixels to time
480
+ * @param {Number} x canvas position in pixels
481
+ */
482
+ xToTime( x ) {
483
+ return (x - this.session.left_margin) / this.secondsToPixels + this.session.start_time;
484
+ }
485
+ /**
486
+ * @method xToTime
487
+ * @description Converts time to disance in pixels
488
+ * @param {Number} t time
489
+ */
490
+ timeToX( t ) {
491
+ return this.session.left_margin + (t - this.session.start_time) * this.secondsToPixels;
492
+ }
493
+
494
+ /**
495
+ * @method updateHeader
496
+ * @param {*}
497
+ */
498
+ updateHeader() {
499
+
500
+ if(this.header)
501
+ this.header.clear();
502
+ else {
503
+ this.header = new LX.Panel({id:'lextimelineheader', height: this.header_offset+"px"});
504
+ this.root.root.appendChild(this.header.root);
505
+ }
506
+
507
+ let header = this.header;
508
+ LX.DEFAULT_NAME_WIDTH = "50%";
509
+ header.sameLine();
510
+
511
+ if(this.name) {
512
+ header.addTitle(this.name);
513
+ }
514
+
515
+ header.addButton('', '<i class="fa-solid fa-'+ (this.playing ? 'pause' : 'play') +'"></i>', (value, event) => {
516
+ this.changeState();
517
+ }, {width: "40px", buttonClass: "accept"});
518
+
519
+ header.addButton('', '<i class="fa-solid fa-rotate"></i>', (value, event) => {
520
+ this.loop = !this.loop;
521
+ if(this.onChangePlayMode) {
522
+ this.onChangePlayMode(this.loop);
523
+ }
524
+
525
+ }, {width: "40px", selectable: true, selected: this.loop});
526
+
527
+ if(this.onBeforeCreateTopBar)
528
+ this.onBeforeCreateTopBar(header);
529
+
530
+ header.addNumber("Current Time", this.currentTime, (value, event) => {
531
+ if(value > this.duration) {
532
+ value = this.duration;
533
+ if(event.constructor != CustomEvent) {
534
+ LX.emit( "@on_current_time_" + this.constructor.name, value);
535
+ }
536
+ }
537
+
538
+ this.currentTime = value;
539
+ if(this.onSetTime)
540
+ this.onSetTime(this.currentTime);
541
+ this.draw();
542
+
543
+ }, {signal: "@on_current_time_" + this.constructor.name, step: 0.01, min: 0, precision: 3, skipSlider: true});
544
+
545
+ header.addNumber("Duration", +this.duration.toFixed(3), (value, event) => {
546
+ this.setDuration(value, false)}, {step: 0.01, min: 0, signal: "@on_set_duration"
547
+ });
548
+
549
+ header.addNumber("Speed", +this.speed.toFixed(3), (value, event) => {
550
+ this.setSpeed(value)}, {step: 0.01});
551
+
552
+ if(this.onAfterCreateTopBar)
553
+ this.onAfterCreateTopBar(header);
554
+
555
+ if(this.onShowOptimizeMenu)
556
+ header.addButton("", '<i class="fa-solid fa-filter"></i>', (value, event) => {this.onShowOptimizeMenu(event)}, {width: "40px"});
557
+
558
+ header.addButton("", '<i class="fa-solid fa-gear"></i>', (value, event) => {
559
+ if(this.dialog)
560
+ return;
561
+ this.dialog = new LX.Dialog("Configuration", d => {
562
+ d.addNumber("Framerate", this.framerate, (v) => {
563
+ this.framerate = v;
564
+ }, {min: 0, disabled: false});
565
+ d.addNumber("Num items", Object.keys(this.tracksPerItem).length, null, {disabled: true});
566
+ d.addNumber("Num tracks", this.animationClip ? this.animationClip.tracks.length : 0, null, {disabled: true});
567
+ if(this.onShowOptimizeMenu)
568
+ d.addNumber("Optimize Threshold", this.optimizeThreshold, v => {
569
+ this.optimizeThreshold = v;
570
+ }, {min: 0, max: 0.25, step: 0.001, precision: 4});
571
+
572
+ }, {
573
+ onclose: (root) => {
574
+
575
+ root.remove();
576
+ this.dialog = null;
577
+ }
578
+ })
579
+ }, {width: "40px"})
580
+
581
+ header.endLine();
582
+ LX.DEFAULT_NAME_WIDTH = "30%";
583
+ }
584
+
585
+ /**
586
+ * @method updateLeftPanel Creates/updates the information of the drawn tracks in the left panel
587
+ * @param {Area} area LX.Area where to add the left panel
588
+ */
589
+ updateLeftPanel(area) {
590
+
591
+ if(this.leftPanel)
592
+ this.leftPanel.clear();
593
+ else {
594
+ this.leftPanel = area.addPanel({className: 'lextimelinepanel', width: "100%", height: "100%"});
595
+ }
596
+
597
+ let panel = this.leftPanel;
598
+ panel.sameLine(2);
599
+ const title = panel.addTitle("Tracks");
600
+
601
+ // If new tracks can be added manually, a button is added to the panel
602
+ if(!this.disableNewTracks)
603
+ {
604
+ panel.addButton('', '<i class = "fa-solid fa-plus"></i>', (value, event) => {
605
+ this.addNewTrack();
606
+ }, {width: "40px", height: "40px"});
607
+ }
608
+ panel.endLine();
609
+
610
+ const styles = window.getComputedStyle(title);
611
+ const titleHeight = title.clientHeight + parseFloat(styles['marginTop']) + parseFloat(styles['marginBottom']);
612
+
613
+ // Create a tree with the tracks for the selected item
614
+ let p = new LX.Panel({height: "calc(100% - " + titleHeight + "px)"});
615
+ if(this.animationClip && this.selectedItems) {
616
+ let items = {'id': '', 'children': []};
259
617
 
260
618
  for(let i = 0; i < this.selectedItems.length; i++ ) {
261
619
  let selected = this.selectedItems[i];
@@ -294,13 +652,13 @@ class Timeline {
294
652
  track = tracks[i];
295
653
  }
296
654
  }
297
- if(this.onLockTrack)
655
+ if(this.onLockTrack) {
298
656
  this.onLockTrack(el, track, node)
657
+ }
299
658
  this.draw();
300
659
  }
301
660
 
302
661
  }]})
303
- // panel.addTitle(track.name + (track.type? '(' + track.type + ')' : ''));
304
662
  }
305
663
  items.children.push(t);
306
664
  let el = p.addTree(null, t, {filter: false, rename: false, draggable: false, onevent: (e) => {
@@ -322,126 +680,95 @@ class Timeline {
322
680
  }
323
681
  panel.attach(p.root)
324
682
  p.root.style.overflowY = "scroll";
325
- p.root.addEventListener("scroll", (e) => {
326
-
683
+ p.root.addEventListener("scroll", (e) => {
327
684
  this.currentScroll = e.currentTarget.scrollTop/(e.currentTarget.scrollHeight - e.currentTarget.clientHeight);
328
- })
685
+ });
329
686
  // for(let i = 0; i < this.animationClip.tracks.length; i++) {
330
687
  // let track = this.animationClip.tracks[i];
331
688
  // panel.addTitle(track.name + (track.type? '(' + track.type + ')' : ''));
332
689
  // }
333
- if(this.leftPanel.parent.root.classList.contains("hidden") || !this.root.root.parent)
690
+ if(this.leftPanel.parent.root.classList.contains("hidden") || !this.root.root.parent) {
334
691
  return;
692
+ }
335
693
  this.resizeCanvas([ this.root.root.clientWidth - this.leftPanel.root.clientWidth - 8, this.size[1]]);
336
694
  }
337
695
 
338
696
  /**
339
- * @method addNewTrack
697
+ * @deprecated
698
+ * @method drawMarkers
699
+ * @param {*} ctx
700
+ * @param {*} markers
701
+ * TODO
340
702
  */
341
703
 
342
- addNewTrack() {
343
-
344
- if(!this.animationClip)
345
- this.animationClip = {tracks:[]};
346
-
347
- let trackInfo = {
348
- idx: this.animationClip.tracks.length,
349
- values: [], times: [],
350
- selected: [], edited: [], hovered: []
351
- };
352
-
353
- this.animationClip.tracks.push(trackInfo);
354
- this.updateLeftPanel();
355
- return trackInfo.idx;
356
- }
357
-
358
- getTracksInRange( minY, maxY, threshold ) {
359
-
360
- let tracks = [];
704
+ drawMarkers( ctx, markers ) {
361
705
 
362
- // Manage negative selection
363
- if(minY > maxY) {
364
- let aux = minY;
365
- minY = maxY;
366
- maxY = aux;
706
+ //render markers
707
+ ctx.fillStyle = "white";
708
+ ctx.textAlign = "left";
709
+ let markersPos = [];
710
+ for (let i = 0; i < markers.length; ++i) {
711
+ let marker = markers[i];
712
+ if (marker.time < this.startTime - this.pixelsToSeconds * 100 ||
713
+ marker.time > this.endTime)
714
+ continue;
715
+ var x = this.timeToX(marker.time);
716
+ markersPos.push(x);
717
+ ctx.save();
718
+ ctx.translate(x, 0);
719
+ ctx.rotate(Math.PI * -0.25);
720
+ ctx.fillText(marker.title, 20, 4);
721
+ ctx.restore();
367
722
  }
368
723
 
369
- for(let i = this.tracksDrawn.length - 1; i >= 0; --i) {
370
- let t = this.tracksDrawn[i];
371
- let pos = t[1] - this.topMargin, size = t[2];
372
- if( pos + threshold >= minY && (pos + size - threshold) <= maxY ) {
373
- tracks.push( t[0] );
724
+ if (markersPos.length) {
725
+ ctx.beginPath();
726
+ for (var i = 0; i < markersPos.length; ++i) {
727
+ ctx.moveTo(markersPos[i] - 5, 0);
728
+ ctx.lineTo(markersPos[i], -5);
729
+ ctx.lineTo(markersPos[i] + 5, 0);
730
+ ctx.lineTo(markersPos[i], 5);
731
+ ctx.lineTo(markersPos[i] - 5, 0);
374
732
  }
733
+ ctx.fill();
375
734
  }
376
-
377
- return tracks;
378
- }
379
-
380
- getCurrentContent(track, time, threshold) {
381
-
382
- if(this.getCurrentKeyFrame)
383
- return this.getCurrentKeyFrame(track, time, threshold);
384
-
385
- if(this.getCurrentClip)
386
- return this.getCurrentClip(track, time, threshold);
387
735
  }
388
736
 
389
737
  /**
390
- * @method setAnimationClip
391
- * @param {*} animation
392
- * @param {boolean} needsToProcess
393
- * TODO
738
+ * @method drawTimeInfo
739
+ * @description Draw top bar time information (number and lines)
394
740
  */
395
-
396
- setAnimationClip( animation, needsToProcess = true ) {
397
- if(!animation || !animation.tracks || needsToProcess) {
398
- this.processTracks(animation); // generate default animationclip or process the user's one
399
- }
400
- else{
401
- this.animationClip = animation;
402
- }
403
-
404
- this.duration = this.animationClip.duration;
405
- this.speed = this.animationClip.speed ?? this.speed;
406
- var w = Math.max(300, this.canvas.width);
407
- this.secondsToPixels = ( w - this.session.left_margin ) / this.duration;
408
-
409
- //this.updateHeader();
410
- this.updateLeftPanel();
411
-
412
- return this.animationClip;
413
- }
414
-
415
- drawTimeInfo (w, h = this.topMargin) {
741
+ drawTimeInfo() {
416
742
 
417
743
  let ctx = this.canvas.getContext("2d");
418
744
  ctx.font = "11px " + Timeline.FONT;//"11px Calibri";
419
745
  ctx.textAlign = "center";
420
746
 
421
- let canvas = this.canvas;
422
-
747
+ const canvas = this.canvas;
748
+ const h = this.topMargin;
749
+
423
750
  // Draw time markers
424
- let startx = Math.round( this.timeToX( this.startTime ) ) + 0.5;
425
- let endx = Math.round( this.timeToX( this.endTime ) ) + 0.5;
751
+ const startX = Math.round( this.timeToX( this.startTime ) ) + 0.5;
426
752
  let tick_time = this.secondsToPixels > 400 ? 0.1 : 0.5;
427
- if(this.secondsToPixels < 100 )
753
+ if(this.secondsToPixels < 100 ) {
428
754
  tick_time = 1;
755
+ }
429
756
 
430
757
  ctx.save();
431
-
432
758
  ctx.fillStyle = Timeline.BACKGROUND_COLOR;
433
759
  ctx.fillRect( this.session.left_margin, 0, canvas.width, h );
434
760
  ctx.strokeStyle = LX.Timeline.FONT_COLOR;
435
761
 
436
- if(this.secondsToPixels > 200 )
437
- {
762
+ // Paint in-between time info if there's enough zoom in
763
+ if(this.secondsToPixels > 200 ) {
438
764
  ctx.globalAlpha = 0.5 * (1.0 - LX.UTILS.clamp( 200 / this.secondsToPixels, 0, 1));
439
765
  ctx.beginPath();
440
766
  for( let time = this.startTime; time <= this.endTime; time += 1 / this.framerate )
441
767
  {
442
- let x = this.timeToX( time );
443
- if(x < this.session.left_margin)
768
+ const x = this.timeToX( time );
769
+ if(x < this.session.left_margin) {
444
770
  continue;
771
+ }
445
772
  ctx.moveTo(Math.round(x) + 0.5, h * 0.9);
446
773
  ctx.lineTo(Math.round(x) + 0.5, h * 0.95);
447
774
  }
@@ -451,17 +778,16 @@ class Timeline {
451
778
 
452
779
  ctx.globalAlpha = 0.5;
453
780
  ctx.beginPath();
454
- let times = this._times;
455
- this._times.length = 0;
781
+ let times = [];
456
782
 
457
- for( let time = this.startTime; time <= this.endTime; time += tick_time)
458
- {
459
- let x = this.timeToX( time );
783
+ for( let time = this.startTime; time <= this.endTime; time += tick_time) {
784
+ const x = this.timeToX( time );
460
785
 
461
- if(x < this.session.left_margin)
786
+ if(x < this.session.left_margin) {
462
787
  continue;
788
+ }
463
789
 
464
- let is_tick = time % 5 == 0;
790
+ const is_tick = time % 5 == 0;
465
791
  if(is_tick || this.secondsToPixels > 70 ) {
466
792
  times.push([x,time]);
467
793
  ctx.moveTo(Math.round(x) + 0.5, h * 0.4 + (is_tick ? h * 0.3 : h * 0.4) );
@@ -470,40 +796,43 @@ class Timeline {
470
796
  }
471
797
  }
472
798
 
473
- let x = startx;
799
+ let x = startX;
474
800
  if(x < this.session.left_margin) {
475
801
  x = this.session.left_margin;
476
802
  }
477
- //ctx.moveTo(x, h - 0.5);
478
- // ctx.lineTo( endx, h - 0.5);
803
+
479
804
  ctx.globalAlpha = this.opacity;
480
805
 
481
806
  // Time seconds in text
482
807
  ctx.fillStyle = Timeline.FONT_COLOR//"#888";
483
- for(var i = 0; i < times.length; ++i)
484
- {
808
+ for(var i = 0; i < times.length; ++i) {
485
809
  let time = times[i][1];
486
810
  ctx.fillText( time == (time|0) ? time : time.toFixed(1), times[i][0], h * 0.6);
487
811
  }
488
-
489
812
  ctx.restore();
490
813
  }
491
814
 
815
+ /**
816
+ * @method drawTracksBackground
817
+ * @description Draw empty tracks
818
+ * @param {Number} w
819
+ * @param {Number} h
820
+ */
492
821
  drawTracksBackground(w, h) {
493
822
 
494
- let canvas = this.canvas;
823
+ const canvas = this.canvas;
495
824
  let ctx = canvas.getContext("2d");
496
- let duration = this.duration;
825
+ const duration = this.duration;
497
826
  ctx.globalAlpha = this.opacity;
498
- //content
499
- let margin = this.session.left_margin;
500
- let timeline_height = this.topMargin;
501
- let line_height = this.trackHeight;
827
+
828
+ const margin = this.session.left_margin;
829
+ const timeline_height = this.topMargin;
830
+ const line_height = this.trackHeight;
502
831
 
503
- //fill track lines
504
832
  w = w || canvas.width;
505
- let max_tracks = Math.ceil( (h - timeline_height + this.currentScrollInPixels) / line_height );
506
-
833
+ const max_tracks = Math.ceil( (h - timeline_height + this.currentScrollInPixels) / line_height );
834
+
835
+ // Fill tracks background
507
836
  ctx.save();
508
837
  ctx.fillStyle = Timeline.BACKGROUND_COLOR;
509
838
  for(let i = 0; i <= max_tracks; ++i)
@@ -512,19 +841,19 @@ class Timeline {
512
841
  ctx.fillRect(0, timeline_height + i * line_height - this.currentScrollInPixels, w, line_height );
513
842
  }
514
843
 
515
- //black bg
844
+ // Fill background with opacity
516
845
  ctx.globalAlpha = 0.7;
517
846
  ctx.fillStyle = Timeline.BACKGROUND_COLOR;
518
847
  ctx.fillRect( margin, 0, canvas.width - margin, canvas.height);
519
848
  ctx.globalAlpha = this.opacity;
520
849
 
521
- //bg lines
850
+ // Draw start and end lines
522
851
  ctx.strokeStyle = "#444";
523
- ctx.beginPath();
524
-
852
+ ctx.beginPath();
525
853
  let pos = this.timeToX( 0 );
526
- if(pos < margin)
854
+ if(pos < margin) {
527
855
  pos = margin;
856
+ }
528
857
  ctx.moveTo( pos + 0.5, timeline_height);
529
858
  ctx.lineTo( pos + 0.5, canvas.height);
530
859
  ctx.moveTo( Math.round( this.timeToX( duration ) ) + 0.5, timeline_height);
@@ -536,42 +865,40 @@ class Timeline {
536
865
 
537
866
  /**
538
867
  * @method draw
539
- * @param {*} currentTime
540
- * @param {*} rect
541
- * TODO
868
+ * @param {Number} currentTime
869
+ * @param {Array} rect [x, y, width, height]
542
870
  */
543
-
544
871
  draw( currentTime = this.currentTime, rect ) {
545
872
 
546
873
  let ctx = this.canvas.getContext("2d");
547
874
  ctx.textBaseline = "bottom";
548
875
  ctx.font = "11px " + Timeline.FONT;//"11px Calibri";
549
- if(!rect)
876
+ if(!rect) {
550
877
  rect = [0, ctx.canvas.height - ctx.canvas.height , ctx.canvas.width, ctx.canvas.height ];
878
+ }
551
879
 
552
- // this.canvas = ctx.canvas;
553
880
  this.position[0] = rect[0];
554
881
  this.position[1] = rect[1];
555
882
  let w = rect[2];
556
883
  let h = rect[3];
557
884
  this.currentTime = currentTime;
558
- // this.updateHeader();
559
885
  this.currentScrollInPixels = this.scrollableHeight <= h ? 0 : (this.currentScroll * (this.scrollableHeight - h));
560
886
 
561
- //zoom
887
+ // Manage zoom
562
888
  if(this.duration > 0)
563
889
  {
564
890
  this.startTime = -50 / this.secondsToPixels;
565
- // this.startTime = Math.floor( this.session.start_time ); //seconds
566
891
  this.startTime = this.session.start_time ; //seconds
567
- if(this.startTime < 0)
892
+ if(this.startTime < 0) {
568
893
  this.startTime = 0;
569
- // this.endTime = Math.ceil( this.startTime + (w - this.session.left_margin) * this.pixelsToSeconds );
894
+ }
570
895
  this.endTime = this.startTime + (w - this.session.left_margin) * this.pixelsToSeconds ;
571
- if(this.endTime > this.duration)
896
+ if(this.endTime > this.duration) {
572
897
  this.endTime = this.duration;
573
- if(this.startTime > this.endTime) //avoids weird bug
898
+ }
899
+ if(this.startTime > this.endTime) { //avoids weird bug
574
900
  this.endTime = this.startTime + 1;
901
+ }
575
902
  }
576
903
 
577
904
  this.tracksDrawn.length = 0;
@@ -582,47 +909,48 @@ class Timeline {
582
909
  ctx.clearRect(0,0, this.canvas.width, this.canvas.height );
583
910
 
584
911
  this.drawTracksBackground(w, h);
585
-
586
- if(this.onDrawContent && this.animationClip) {
912
+
913
+ // Draw content
914
+ if(this.drawContent && this.animationClip) {
587
915
 
588
- ctx.translate( this.position[0], this.position[1] + this.topMargin ); //20 is the top margin area
589
-
590
- this.onDrawContent( ctx, this.timeStart, this.timeEnd, this );
591
-
592
- ctx.translate( -this.position[0], -(this.position[1] + this.topMargin) ); //20 is the top margin area
916
+ ctx.translate( this.position[0], this.position[1] + this.topMargin );
917
+ this.drawContent( ctx, this.timeStart, this.timeEnd, this );
918
+ ctx.translate( -this.position[0], -(this.position[1] + this.topMargin) );
593
919
  }
594
920
 
595
- //scrollbar
596
- if( h < this.scrollableHeight )
597
- {
921
+ // Draw scrollbar
922
+ if( h < this.scrollableHeight ) {
598
923
  ctx.fillStyle = "#222";
599
924
  ctx.fillRect( w - this.session.left_margin - 10, 0, 10, h );
600
925
  var scrollh = (h - this.topMargin)*h/ this.scrollableHeight;
601
926
  ctx.fillStyle = this.grabbingScroll ? Timeline.FONT_COLOR : Timeline.TRACK_COLOR_SECONDARY;
602
927
  ctx.roundRect( w - 8, this.currentScroll * (h - scrollh) + this.topMargin, 4, scrollh - this.topMargin, 5, true );
603
928
  }
604
- this.drawTimeInfo(w);
605
929
 
606
- // Current time marker vertical line
607
- let truePos = Math.round( this.timeToX( this.currentTime ) ) + 0.5;
608
- let quantCurrentTime = Math.round( this.currentTime * this.framerate ) / this.framerate;
609
- let pos = Math.round( this.timeToX( quantCurrentTime ) ) + 0.5; //current_time is quantized
610
-
611
- let posy = this.topMargin * 0.4;
612
- if(pos >= this.session.left_margin)
613
- {
930
+ // Draw top bar with time information
931
+ this.drawTimeInfo();
932
+
933
+ // Draw current time marker (vertical line)
934
+ const quantCurrentTime = Math.round( this.currentTime * this.framerate ) / this.framerate;
935
+ const posX = Math.round( this.timeToX( quantCurrentTime ) ) + 0.5; //current_time is quantized
936
+ const posY = this.topMargin * 0.4;
937
+ const truePos = Math.round( this.timeToX( this.currentTime ) ) + 0.5;
938
+
939
+ if(posX >= this.session.left_margin) { // Draw it only if its inside the timeline tracks area
940
+ // Draw vertical line
614
941
  ctx.strokeStyle = ctx.fillStyle = LX.getThemeColor("global-selected");
615
942
  ctx.globalAlpha = this.opacity;
616
943
  ctx.beginPath();
617
- ctx.moveTo(truePos, posy * 0.6); ctx.lineTo(truePos, this.canvas.height);//line
944
+ ctx.moveTo(truePos, posY * 0.6); ctx.lineTo(truePos, this.canvas.height); //line
618
945
  ctx.stroke();
619
946
  ctx.closePath();
947
+
948
+ // Draw top decoration
620
949
  ctx.shadowBlur = 8;
621
950
  ctx.shadowColor = LX.getThemeColor("global-selected");
622
951
  ctx.shadowOffsetX = 1;
623
952
  ctx.shadowOffsetY = 1;
624
-
625
- ctx.roundRect( truePos - 10, posy * 0.6, 20, posy, 5, true );
953
+ ctx.roundRect( truePos - 10, posY * 0.6, 20, posY, 5, true );
626
954
  ctx.fill();
627
955
  ctx.shadowBlur = 0;
628
956
  ctx.shadowOffsetX = 0;
@@ -632,11 +960,10 @@ class Timeline {
632
960
  // Current time seconds in text
633
961
  ctx.font = "11px " + Timeline.FONT;//"11px Calibri";
634
962
  ctx.textAlign = "center";
635
- //ctx.textBaseline = "middle";
636
963
  ctx.fillStyle = Timeline.COLOR_UNACTIVE//"#888";
637
964
  ctx.fillText( this.currentTime.toFixed(1), truePos, this.topMargin * 0.6 );
638
965
 
639
- // Selections
966
+ // Draw box selection
640
967
  ctx.strokeStyle = ctx.fillStyle = Timeline.FONT_COLOR;
641
968
  ctx.translate( this.position[0], this.position[1] + this.topMargin )
642
969
  if(this.boxSelection && this.boxSelectionStart && this.boxSelectionEnd) {
@@ -647,237 +974,98 @@ class Timeline {
647
974
  ctx.stroke();
648
975
  ctx.globalAlpha = this.opacity;
649
976
  }
650
- ctx.translate( -this.position[0], -(this.position[1] + this.topMargin) ); //20 is the top margin area
651
-
977
+ ctx.translate( -this.position[0], -(this.position[1] + this.topMargin) );
652
978
  }
653
979
 
654
980
  /**
655
- * @method drawMarkers
656
- * @param {*} ctx
657
- * @param {*} markers
658
- * TODO
981
+ * @method processMouse
982
+ * @param {Event} e
659
983
  */
984
+ processMouse( e ) {
660
985
 
661
- drawMarkers( ctx, markers ) {
662
-
663
- //render markers
664
- ctx.fillStyle = "white";
665
- ctx.textAlign = "left";
666
- let markersPos = [];
667
- for (let i = 0; i < markers.length; ++i) {
668
- let marker = markers[i];
669
- if (marker.time < this.startTime - this.pixelsToSeconds * 100 ||
670
- marker.time > this.endTime)
671
- continue;
672
- var x = this.timeToX(marker.time);
673
- markersPos.push(x);
674
- ctx.save();
675
- ctx.translate(x, 0);
676
- ctx.rotate(Math.PI * -0.25);
677
- ctx.fillText(marker.title, 20, 4);
678
- ctx.restore();
679
- }
680
-
681
- if (markersPos.length) {
682
- ctx.beginPath();
683
- for (var i = 0; i < markersPos.length; ++i) {
684
- ctx.moveTo(markersPos[i] - 5, 0);
685
- ctx.lineTo(markersPos[i], -5);
686
- ctx.lineTo(markersPos[i] + 5, 0);
687
- ctx.lineTo(markersPos[i], 5);
688
- ctx.lineTo(markersPos[i] - 5, 0);
689
- }
690
- ctx.fill();
986
+ if(!this.canvas) {
987
+ return;
691
988
  }
692
- }
989
+
990
+ e.multipleSelection = false;
693
991
 
694
- /**
695
- * @method clearState
696
- */
992
+ const w = this.canvas.width;
993
+ const h = this.canvas.height;
697
994
 
698
- clearState() {
699
- this.trackState = [];
700
- }
995
+ const x = e.offsetX;
996
+ const y = e.offsetY;
701
997
 
702
- /**
703
- * @method setDuration
704
- * @param {Number} t
705
- */
998
+ e.deltax = x - this.lastMouse[0];
999
+ e.deltay = y - this.lastMouse[1];
1000
+
1001
+ const localX = e.offsetX - this.position[0];
1002
+ const localY = e.offsetY - this.position[1];
706
1003
 
707
- setDuration( t, updateHeader = true ) {
708
- let v = this.validateDuration(t);
709
- let decimals = t.toString().split('.')[1] ? t.toString().split('.')[1].length : 0;
710
- updateHeader = (updateHeader || +v.toFixed(decimals) != t);
711
- this.duration = this.animationClip.duration = v;
1004
+ const currentTimeX = this.timeToX( this.currentTime );
1005
+ const current_grabbing_timeline = localY < this.topMargin && localX > this.session.left_margin && localX > (currentTimeX - 6) && localX < (currentTimeX + 6);
712
1006
 
713
- if(updateHeader) {
714
- LX.emit( "@on_set_duration", +v.toFixed(3));
1007
+ // Set cursor style depending of the mouse action
1008
+ if( current_grabbing_timeline ) {
1009
+ this.canvas.style.cursor = "col-resize";
1010
+ }
1011
+ else if(this.movingKeys) {
1012
+ this.canvas.style.cursor = "grabbing";
1013
+ }
1014
+ else if(e.shiftKey) {
1015
+ this.canvas.style.cursor = "crosshair";
1016
+ }
1017
+ else {
1018
+ this.canvas.style.cursor = "default";
715
1019
  }
716
1020
 
717
- if( this.onSetDuration )
718
- this.onSetDuration( v );
719
- }
720
-
721
- /**
722
- * @method validateDuration
723
- * @param {Number} t
724
- * @returns minimum available duration
725
- */
726
- validateDuration(t) {
727
- return t;
728
- }
729
-
1021
+ // Zoom in/out or scroll content
1022
+ if( e.type == "wheel" ) {
1023
+ if(e.shiftKey) {
1024
+ this.setScale( e.wheelDelta < 0 ? 0.95 : 1.05 );
1025
+ }
1026
+ else if( h < this.scrollableHeight) {
1027
+ this.leftPanel.root.children[1].scrollTop += e.deltaY * 0.1 ;
1028
+ }
1029
+ return;
1030
+ }
730
1031
 
731
- /**
732
- * @method setSpeed
733
- * @param {Number} speed
734
- */
1032
+ // Check if the mouse is inside the timeline
1033
+ const is_inside = x >= this.position[0] && x <= (this.position[0] + this.size[0]) &&
1034
+ y >= this.position[1] && y <= (this.position[1] + this.size[1]);
735
1035
 
736
- setSpeed(speed) {
737
- this.speed = speed;
738
- LX.emit( "@on_set_speed", +speed.toFixed(3));
739
-
1036
+ if(!is_inside) {
1037
+ return true;
1038
+ }
1039
+ // Check if the mouse is inside some track
1040
+ let track = null;
1041
+ for(let i = this.tracksDrawn.length - 1; i >= 0; --i)
1042
+ {
1043
+ const t = this.tracksDrawn[i];
1044
+ if( t[1] >= this.topMargin && localY >= t[1] && localY < (t[1] + t[2]) )
1045
+ {
1046
+ track = t[0];
1047
+ break;
1048
+ }
1049
+ }
740
1050
 
741
- if( this.onSetSpeed )
742
- this.onSetSpeed( speed );
743
- }
1051
+ e.track = track;
1052
+ e.localX = localX;
1053
+ e.localY = localY;
744
1054
 
745
- // Converts distance in pixels to time
746
- xToTime( x ) {
747
- return (x - this.session.left_margin) / this.secondsToPixels + this.session.start_time;
748
- }
1055
+ const time = this.xToTime(x, true);
1056
+ if( e.type == "mouseup" )
1057
+ {
1058
+ if(!this.active) {
1059
+ this.grabbing_timeline = false;
1060
+ this.grabbing = false;
1061
+ this.grabbingScroll = false;
1062
+ this.movingKeys = false;
1063
+ this.timeBeforeMove = null;
1064
+ return;
1065
+ }
749
1066
 
750
- // Converts time to disance in pixels
751
- timeToX( t ) {
752
- return this.session.left_margin + (t - this.session.start_time) * this.secondsToPixels;
753
- }
754
-
755
- getCurrentFrame( framerate ) {
756
- return Math.floor(this.currentTime * framerate);
757
- }
758
-
759
- /**
760
- * @method setScale
761
- * @param {*} v
762
- * TODO
763
- */
764
-
765
- setScale( v ) {
766
-
767
- if(!this.session)
768
- return;
769
-
770
- if( this.secondsToPixels * v < 100)
771
- return;
772
-
773
- const xCurrentTime = this.timeToX(this.currentTime);
774
- this.secondsToPixels *= v;
775
- this.pixelsToSeconds = 1 / this.secondsToPixels;
776
- this.session.start_time += this.currentTime - this.xToTime(xCurrentTime);
777
- this.draw();
778
- }
779
-
780
- /**
781
- * @method setFramerate
782
- * @param {*} v
783
- */
784
-
785
- setFramerate( v ) {
786
- this.framerate = v;
787
- }
788
-
789
- /**
790
- * @method processMouse
791
- * @param {*} e
792
- */
793
-
794
- processMouse( e ) {
795
-
796
- if(!this.canvas)
797
- return;
798
-
799
- e.multipleSelection = false;
800
-
801
- let h = this.canvas.height;
802
- let w = this.canvas.width;
803
-
804
- // Process mouse
805
- let x = e.offsetX;
806
- let y = e.offsetY;
807
- e.deltax = x - this.lastMouse[0];
808
- e.deltay = y - this.lastMouse[1];
809
- let localX = e.offsetX - this.position[0];
810
- let localY = e.offsetY - this.position[1];
811
-
812
- let timeX = this.timeToX( this.currentTime );
813
- let current_grabbing_timeline = localY < this.topMargin && localX > this.session.left_margin &&
814
- localX > (timeX - 6) && localX < (timeX + 6);
815
-
816
- if( current_grabbing_timeline ) {
817
- this.canvas.style.cursor = "col-resize";
818
- }
819
- else if(this.movingKeys) {
820
- this.canvas.style.cursor = "grabbing";
821
- }
822
- else if(e.shiftKey) {
823
- this.canvas.style.cursor = "crosshair";
824
- }
825
- else {
826
- this.canvas.style.cursor = "default";
827
- }
828
-
829
- if( e.type == "wheel" ) {
830
- if(e.shiftKey)
831
- {
832
- this.setScale( e.wheelDelta < 0 ? 0.95 : 1.05 );
833
- }
834
- else if( h < this.scrollableHeight)
835
- {
836
- this.leftPanel.root.children[1].scrollTop += e.deltaY * 0.1 ;
837
- }
838
-
839
- return;
840
- }
841
-
842
- var time = this.xToTime(x, true);
843
-
844
- var is_inside = x >= this.position[0] && x <= (this.position[0] + this.size[0]) &&
845
- y >= this.position[1] && y <= (this.position[1] + this.size[1]);
846
-
847
- var track = null;
848
- for(var i = this.tracksDrawn.length - 1; i >= 0; --i)
849
- {
850
- var t = this.tracksDrawn[i];
851
- if( t[1] >= this.topMargin && localY >= t[1] && localY < (t[1] + t[2]) )
852
- {
853
- track = t[0];
854
- break;
855
- }
856
- }
857
-
858
- e.track = track;
859
- e.localX = localX;
860
- e.localY = localY;
861
-
862
- const innerSetTime = (t) => {
863
- LX.emit( "@on_current_time_" + this.constructor.name, t);
864
- if( this.onSetTime )
865
- this.onSetTime( t );
866
- }
867
-
868
- if( e.type == "mouseup" )
869
- {
870
- if(!this.active) {
871
- this.grabbing_timeline = false;
872
- this.grabbing = false;
873
- this.grabbingScroll = false;
874
- this.movingKeys = false;
875
- this.timeBeforeMove = null;
876
- return;
877
- }
878
- // this.canvas.style.cursor = "default";
879
- const discard = this.movingKeys || (LX.UTILS.getTime() - this.clickTime) > 420; // ms
880
- this.movingKeys ? innerSetTime( this.currentTime ) : 0;
1067
+ const discard = this.movingKeys || (LX.UTILS.getTime() - this.clickTime) > 420; // ms
1068
+ this.movingKeys ? LX.emit( "@on_current_time_" + this.constructor.name, this.currentTime) : 0;
881
1069
 
882
1070
  this.grabbing_timeline = false;
883
1071
  this.grabbing = false;
@@ -888,7 +1076,7 @@ class Timeline {
888
1076
 
889
1077
  if(e.localY <= this.topMargin && !e.shiftKey) {
890
1078
  this.currentTime = Math.max(0, time);
891
- innerSetTime(this.currentTime);
1079
+ LX.emit( "@on_current_time_" + this.constructor.name, this.currentTime)
892
1080
  return;
893
1081
  }
894
1082
 
@@ -899,7 +1087,6 @@ class Timeline {
899
1087
  this.updateLeftPanel();
900
1088
  }
901
1089
 
902
-
903
1090
  if( e.type == "mousedown") {
904
1091
 
905
1092
  this.clickTime = LX.UTILS.getTime();
@@ -918,10 +1105,9 @@ class Timeline {
918
1105
 
919
1106
  this.grabbing = true;
920
1107
  this.grabTime = time - this.currentTime;
921
- if(!track || track && this.getCurrentContent(track, time, 0.001) == undefined) {
1108
+ if(!track || track && this.getContent(track, time, 0.001) == undefined) {
922
1109
  this.grabbing_timeline = current_grabbing_timeline;
923
1110
  }
924
-
925
1111
  if(this.onMouseDown && this.active )
926
1112
  this.onMouseDown(e, time);
927
1113
  }
@@ -1027,95 +1213,6 @@ class Timeline {
1027
1213
  }
1028
1214
  }
1029
1215
 
1030
- /**
1031
- * @method drawTrackWithKeyframes
1032
- * @param {*} ctx
1033
- * ...
1034
- * @description helper function, you can call it from onDrawContent to render all the keyframes
1035
- * TODO
1036
- */
1037
-
1038
- drawTrackWithKeyframes( ctx, y, trackHeight, title, track, trackInfo ) {
1039
-
1040
- if(trackInfo.enabled === false) {
1041
- ctx.globalAlpha = 0.4;
1042
- }
1043
-
1044
- ctx.font = Math.floor( trackHeight * 0.8) + "px" + Timeline.FONT;
1045
- ctx.textAlign = "left";
1046
-
1047
- ctx.globalAlpha = 0.2;
1048
-
1049
- if(trackInfo.isSelected) {
1050
- ctx.fillStyle = Timeline.TRACK_SELECTED;//"#2c303570";
1051
- ctx.fillRect(0, y, ctx.canvas.width, trackHeight );
1052
- }
1053
- ctx.fillStyle = Timeline.COLOR;//"#2c303570";
1054
- ctx.globalAlpha = 1;
1055
-
1056
- let keyframes = track.times;
1057
-
1058
- if(!keyframes) {
1059
- return;
1060
- }
1061
-
1062
- this.tracksDrawn.push([track,y+this.topMargin,trackHeight]);
1063
-
1064
- for(let j = 0; j < keyframes.length; ++j)
1065
- {
1066
- let time = keyframes[j];
1067
- let selected = trackInfo.selected[j];
1068
- if( time < this.startTime || time > this.endTime ) {
1069
- continue;
1070
- }
1071
- let keyframePosX = this.timeToX( time );
1072
- if( keyframePosX > this.sidebarWidth ){
1073
- ctx.save();
1074
-
1075
- let margin = -1;
1076
- let size = trackHeight * 0.3;
1077
-
1078
- if(trackInfo.edited[j]) {
1079
- ctx.fillStyle = Timeline.COLOR_EDITED;
1080
- }
1081
-
1082
- if(selected) {
1083
- ctx.fillStyle = Timeline.COLOR_SELECTED;
1084
- ctx.shadowBlur = 8;
1085
- size = trackHeight * 0.35;
1086
- margin = 0;
1087
- }
1088
-
1089
- if(trackInfo.hovered[j]) {
1090
- size = trackHeight * 0.35;
1091
- ctx.fillStyle = Timeline.COLOR_HOVERED;
1092
- ctx.shadowBlur = 8;
1093
- margin = 0;
1094
- }
1095
- if(trackInfo.locked) {
1096
- ctx.fillStyle = Timeline.COLOR_LOCK;
1097
- }
1098
-
1099
- if(!this.active || trackInfo.active == false) {
1100
- ctx.fillStyle = Timeline.COLOR_UNACTIVE;
1101
- }
1102
-
1103
- ctx.translate(keyframePosX, y + this.trackHeight * 0.75 + margin);
1104
- ctx.rotate(45 * Math.PI / 180);
1105
- ctx.fillRect( -size, -size, size, size);
1106
- if(selected) {
1107
- ctx.globalAlpha = 0.3;
1108
- ctx.fillRect( -size*1.1, -size*1.1, size*1.2, size*1.2);
1109
- }
1110
- ctx.shadowBlur = 0;
1111
- ctx.restore();
1112
- }
1113
- }
1114
-
1115
-
1116
- ctx.globalAlpha = this.opacity;
1117
- }
1118
-
1119
1216
  /**
1120
1217
  * @method drawTrackWithBoxes
1121
1218
  * @param {*} ctx
@@ -1249,208 +1346,395 @@ class Timeline {
1249
1346
 
1250
1347
  ctx.font = "12px" + Timeline.FONT;
1251
1348
  }
1349
+ };
1350
+
1351
+ Timeline.BACKGROUND_COLOR = LX.getThemeColor("global-color-primary");
1352
+ Timeline.TRACK_COLOR_PRIMARY = LX.getThemeColor("global-blur-background");
1353
+ Timeline.TRACK_COLOR_SECONDARY = LX.getThemeColor("global-color-terciary");
1354
+ Timeline.TRACK_SELECTED = LX.getThemeColor("global-selected");
1355
+ Timeline.TRACK_SELECTED_LIGHT = LX.getThemeColor("global-selected-light");
1356
+ Timeline.FONT = LX.getThemeColor("global-font");
1357
+ Timeline.FONT_COLOR = LX.getThemeColor("global-text");
1358
+ Timeline.COLOR = LX.getThemeColor("global-selected-dark");//"#5e9fdd";
1359
+ Timeline.COLOR_SELECTED = Timeline.COLOR_HOVERED = "rgba(250,250,20,1)";///"rgba(250,250,20,1)";
1360
+ // Timeline.COLOR_HOVERED = LX.getThemeColor("global-selected");
1361
+ Timeline.COLOR_UNACTIVE = "rgba(250,250,250,0.7)";
1362
+ Timeline.COLOR_LOCK = "rgba(255,125,125,0.7)";
1363
+ Timeline.COLOR_EDITED = "white"//"rgba(125,250,250, 1)";
1364
+
1365
+ LX.Timeline = Timeline;
1366
+
1367
+ /**
1368
+ * @class KeyFramesTimeline
1369
+ */
1370
+
1371
+ class KeyFramesTimeline extends Timeline {
1252
1372
 
1253
1373
  /**
1254
- * @method selectTrack
1255
- * @param {id, parent, children, visible} trackInfo
1256
- */
1374
+ * @param {string} name
1375
+ * @param {object} options = {animationClip, selectedItems, x, y, width, height, canvas, trackHeight}
1376
+ */
1377
+ constructor(name, options = {}) {
1257
1378
 
1258
- selectTrack( trackInfo) {
1259
- this.unSelectAllTracks();
1260
-
1261
- let [name, type] = trackInfo.id.split(" (");
1262
-
1263
- if(type)
1264
- type = type.replaceAll(")", "").replaceAll(" ", "");
1265
- else {
1266
- type = name;
1267
- name = trackInfo.parent ? trackInfo.parent.id : trackInfo.id;
1268
- }
1269
- let tracks = this.tracksPerItem[name];
1379
+ super(name, options);
1270
1380
 
1271
- for(let i = 0; i < tracks.length; i++) {
1272
- if(tracks[i].type != type && tracks.length > 1)
1273
- continue;
1274
- this.tracksPerItem[name][i].isSelected = true;
1275
- trackInfo = this.tracksPerItem[name][i];
1276
- }
1381
+ this.onShowOptimizeMenu = options.onShowOptimizeMenu;
1382
+ this.onGetOptimizeThreshold = options.onGetOptimizeThreshold;
1383
+
1384
+ this.tracksPerItem = {};
1277
1385
 
1278
- if(this.onSelectTrack)
1279
- this.onSelectTrack(trackInfo);
1280
- }
1386
+ // this.selectedItems = selectedItems;
1387
+ this.snappedKeyFrameIndex = -1;
1388
+ this.autoKeyEnabled = false;
1281
1389
 
1282
- unSelectAllTracks() {
1283
- for(let i = 0; i < this.selectedItems.length; i++) {
1284
- let item = this.selectedItems[i];
1285
- let tracks = this.tracksPerItem[item];
1286
- for(let t = 0; t < tracks.length; t++) {
1287
- tracks[t].isSelected = false;
1288
- }
1390
+ if(this.animationClip && this.animationClip.tracks.length) {
1391
+ this.processTracks(this.animationClip);
1289
1392
  }
1290
1393
  }
1291
1394
 
1292
1395
  /**
1293
- * @method changeTrackVisibility
1294
- * @param {id, parent, children, visible} trackInfo
1396
+ * @method processTrackS
1397
+ * @description Creates a map for each item -> tracks
1398
+ * @param {Object} animation
1295
1399
  */
1400
+ processTracks(animation) {
1296
1401
 
1297
- changeTrackVisibility(trackInfo, visible) {
1298
- let [name, type] = trackInfo.id.split(" (");
1299
- if(type)
1300
- type = type.replaceAll(")", "").replaceAll(" ", "");
1301
- else {
1302
- type = name;
1303
- name = trackInfo.parent ? trackInfo.parent.id : trackInfo.id;
1304
- }
1305
- trackInfo = {name, type};
1306
- let tracks = this.tracksPerItem[name];
1307
-
1308
- for(let i = 0; i < tracks.length; i++) {
1309
- if(tracks[i].type != type && tracks.length > 1)
1310
- continue;
1311
- this.tracksPerItem[name][i].active = visible;
1312
- trackInfo = this.tracksPerItem[name][i];
1402
+ this.tracksPerItem = {};
1403
+ this.tracksDictionary = {};
1404
+ this.animationClip = {
1405
+ name: (animation && animation.name) ? animation.name : "animationClip",
1406
+ duration: animation ? animation.duration : 0,
1407
+ speed: (animation && animation.speed ) ? animation.speed : this.speed,
1408
+ tracks: []
1409
+ };
1410
+ if (animation && animation.tracks) {
1411
+ for( let i = 0; i < animation.tracks.length; ++i ) {
1412
+
1413
+ let track = animation.tracks[i];
1414
+
1415
+ const [name, type] = this.processTrackName(track.name);
1416
+
1417
+ let valueDim = track.dim;
1418
+ if ( !valueDim || valueDim < 0 ){
1419
+ if ( track.times.length && track.values.length ){ valueDim = track.values.length/track.times.length; }
1420
+ else{ valueDim = 1; }
1421
+ }
1422
+
1423
+ let leftOver = track.values.length % valueDim; // just in case values has an incorrect length
1424
+ let amounEntries = Math.min( track.times.length, track.values.length - leftOver );
1425
+ let times = track.times.slice(0, amounEntries);
1426
+ let values = track.values.slice(0, amounEntries * valueDim);
1427
+ let boolArray = (new Array(amounEntries)).fill(false);
1428
+
1429
+ // TO DO: Check if it can be replaced for processTrack function
1430
+ let trackInfo = {
1431
+ fullname: track.name,
1432
+ name: name, type: type,
1433
+ active: true,
1434
+ dim: valueDim,
1435
+ selected: boolArray.slice(), edited: boolArray.slice(), hovered: boolArray.slice(),
1436
+ times: times,
1437
+ values: values
1438
+ };
1439
+
1440
+ if(!this.tracksPerItem[name]) {
1441
+ this.tracksPerItem[name] = [trackInfo];
1442
+ }else {
1443
+ this.tracksPerItem[name].push( trackInfo );
1444
+ }
1445
+
1446
+ const trackIndex = this.tracksPerItem[name].length - 1;
1447
+ trackInfo.idx = trackIndex; // index of track in "name"
1448
+ trackInfo.clipIdx = i; // index of track in the entire animation
1449
+
1450
+ // Save index also in original track
1451
+ track.idx = trackIndex;
1452
+ this.tracksDictionary[track.name] = name; // map original track name with shortened one
1453
+
1454
+ this.animationClip.tracks.push(trackInfo);
1455
+ }
1313
1456
  }
1314
- this.draw();
1315
- if(this.onChangeTrackVisibility)
1316
- this.onChangeTrackVisibility(trackInfo, visible);
1457
+ this.resize();
1317
1458
  }
1318
1459
 
1319
1460
  /**
1320
- * @method changeTrackDisplay
1321
- * @param {id, parent, children, display} trackInfo
1322
- */
1461
+ * @method processTrackName
1462
+ * @description Process track name to get the its name and its type separately
1463
+ * @param {String} trackName
1464
+ */
1465
+ processTrackName( trackName ) {
1323
1466
 
1324
- changeTrackDisplay(trackInfo, hide) {
1467
+ let name, type;
1325
1468
 
1326
- for(let idx = 0; idx < trackInfo.children.length; idx++) {
1327
- let [name, type] = trackInfo.children[idx].id.split(" (");
1328
- if(type)
1329
- type = type.replaceAll(")", "").replaceAll(" ", "");
1330
- else {
1331
- type = name;
1332
- name = trackInfo.parent ? trackInfo.parent.id : trackInfo.id;
1333
- }
1334
- //trackInfo = {name, type};
1335
- let tracks = this.tracksPerItem[name];
1469
+ // Support other versions
1470
+ if(trackName.includes("[")) {
1471
+ const nameIndex = trackName.indexOf('['),
1472
+ trackNameInfo = trackName.substr(nameIndex+1).split("].");
1473
+ name = trackNameInfo[0];
1474
+ type = trackNameInfo[1];
1475
+ }
1476
+ else {
1477
+ const trackNameInfo = trackName.split(".");
1478
+ name = trackNameInfo[0];
1479
+ type = trackNameInfo[1];
1480
+ }
1481
+ return [name, type];
1482
+ }
1336
1483
 
1337
- for(let i = 0; i < tracks.length; i++) {
1338
- if(tracks[i].type != type && tracks.length > 1)
1339
- continue;
1340
- this.tracksPerItem[name][i].hide = hide;
1341
- // trackInfo = this.tracksPerItem[name][i];
1484
+ processTrack(trackIdx) {
1485
+ if(!this.animationClip) {
1486
+ return;
1487
+ }
1488
+
1489
+ const track = this.animationClip.tracks[trackIdx];
1490
+ const [name, type] = this.processTrackName(track.fullname || track.name);
1491
+
1492
+ let trackInfo = {
1493
+ fullname: track.name,
1494
+ name: name, type: type,
1495
+ dim: track.values.length/track.times.length,
1496
+ selected: [], edited: [], hovered: [], active: true,
1497
+ times: track.times,
1498
+ values: track.values
1499
+ };
1500
+
1501
+ for(let i = 0; i < this.tracksPerItem[name].length; i++) {
1502
+ if(this.tracksPerItem[name][i].fullname == trackInfo.fullname) {
1503
+ trackInfo.idx = this.tracksPerItem[name][i].idx;
1504
+ trackInfo.clipIdx = this.tracksPerItem[name][i].clipIdx;
1505
+ this.tracksPerItem[name][i] = trackInfo;
1506
+ return;
1342
1507
  }
1343
1508
  }
1509
+ }
1510
+
1511
+ onUpdateTracks ( keyType ) {
1512
+
1513
+ if(this.selectedItems == null || this.lastKeyFramesSelected.length || !this.autoKeyEnabled)
1514
+ return;
1515
+
1516
+ for(let i = 0; i < this.selectedItems.length; i++) {
1517
+ let tracks = this.tracksPerItem[this.selectedItems[i]];
1518
+ if(!tracks) continue;
1519
+
1520
+ // Get current track
1521
+ const selectedTrackIdx = tracks.findIndex( t => t.type === keyType );
1522
+ if(selectedTrackIdx < 0)
1523
+ return;
1524
+ let track = tracks[ selectedTrackIdx ];
1525
+
1526
+ // Add new keyframe
1527
+ const newIdx = this.addKeyFrame( track );
1528
+ if(newIdx === null)
1529
+ continue;
1530
+
1531
+ // Select it
1532
+ this.lastKeyFramesSelected.push( [track.name, track.idx, newIdx] );
1533
+ track.selected[newIdx] = true;
1534
+
1535
+ }
1344
1536
 
1345
- this.draw();
1537
+ LX.emit( "@on_current_time_" + this.constructor.name, this.currentTime );
1538
+ return true; // Handled
1539
+ }
1540
+
1541
+
1542
+
1543
+ updateTrack(trackIdx, track) {
1544
+ if(!this.animationClip)
1545
+ return;
1546
+ this.animationClip.tracks[trackIdx].values = track.values;
1547
+ this.animationClip.tracks[trackIdx].times = track.times;
1548
+ this.processTrack(trackIdx);
1346
1549
 
1347
- if(this.onChangeTrackDisplay)
1348
- this.onChangeTrackDisplay(trackInfo, hide)
1550
+ }
1551
+
1552
+ optimizeTrack(trackIdx) {
1553
+ const track = this.animationClip.tracks[trackIdx];
1554
+ if(track.optimize) {
1555
+ track.optimize( this.optimizeThreshold );
1556
+ if(this.onOptimizeTracks) {
1557
+ this.onOptimizeTracks(trackIdx);
1558
+ }
1559
+ }
1560
+ }
1561
+
1562
+ optimizeTracks() {
1563
+
1564
+ if(!this.animationClip) {
1565
+ return;
1566
+ }
1567
+
1568
+ for( let i = 0; i < this.animationClip.tracks.length; ++i ) {
1569
+ const track = this.animationClip.tracks[i];
1570
+ if(track.optimize) {
1571
+
1572
+ track.optimize( this.optimizeThreshold );
1573
+ if(this.onOptimizeTracks) {
1574
+ this.onOptimizeTracks(i);
1575
+ }
1576
+ }
1577
+ }
1349
1578
  }
1350
1579
 
1351
1580
  /**
1352
- * @method resize
1353
- * @param {*} size
1354
- *
1355
- *
1581
+ * @method setSelectedItems
1582
+ * @param {Array} itemsName
1356
1583
  */
1357
- resize( size = [this.root.parent.root.clientWidth, this.root.parent.root.clientHeight]) {
1584
+ setSelectedItems( itemsName ) {
1358
1585
 
1359
- // this.root.root.style.width = size[0] + "px";
1360
- // this.root.root.style.height = size[1] + "px";
1361
-
1362
- this.size = size;
1363
- //this.content_area.setSize([size[0], size[1] - this.header_offset]);
1364
- this.content_area.root.style.height = "calc(100% - "+ this.header_offset + "px)";
1586
+ if(itemsName.constructor !== Array)
1587
+ throw("Item name has to be an array!");
1365
1588
 
1366
- let w = size[0] - this.leftPanel.root.clientWidth - 8;
1367
- this.resizeCanvas([w , size[1]]);
1589
+ this.selectedItems = itemsName;
1590
+ this.unSelectAllKeyFrames();
1591
+ this.updateLeftPanel();
1592
+ this.resize();
1368
1593
  }
1369
1594
 
1370
- resizeCanvas( size ) {
1371
- if( size[0] <= 0 && size[1] <=0 )
1595
+ /**
1596
+ * @method getNumTracks
1597
+ * @description Get how many tracks are related to the passed item
1598
+ * @param {Object} item
1599
+ */
1600
+ getNumTracks( item ) {
1601
+ if(!item || !this.tracksPerItem) {
1372
1602
  return;
1373
- if(Math.abs(this.canvas.width - size[0]) > 1) {
1374
-
1375
- var w = Math.max(300, size[0] );
1376
- this.secondsToPixels = ( w- this.session.left_margin ) / this.duration;
1377
- this.pixelsToSeconds = 1 / this.secondsToPixels;
1378
1603
  }
1379
- size[1] -= this.header_offset;
1380
- //this.canvasArea.setSize(size);
1381
- //this.canvasArea.root.style.height = "calc(100% - "+ this.header_offset + "px)";
1382
- this.canvas.width = this.canvasArea.root.clientWidth;
1383
- this.canvas.height = this.canvasArea.root.clientHeight;
1384
- this.draw(this.currentTime);
1604
+ const tracks = this.tracksPerItem[item.name];
1605
+ return tracks ? tracks.length : null;
1385
1606
  }
1386
1607
 
1387
- /**
1388
- * @method hide
1389
- * Hide timeline area
1390
- */
1391
- hide() {
1392
- this.root.hide();
1608
+ /**
1609
+ * @method getTrack
1610
+ * @description Get track given the item's name and track's id
1611
+ * @param {Object} trackInfo {name, track idx}
1612
+ * @returns {Object} track
1613
+ */
1614
+ getTrack( trackInfo ) {
1615
+ const [name, trackIndex] = trackInfo;
1616
+ return this.tracksPerItem[name][trackIndex];
1393
1617
  }
1394
1618
 
1395
1619
  /**
1396
- * @method show
1397
- * Show timeline area if it is hidden
1398
- */
1399
- show() {
1400
-
1401
- this.root.show();
1402
- this.updateLeftPanel();
1403
- this.resize();
1620
+ * @method getKeyFrame
1621
+ * @description Get the keyframe in the given time. If there isn't any in that timem it returns null
1622
+ * @param {Object} track {times, values, ...}
1623
+ * @param {Number} time current time
1624
+ * @param {Number} threshold
1625
+ * @returns {Number or Null} keyframe index
1626
+ */
1627
+ getKeyFrame( track, time, threshold ) {
1628
+
1629
+ if(!track || !track.times.length)
1630
+ return;
1631
+
1632
+ // Avoid iterating through all timestamps
1633
+ if((time + threshold) < track.times[0])
1634
+ return;
1635
+
1636
+ for(let i = 0; i < track.times.length; ++i) {
1637
+ let t = track.times[i];
1638
+ if(t >= (time - threshold) &&
1639
+ t <= (time + threshold)) {
1640
+ return i;
1641
+ }
1642
+ }
1643
+ return;
1404
1644
  }
1405
- };
1406
1645
 
1407
- Timeline.BACKGROUND_COLOR = LX.getThemeColor("global-color-primary");
1408
- Timeline.TRACK_COLOR_PRIMARY = LX.getThemeColor("global-blur-background");
1409
- Timeline.TRACK_COLOR_SECONDARY = LX.getThemeColor("global-color-terciary");
1410
- Timeline.TRACK_SELECTED = LX.getThemeColor("global-selected");
1411
- Timeline.TRACK_SELECTED_LIGHT = LX.getThemeColor("global-selected-light");
1412
- Timeline.FONT = LX.getThemeColor("global-font");
1413
- Timeline.FONT_COLOR = LX.getThemeColor("global-text");
1414
- Timeline.COLOR = LX.getThemeColor("global-selected-dark");//"#5e9fdd";
1415
- Timeline.COLOR_HOVERED = LX.getThemeColor("global-selected");
1416
- Timeline.COLOR_SELECTED = "rgba(250,250,20,1)"///"rgba(250,250,20,1)";
1417
- Timeline.COLOR_UNACTIVE = "rgba(250,250,250,0.7)";
1418
- Timeline.COLOR_LOCK = "rgba(255,125,125,0.7)";
1419
- Timeline.COLOR_EDITED = "white"//"rgba(125,250,250, 1)";
1646
+ /**
1647
+ * @method getNearestKeyFrame
1648
+ * @description Get the nearest keyframe in the given time. It always returns a keyframe index
1649
+ * @param {Object} track {times, values, ...}
1650
+ * @param {Number} time current time
1651
+ * @param {Number} threshold
1652
+ * @returns {Number} keyframe index
1653
+ */
1654
+ getNearestKeyFrame( track, time ) {
1420
1655
 
1421
- LX.Timeline = Timeline;
1656
+ if(!track || !track.times.length) {
1657
+ return;
1658
+ }
1422
1659
 
1423
- /**
1424
- * @class KeyFramesTimeline
1425
- */
1660
+ return track.times.reduce((a, b) => {
1661
+ return Math.abs(b - time) < Math.abs(a - time) ? b : a;
1662
+ });
1663
+ }
1664
+
1665
+ /**
1666
+ * @method getKeyFramesInRange
1667
+ * @description Get the keyframes inside a time range
1668
+ * @param {Object} track {times, values, ...}
1669
+ * @param {Number} minTime minimum time in the range
1670
+ * @param {Number} maxTime maximum time in the range
1671
+ * @param {Number} threshold
1672
+ * @returns {Number} keyframe index
1673
+ */
1674
+ getKeyFramesInRange( track, minTime, maxTime, threshold ) {
1675
+
1676
+ if(!track || !track.times.length)
1677
+ return;
1678
+
1679
+ // Manage negative selection
1680
+ if(minTime > maxTime) {
1681
+ let aux = minTime;
1682
+ minTime = maxTime;
1683
+ maxTime = aux;
1684
+ }
1685
+
1686
+ // Avoid iterating through all timestamps
1687
+ if((maxTime + threshold) < track.times[0])
1688
+ return;
1689
+
1690
+ let indices = [];
1426
1691
 
1427
- class KeyFramesTimeline extends Timeline {
1692
+ for(let i = 0; i < track.times.length; ++i) {
1693
+ let t = track.times[i];
1694
+ if(t >= (minTime - threshold) &&
1695
+ t <= (maxTime + threshold)) {
1696
+ indices.push(i);
1697
+ }
1698
+ }
1428
1699
 
1429
- /**
1430
- * @param {string} name
1431
- * @param {object} options = {animationClip, selectedItems, x, y, width, height, canvas, trackHeight}
1700
+ return indices;
1701
+ }
1702
+
1703
+ /**
1704
+ * @method getNumKeyFramesSelected
1705
+ * @description Get the number of keyframes that are selected
1706
+ * @returns {Number} number of selected keyframes
1432
1707
  */
1433
- constructor(name, options = {}) {
1708
+ getNumKeyFramesSelected() {
1709
+ return this.lastKeyFramesSelected.length;
1710
+ }
1711
+
1712
+ /**
1713
+ * @override
1714
+ * @method addNewTrack
1715
+ * @description Add empty new track
1716
+ */
1717
+ addNewTrack() {
1434
1718
 
1435
- super(name, options);
1436
-
1437
- this.tracksPerItem = {};
1438
-
1439
- // this.selectedItems = selectedItems;
1440
- this.snappedKeyFrameIndex = -1;
1441
- this.autoKeyEnabled = false;
1719
+ if(!this.animationClip)
1720
+ this.animationClip = {tracks:[]};
1442
1721
 
1722
+ let trackInfo = {
1723
+ idx: this.animationClip.tracks.length,
1724
+ values: [], times: [],
1725
+ selected: [], edited: [], hovered: []
1726
+ };
1443
1727
 
1444
- if(this.animationClip && this.animationClip.tracks.length) {
1445
- this.processTracks(this.animationClip);
1446
- }
1728
+ this.animationClip.tracks.push(trackInfo);
1729
+ this.updateLeftPanel();
1730
+ return trackInfo.idx;
1447
1731
  }
1448
1732
 
1449
- onMouseUp( e, time ) {
1733
+ onMouseUp( e ) {
1450
1734
 
1451
- let track = e.track;
1452
- let localX = e.localX;
1453
- let discard = e.discard;
1735
+ const track = e.track;
1736
+ const localX = e.localX;
1737
+ const discard = e.discard;
1454
1738
 
1455
1739
  if(e.shiftKey) {
1456
1740
  e.multipleSelection = true;
@@ -1459,7 +1743,7 @@ class KeyFramesTimeline extends Timeline {
1459
1743
  this.processCurrentKeyFrame( e, null, track, localX, true );
1460
1744
  }
1461
1745
  // Box selection
1462
- else if(this.boxSelection) {
1746
+ else if(this.boxSelectionEnd) {
1463
1747
 
1464
1748
  this.unSelectAllKeyFrames();
1465
1749
 
@@ -1478,7 +1762,8 @@ class KeyFramesTimeline extends Timeline {
1478
1762
  }
1479
1763
  }
1480
1764
 
1481
- }else {
1765
+ }
1766
+ else {
1482
1767
  let boundingBox = this.canvas.getBoundingClientRect();
1483
1768
  if(e.y < boundingBox.top || e.y > boundingBox.bottom) {
1484
1769
  return;
@@ -1499,10 +1784,10 @@ class KeyFramesTimeline extends Timeline {
1499
1784
  this.boxSelectionEnd = null;
1500
1785
  }
1501
1786
 
1502
- onMouseDown( e, time ) {
1503
- let localX = e.localX;
1504
- let localY = e.localY;
1505
- let track = e.track;
1787
+ onMouseDown( e ) {
1788
+ const localX = e.localX;
1789
+ const localY = e.localY;
1790
+ const track = e.track;
1506
1791
 
1507
1792
  if(e.shiftKey) {
1508
1793
 
@@ -1511,9 +1796,9 @@ class KeyFramesTimeline extends Timeline {
1511
1796
  e.multipleSelection = true;
1512
1797
  }
1513
1798
  else if(track && !track.locked) {
1514
- const keyFrameIndex = this.getCurrentKeyFrame( track, this.xToTime( localX ), this.pixelsToSeconds * 5 );
1799
+ const keyFrameIndex = this.getKeyFrame( track, this.xToTime( localX ), this.pixelsToSeconds * 5 );
1515
1800
  if( keyFrameIndex != undefined ) {
1516
- this.processCurrentKeyFrame( e, keyFrameIndex, track, null, e.multipleSelection ); // Settings this as multiple so time is not being set
1801
+ this.processCurrentKeyFrame( e, keyFrameIndex, track, null, this.lastKeyFramesSelected.length > 1 || e.multipleSelection ); // Settings this as multiple so time is not being set
1517
1802
  if(e.ctrlKey ) {
1518
1803
 
1519
1804
  this.movingKeys = true;
@@ -1530,82 +1815,104 @@ class KeyFramesTimeline extends Timeline {
1530
1815
  this.timeBeforeMove = track.times[ keyFrameIndex ];
1531
1816
  }
1532
1817
  }
1818
+ else if(!track) {
1819
+ this.unSelectAllKeyFrames()
1820
+ }
1533
1821
  }
1534
1822
 
1535
- onMouseMove( e, time ) {
1823
+ onMouseMove( e ) {
1536
1824
 
1537
- let localX = e.localX;
1538
- let localY = e.localY;
1825
+ const localX = e.localX;
1539
1826
  let track = e.track;
1540
1827
 
1541
- const innerSetTime = (t) => {
1542
- LX.emit( "@on_current_time_" + this.constructor.name, t);
1543
- if( this.onSetTime )
1544
- this.onSetTime( t );
1545
- }
1546
1828
  // Manage keyframe movement
1547
1829
  if(this.movingKeys) {
1548
1830
 
1549
- this.clearState();
1831
+ //this.clearState();
1550
1832
  const newTime = this.xToTime( localX );
1551
1833
 
1834
+ // Update times of the selected frames
1552
1835
  for(let [name, idx, keyIndex, keyTime] of this.lastKeyFramesSelected) {
1553
1836
  track = this.tracksPerItem[name][idx];
1554
1837
  if(track && track.locked)
1555
- return;
1838
+ continue;
1556
1839
 
1557
1840
  this.canvas.style.cursor = "grabbing";
1558
1841
 
1559
1842
  const delta = this.timeBeforeMove - keyTime;
1560
1843
  this.animationClip.tracks[ track.clipIdx ].times[ keyIndex ] = Math.min( this.animationClip.duration, Math.max(0, newTime - delta) );
1561
1844
  }
1845
+
1846
+ // Reorder track keyframes if necessary
1847
+ for( let i = 0; i < this.lastKeyFramesSelected.length; ++i ){
1848
+ let s = this.lastKeyFramesSelected[i]; // pointer
1849
+ const name = s[0], localTrackIdx = s[1], keyTime = s[3];
1850
+ track = this.tracksPerItem[name][localTrackIdx];
1851
+
1852
+ if(track && track.locked) {
1853
+ continue;
1854
+ }
1562
1855
 
1856
+ let times = track.times;
1857
+ let k = s[2];
1858
+ for( ; k > 0; --k ){
1859
+ if ( times[k-1] < times[k] ){
1860
+ break;
1861
+ }
1862
+ this.swapKeyFrames(track, k-1, k);
1863
+ }
1864
+ for( ; k < track.times.length-1; ++k ){
1865
+ if ( times[k+1] > times[k] ){
1866
+ break;
1867
+ }
1868
+ this.swapKeyFrames(track, k+1, k);
1869
+ }
1870
+ s[2] = k; // "s" is a pointer. Modify selected keyFrame index
1871
+ }
1563
1872
  return;
1564
1873
  }
1565
1874
 
1566
- const removeHover = () => {
1567
- if(this.lastHovered)
1568
- this.tracksPerItem[ this.lastHovered[0] ][ this.lastHovered[1] ].hovered[ this.lastHovered[2] ] = undefined;
1569
- };
1570
-
1571
1875
  if( this.grabbing && e.button != 2) {
1572
-
1573
1876
  // fix this
1574
- if(e.shiftKey && track) {
1575
-
1877
+ if(e.ctrlKey && track) {
1576
1878
  let keyFrameIndex = this.getNearestKeyFrame( track, this.currentTime);
1577
-
1879
+
1578
1880
  if(keyFrameIndex != this.snappedKeyFrameIndex){
1579
1881
  this.snappedKeyFrameIndex = keyFrameIndex;
1580
1882
  this.currentTime = track.times[ keyFrameIndex ];
1581
- innerSetTime( this.currentTime );
1883
+ LX.emit( "@on_current_time_" + this.constructor.name, this.currentTime);
1582
1884
  }
1583
1885
  }
1584
- else{
1585
- innerSetTime( this.currentTime );
1886
+ else {
1887
+
1888
+ LX.emit( "@on_current_time_" + this.constructor.name, this.currentTime);
1889
+
1586
1890
  }
1587
-
1588
1891
  }
1892
+
1893
+
1894
+ // Check if a keyframe is hovered
1589
1895
  else if(track) {
1590
1896
 
1591
- let keyFrameIndex = this.getCurrentKeyFrame( track, this.xToTime( localX ), this.pixelsToSeconds * 5 );
1897
+ const keyFrameIndex = this.getKeyFrame( track, this.xToTime( localX ), this.pixelsToSeconds * 5 );
1592
1898
  if(keyFrameIndex != undefined) {
1593
1899
 
1594
1900
  const name = this.tracksDictionary[track.fullname];
1595
1901
  let t = this.tracksPerItem[ name ][track.idx];
1596
1902
  if(t && t.locked)
1597
1903
  return;
1598
- removeHover();
1904
+ this.unHoverAll();
1599
1905
 
1600
1906
  this.lastHovered = [name, track.idx, keyFrameIndex];
1601
1907
  t.hovered[keyFrameIndex] = true;
1602
1908
 
1603
- }else {
1604
- removeHover();
1909
+ }
1910
+ else {
1911
+ this.unHoverAll();
1605
1912
  }
1606
1913
  }
1607
1914
  else {
1608
- removeHover();
1915
+ this.unHoverAll();
1609
1916
  }
1610
1917
  }
1611
1918
 
@@ -1615,7 +1922,6 @@ class KeyFramesTimeline extends Timeline {
1615
1922
  e.stopPropagation();
1616
1923
 
1617
1924
  let actions = [];
1618
- //let track = this.NMFtimeline.clip.tracks[0];
1619
1925
  if(this.lastKeyFramesSelected && this.lastKeyFramesSelected.length) {
1620
1926
  if(this.lastKeyFramesSelected.length == 1 && this.clipboard && this.clipboard.value)
1621
1927
  {
@@ -1632,8 +1938,7 @@ class KeyFramesTimeline extends Timeline {
1632
1938
  {
1633
1939
  title: "Copy",// + " <i class='bi bi-clipboard-fill float-right'></i>",
1634
1940
  callback: () => {
1635
- this.copyContent();
1636
-
1941
+ this.copyContent(); // copy value and keyframes selected
1637
1942
  }
1638
1943
  }
1639
1944
  )
@@ -1641,7 +1946,7 @@ class KeyFramesTimeline extends Timeline {
1641
1946
  {
1642
1947
  title: "Delete",// + " <i class='bi bi-trash float-right'></i>",
1643
1948
  callback: () => {
1644
- this.deleteKeyFrame({});
1949
+ this.deleteContent({}); // TODO multipleSelection
1645
1950
  }
1646
1951
  }
1647
1952
  )
@@ -1677,30 +1982,34 @@ class KeyFramesTimeline extends Timeline {
1677
1982
 
1678
1983
  }
1679
1984
 
1680
- onDrawContent( ctx, timeStart, timeEnd ) {
1985
+ drawContent( ctx ) {
1681
1986
 
1682
- if(this.selectedItems == null || !this.tracksPerItem)
1987
+ if(this.selectedItems == null || !this.tracksPerItem) {
1683
1988
  return;
1989
+ }
1684
1990
 
1685
1991
  ctx.save();
1686
1992
  this.scrollableHeight = this.topMargin;
1687
-
1993
+ const height = this.trackHeight;
1688
1994
  let offset = this.trackHeight;
1995
+ let scroll_y = - this.currentScrollInPixels;
1996
+ this.tracksDrawn = [];
1997
+
1689
1998
  for(let t = 0; t < this.selectedItems.length; t++) {
1690
1999
  let tracks = this.tracksPerItem[this.selectedItems[t]] ? this.tracksPerItem[this.selectedItems[t]] : [{name: this.selectedItems[t]}];
1691
- if(!tracks) continue;
2000
+ if(!tracks) {
2001
+ continue;
2002
+ }
1692
2003
 
1693
- const height = this.trackHeight;
1694
- this.scrollableHeight += (tracks.length+1)*height;
1695
- let scroll_y = - this.currentScrollInPixels;
2004
+ this.scrollableHeight += (tracks.length + 1) * height;
1696
2005
 
1697
2006
  let offsetI = 0;
1698
2007
  for(let i = 0; i < tracks.length; i++) {
1699
- let track = tracks[i];
2008
+ const track = tracks[i];
1700
2009
  if(track.hide) {
1701
2010
  continue;
1702
2011
  }
1703
- this.drawTrackWithKeyframes(ctx, offsetI * height + offset + scroll_y, height, track.name + " (" + track.type + ")", this.animationClip.tracks[track.clipIdx], track);
2012
+ this.#drawTrackWithKeyframes(ctx, offsetI * height + offset + scroll_y, height, this.animationClip.tracks[track.clipIdx], track);
1704
2013
  offsetI++;
1705
2014
  }
1706
2015
  offset += offsetI * height + height;
@@ -1709,158 +2018,91 @@ class KeyFramesTimeline extends Timeline {
1709
2018
  ctx.restore();
1710
2019
  };
1711
2020
 
1712
- onUpdateTracks ( keyType ) {
1713
-
1714
- if(this.selectedItems == null || this.lastKeyFramesSelected.length || !this.autoKeyEnabled)
1715
- return;
1716
-
1717
- for(let i = 0; i < this.selectedItems.length; i++) {
1718
- let tracks = this.tracksPerItem[this.selectedItems[i]];
1719
- if(!tracks) continue;
1720
-
1721
- // Get current track
1722
- const selectedTrackIdx = tracks.findIndex( t => t.type === keyType );
1723
- if(selectedTrackIdx < 0)
1724
- return;
1725
- let track = tracks[ selectedTrackIdx ];
1726
-
1727
- // Add new keyframe
1728
- const newIdx = this.addKeyFrame( track );
1729
- if(newIdx === null)
1730
- continue;
1731
-
1732
- // Select it
1733
- this.lastKeyFramesSelected.push( [track.name, track.idx, newIdx] );
1734
- track.selected[newIdx] = true;
2021
+ /**
2022
+ * @method drawTrackWithKeyframes
2023
+ * @param {*} ctx
2024
+ * ...
2025
+ * @description helper function, you can call it from drawContent to render all the keyframes
2026
+ * TODO
2027
+ */
1735
2028
 
2029
+ #drawTrackWithKeyframes( ctx, y, trackHeight, track, trackInfo ) {
2030
+
2031
+ if(trackInfo.enabled === false) {
2032
+ ctx.globalAlpha = 0.4;
1736
2033
  }
1737
- //LX.emit( "@on_current_time_" + this.constructor.name, this.currentTime );
1738
- // Update time
1739
- if(this.onSetTime)
1740
- this.onSetTime(this.currentTime);
1741
-
1742
- return true; // Handled
1743
- }
1744
-
1745
- // Creates a map for each item -> tracks
1746
- processTracks(animation) {
1747
2034
 
1748
- this.tracksPerItem = {};
1749
- this.tracksDictionary = {};
1750
- this.animationClip = {
1751
- name: (animation && animation.name) ? animation.name : "animationClip",
1752
- duration: animation ? animation.duration : 0,
1753
- speed: (animation && animation.speed ) ? animation.speed : this.speed,
1754
- tracks: []
1755
- };
1756
- if (animation && animation.tracks) {
1757
- for( let i = 0; i < animation.tracks.length; ++i ) {
1758
-
1759
- let track = animation.tracks[i];
1760
-
1761
- const [name, type] = this.getTrackName(track.name);
1762
-
1763
- let trackInfo = {
1764
- fullname: track.name,
1765
- name: name, type: type,
1766
- dim: track.values.length/track.times.length,
1767
- selected: [], edited: [], hovered: [], active: true,
1768
- times: track.times,
1769
- values: track.values
1770
- };
1771
-
1772
- if(!this.tracksPerItem[name]) {
1773
- this.tracksPerItem[name] = [trackInfo];
1774
- }else {
1775
- this.tracksPerItem[name].push( trackInfo );
1776
- }
1777
-
1778
-
1779
- const trackIndex = this.tracksPerItem[name].length - 1;
1780
- this.tracksPerItem[name][trackIndex].idx = trackIndex;
1781
- this.tracksPerItem[name][trackIndex].clipIdx = i;
1782
-
1783
- // Save index also in original track
1784
- track.idx = trackIndex;
1785
- this.tracksDictionary[track.name] = name;
1786
-
1787
- this.animationClip.tracks.push(trackInfo);
1788
- }
2035
+ ctx.font = Math.floor( trackHeight * 0.8) + "px" + Timeline.FONT;
2036
+ ctx.textAlign = "left";
2037
+
2038
+ ctx.globalAlpha = 0.2;
2039
+
2040
+ if(trackInfo.isSelected) {
2041
+ ctx.fillStyle = Timeline.TRACK_SELECTED;//"#2c303570";
2042
+ ctx.fillRect(0, y, ctx.canvas.width, trackHeight );
1789
2043
  }
1790
- this.resize();
1791
- }
1792
-
1793
- updateTrack(trackIdx, track) {
1794
- if(!this.animationClip)
1795
- return;
1796
- this.animationClip.tracks[trackIdx].values = track.values;
1797
- this.animationClip.tracks[trackIdx].times = track.times;
1798
- this.processTrack(trackIdx);
1799
-
1800
- }
2044
+ ctx.fillStyle = Timeline.COLOR;//"#2c303570";
2045
+ ctx.globalAlpha = 1;
2046
+
2047
+ let keyframes = track.times;
1801
2048
 
1802
- processTrack(trackIdx) {
1803
- if(!this.animationClip)
2049
+ if(!keyframes) {
1804
2050
  return;
1805
-
1806
- let track = this.animationClip.tracks[trackIdx];
1807
-
1808
- const [name, type] = this.getTrackName(track.fullname || track.name);
1809
-
1810
- let trackInfo = {
1811
- fullname: track.name,
1812
- name: name, type: type,
1813
- dim: track.values.length/track.times.length,
1814
- selected: [], edited: [], hovered: [], active: true,
1815
- times: track.times,
1816
- values: track.values
1817
- };
2051
+ }
1818
2052
 
1819
- for(let i = 0; i < this.tracksPerItem[name].length; i++) {
1820
- if(this.tracksPerItem[name][i].fullname == trackInfo.fullname) {
1821
- trackInfo.idx = this.tracksPerItem[name][i].idx;
1822
- trackInfo.clipIdx = this.tracksPerItem[name][i].clipIdx;
1823
- this.tracksPerItem[name][i] = trackInfo;
1824
- return;
2053
+ this.tracksDrawn.push([track,y+this.topMargin,trackHeight]);
2054
+
2055
+ for(let j = 0; j < keyframes.length; ++j)
2056
+ {
2057
+ let time = keyframes[j];
2058
+ let selected = trackInfo.selected[j];
2059
+ if( time < this.startTime || time > this.endTime ) {
2060
+ continue;
1825
2061
  }
1826
- }
1827
- }
1828
-
1829
- optimizeTrack(trackIdx) {
1830
- const track = this.animationClip.tracks[trackIdx];
1831
- if(track.optimize) {
1832
-
1833
- track.optimize( this.optimizeThreshold );
1834
- if(this.onOptimizeTracks)
1835
- this.onOptimizeTracks(trackIdx);
1836
- }
1837
- }
1838
-
1839
- optimizeTracks() {
2062
+ const keyframePosX = this.timeToX( time );
2063
+ ctx.save();
1840
2064
 
1841
- if(!this.animationClip)
1842
- return;
2065
+ let margin = -1;
2066
+ let size = trackHeight * 0.3;
2067
+
2068
+ if(trackInfo.edited[j]) {
2069
+ ctx.fillStyle = Timeline.COLOR_EDITED;
2070
+ }
2071
+
2072
+ if(selected) {
2073
+ ctx.fillStyle = Timeline.COLOR_SELECTED;
2074
+ ctx.shadowBlur = 8;
2075
+ size = trackHeight * 0.35;
2076
+ margin = 0;
2077
+ }
1843
2078
 
1844
- for( let i = 0; i < this.animationClip.tracks.length; ++i ) {
1845
- const track = this.animationClip.tracks[i];
1846
- if(track.optimize) {
2079
+ if(trackInfo.hovered[j]) {
2080
+ size = trackHeight * 0.35;
2081
+ ctx.fillStyle = Timeline.COLOR_HOVERED;
2082
+ ctx.shadowBlur = 8;
2083
+ margin = 0;
2084
+ }
2085
+ if(trackInfo.locked) {
2086
+ ctx.fillStyle = Timeline.COLOR_LOCK;
2087
+ }
1847
2088
 
1848
- track.optimize( this.optimizeThreshold );
1849
- if(this.onOptimizeTracks)
1850
- this.onOptimizeTracks(i);
2089
+ if(!this.active || trackInfo.active == false) {
2090
+ ctx.fillStyle = Timeline.COLOR_UNACTIVE;
1851
2091
  }
2092
+
2093
+ ctx.translate(keyframePosX, y + this.trackHeight * 0.75 + margin);
2094
+ ctx.rotate(45 * Math.PI / 180);
2095
+ ctx.fillRect( -size, -size, size, size);
2096
+ if(selected) {
2097
+ ctx.globalAlpha = 0.3;
2098
+ ctx.fillRect( -size*1.1, -size*1.1, size*1.2, size*1.2);
2099
+ }
2100
+ ctx.shadowBlur = 0;
2101
+ ctx.restore();
1852
2102
  }
2103
+ ctx.globalAlpha = this.opacity;
1853
2104
  }
1854
2105
 
1855
-
1856
- getNumTracks( item ) {
1857
- if(!item || !this.tracksPerItem)
1858
- return;
1859
- const tracks = this.tracksPerItem[item.name];
1860
- return tracks ? tracks.length : null;
1861
- }
1862
-
1863
-
1864
2106
  onShowOptimizeMenu( e ) {
1865
2107
 
1866
2108
  if(this.selectedItems == null)
@@ -1889,28 +2131,19 @@ class KeyFramesTimeline extends Timeline {
1889
2131
  });
1890
2132
  }
1891
2133
 
1892
- onPreProcessTrack( track ) {
1893
- const name = this.tracksDictionary[track.fullname];
1894
- let trackInfo = this.tracksPerItem[name][track.idx];
1895
- trackInfo.selected = [];
1896
- trackInfo.edited = [];
1897
- trackInfo.hovered = [];
1898
- }
1899
-
1900
2134
  isKeyFrameSelected( track, index ) {
1901
2135
  return track.selected[ index ];
1902
2136
  }
1903
2137
 
1904
- saveState( clipIdx ) {
1905
-
1906
- const localIdx = this.animationClip.tracks[clipIdx].idx;
1907
- const name = this.getTrackName(this.animationClip.tracks[clipIdx].name)[0];
1908
- const trackInfo = this.tracksPerItem[name][localIdx];
1909
-
2138
+ /**
2139
+ * @param {Number} trackIdx index of track in the animation (not local index)
2140
+ */
2141
+ saveState( trackIdx ) {
2142
+ const trackInfo = this.animationClip.tracks[trackIdx];
1910
2143
  this.trackState.push({
1911
- idx: clipIdx,
1912
- t: this.animationClip.tracks[clipIdx].times.slice(),
1913
- v: this.animationClip.tracks[clipIdx].values.slice(),
2144
+ idx: trackIdx,
2145
+ t: trackInfo.times.slice(),
2146
+ v: trackInfo.values.slice(),
1914
2147
  editedTracks: [].concat(trackInfo.edited)
1915
2148
  });
1916
2149
  }
@@ -1925,7 +2158,7 @@ class KeyFramesTimeline extends Timeline {
1925
2158
  this.animationClip.tracks[state.idx].values = state.v;
1926
2159
 
1927
2160
  const localIdx = this.animationClip.tracks[state.idx].idx;
1928
- const name = this.getTrackName(this.animationClip.tracks[state.idx].name)[0];
2161
+ const name = this.processTrackName(this.animationClip.tracks[state.idx].name)[0];
1929
2162
  this.tracksPerItem[name][localIdx].edited = state.editedTracks;
1930
2163
 
1931
2164
  // Update animation action interpolation info
@@ -1933,6 +2166,43 @@ class KeyFramesTimeline extends Timeline {
1933
2166
  this.onUpdateTrack( state.idx );
1934
2167
  }
1935
2168
 
2169
+ /**
2170
+ *
2171
+ * @param {*} track
2172
+ * @param {Number} srcIdx keyFrame index
2173
+ * @param {Number} trgIdx keyFrame index
2174
+ */
2175
+ swapKeyFrames(track, srcIdx, trgIdx){
2176
+ let times = track.times;
2177
+ let values = track.values;
2178
+
2179
+ let tmp = times[srcIdx];
2180
+ times[srcIdx] = times[trgIdx];
2181
+ times[trgIdx] = tmp;
2182
+
2183
+ tmp = track.hovered[srcIdx];
2184
+ track.hovered[srcIdx] = track.hovered[trgIdx];
2185
+ track.hovered[trgIdx] = tmp;
2186
+
2187
+ tmp = track.edited[srcIdx];
2188
+ track.edited[srcIdx] = track.edited[trgIdx];
2189
+ track.edited[trgIdx] = tmp;
2190
+
2191
+ tmp = track.selected[srcIdx];
2192
+ track.selected[srcIdx] = track.selected[trgIdx];
2193
+ track.selected[trgIdx] = tmp;
2194
+
2195
+ let src = srcIdx * track.dim;
2196
+ let end = src + track.dim;
2197
+ let trg = trgIdx * track.dim;
2198
+ for( ; src < end; ++src ){
2199
+ tmp = values[ src ];
2200
+ values[ src ] = values[ trg ];
2201
+ values[ trg ] = tmp;
2202
+ ++trg;
2203
+ }
2204
+ }
2205
+
1936
2206
  selectKeyFrame( track, selectionInfo, index ) {
1937
2207
 
1938
2208
  if(index == undefined || !track)
@@ -1945,39 +2215,43 @@ class KeyFramesTimeline extends Timeline {
1945
2215
  return;
1946
2216
  track.selected[index] = true;
1947
2217
  this.currentTime = this.animationClip.tracks[track.clipIdx].times[ index ];
1948
- LX.emit( "@on_current_time_" + this.constructor.name, this.currentTime );
1949
- if( this.onSetTime )
1950
- this.onSetTime( this.currentTime);
2218
+ LX.emit( "@on_current_time_" + this.constructor.name, this.currentTime );
1951
2219
  }
1952
2220
 
1953
2221
  copyContent() {
2222
+ if (!this.lastKeyFramesSelected.length){
2223
+ return;
2224
+ }
2225
+
2226
+ // sort keyframes selected by track
1954
2227
  let toCopy = {};
1955
2228
  for(let i = 0; i < this.lastKeyFramesSelected.length; i++){
1956
- let [id, trackIdx, keyIdx] = this.lastKeyFramesSelected[i];
1957
- if(toCopy[this.tracksPerItem[id][trackIdx].clipIdx]) {
1958
- toCopy[this.tracksPerItem[id][trackIdx].clipIdx].idxs.push(keyIdx);
2229
+ let [id, localTrackIdx, keyIdx] = this.lastKeyFramesSelected[i];
2230
+ let track = this.tracksPerItem[id][localTrackIdx];
2231
+ let trackIdx = track.clipIdx;
2232
+
2233
+ if(toCopy[trackIdx]) {
2234
+ toCopy[trackIdx].idxs.push(keyIdx);
1959
2235
  } else {
1960
- toCopy[this.tracksPerItem[id][trackIdx].clipIdx] = {idxs : [keyIdx]};
1961
- toCopy[this.tracksPerItem[id][trackIdx].clipIdx].track = this.tracksPerItem[id][trackIdx]
1962
- }
2236
+ toCopy[trackIdx] = {track: track, idxs : [keyIdx]};
2237
+ }
1963
2238
  if(i == 0) {
1964
- this.copyKeyFrameValue(this.tracksPerItem[id][trackIdx], keyIdx)
2239
+ this.copyKeyFrameValue(track, keyIdx);
1965
2240
  }
1966
2241
  }
1967
- for(let clipIdx in toCopy) {
1968
-
1969
- this.copyKeyFrames(toCopy[clipIdx].track, toCopy[clipIdx].idxs);
2242
+
2243
+ // for each track selected, copy its values
2244
+ for(let trackIdx in toCopy) {
2245
+ this.copyKeyFrames(toCopy[trackIdx].track, toCopy[trackIdx].idxs);
1970
2246
  }
1971
2247
  }
1972
2248
 
2249
+ // copies the current value of the keyframe. This value can be pasted across any track (as long as they are of the same type)
1973
2250
  copyKeyFrameValue( track, index ) {
1974
2251
 
1975
2252
  // 1 element clipboard by now
1976
-
1977
- let values = [];
1978
2253
  let start = index * track.dim;
1979
- for(let i = start; i < start + track.dim; ++i)
1980
- values.push( this.animationClip.tracks[ track.clipIdx ].values[i] );
2254
+ let values = this.animationClip.tracks[ track.clipIdx ].values.slice(start, start + track.dim);
1981
2255
 
1982
2256
  if(!this.clipboard)
1983
2257
  this.clipboard = {};
@@ -1988,45 +2262,49 @@ class KeyFramesTimeline extends Timeline {
1988
2262
  };
1989
2263
  }
1990
2264
 
2265
+ // each track will have its own entry of copied keyframes. When pasting, only the apropiate track's keyframes are pasted
1991
2266
  copyKeyFrames( track, indices ) {
1992
2267
 
1993
- let clipIdx = track.clipIdx;
2268
+ let trackIdx = track.clipIdx;
1994
2269
  if(!this.clipboard)
1995
2270
  this.clipboard = {};
1996
2271
  if(!this.clipboard.keyframes) {
1997
2272
  this.clipboard.keyframes = {};
1998
2273
  }
1999
-
2000
- this.clipboard.keyframes[clipIdx] = { track: track, values:{} };
2274
+
2275
+ this.clipboard.keyframes[trackIdx] = { track: track, values:{}, times:{} };
2276
+
2001
2277
  // 1 element clipboard by now
2002
- for(let idx = 0; idx < indices.length; idx++ ){
2003
- let keyIdx = indices[idx] ;
2004
- let values = [];
2278
+ for(let i = 0; i < indices.length; i++ ){
2279
+ let keyIdx = indices[i];
2005
2280
  let start = keyIdx * track.dim;
2006
- for(let i = start; i < start + track.dim; ++i)
2007
- values.push( this.animationClip.tracks[ clipIdx ].values[i] );
2008
-
2009
- this.clipboard.keyframes[clipIdx].values[indices[idx]] = values;
2281
+ let keyValues = track.values.slice(start, start + track.dim); // copy values into a new array
2282
+ this.clipboard.keyframes[trackIdx].values[keyIdx] = keyValues; // save to clipboard
2283
+ this.clipboard.keyframes[trackIdx].times[keyIdx] = track.times[keyIdx]; // save to clipboard
2010
2284
  };
2011
2285
  }
2012
2286
 
2013
2287
  pasteContent() {
2014
- if(!this.clipboard)
2288
+ if(!this.clipboard) {
2015
2289
  return;
2290
+ }
2016
2291
 
2292
+ // copy the value into the only selected keyframe
2017
2293
  if(this.clipboard.value && this.lastKeyFramesSelected.length == 1) {
2018
2294
 
2019
- let [id, trackIdx, keyIdx] = this.lastKeyFramesSelected[0];
2020
- this.pasteKeyFrameValue({}, this.tracksPerItem[id][trackIdx], keyIdx);
2295
+ let [id, localTrackIdx, keyIdx] = this.lastKeyFramesSelected[0];
2296
+ this.pasteKeyFrameValue({}, this.tracksPerItem[id][localTrackIdx], keyIdx);
2021
2297
  }
2298
+
2299
+ // create new keyframes from the ones copied
2022
2300
  if(this.clipboard.keyframes) {
2023
2301
  let currentTime = this.currentTime;
2024
- for(let clipIdx in this.clipboard.keyframes) {
2025
- let indices = Object.keys( this.clipboard.keyframes[clipIdx].values)
2026
- this.pasteKeyFrames({multipleSelection: this.clipboard.keyframes.length}, clipIdx, indices);
2302
+ for(let trackIdx in this.clipboard.keyframes) {
2303
+ this.pasteKeyFrames({multipleSelection: this.clipboard.keyframes.length}, trackIdx);
2027
2304
  this.currentTime = currentTime;
2028
2305
  }
2029
2306
  }
2307
+ this.clipboard = {};
2030
2308
  }
2031
2309
 
2032
2310
  canPasteKeyFrame () {
@@ -2047,9 +2325,7 @@ class KeyFramesTimeline extends Timeline {
2047
2325
  this.animationClip.tracks[ track.clipIdx ].values[i] = clipboardInfo.values[j];
2048
2326
  ++j;
2049
2327
  }
2050
- LX.emit( "@on_current_time_" + this.constructor.name, this.currentTime);
2051
- if(this.onSetTime)
2052
- this.onSetTime(this.currentTime);
2328
+ LX.emit( "@on_current_time_" + this.constructor.name, this.currentTime);
2053
2329
 
2054
2330
  track.edited[ index ] = true;
2055
2331
  }
@@ -2061,53 +2337,91 @@ class KeyFramesTimeline extends Timeline {
2061
2337
  // Copy to current key
2062
2338
  this.#paste( track, index );
2063
2339
 
2064
- if(!e.multipleSelection)
2065
- return;
2340
+ if(!e || !e.multipleSelection){
2341
+ return;
2342
+ }
2066
2343
 
2067
2344
  // Don't want anything after this
2068
2345
  this.clearState();
2069
2346
 
2070
2347
  // Copy to every selected key
2071
- for(let [name, idx, keyIndex] of this.lastKeyFramesSelected) {
2072
- this.#paste( this.tracksPerItem[name][idx], keyIndex );
2348
+ for(let [name, localTrackIdx, keyIndex] of this.lastKeyFramesSelected) {
2349
+ this.#paste( this.tracksPerItem[name][localTrackIdx], keyIndex );
2073
2350
  }
2074
2351
  }
2075
2352
 
2076
- pasteKeyFrames( e, clipIdx, indices ) {
2353
+ pasteKeyFrames( e, trackIdx ) {
2077
2354
 
2078
- this.saveState(clipIdx);
2355
+ if ( !this.clipboard.keyframes[trackIdx] ){
2356
+ return;
2357
+ }
2358
+ this.saveState(trackIdx);
2359
+
2360
+ let clipboardInfo = this.clipboard.keyframes[trackIdx];
2361
+ let indices = Object.keys(clipboardInfo.values);
2362
+ indices.sort(); // just in case
2079
2363
 
2080
2364
  // Copy to current key
2081
2365
  for(let i = 0; i < indices.length; i++) {
2082
- let value = this.clipboard.keyframes[clipIdx].values[indices[i]];
2366
+ let value = clipboardInfo.values[indices[i]];
2083
2367
  if(typeof value == 'number')
2084
2368
  value = [value];
2085
2369
  if(i > 0) {
2086
- let delta = this.animationClip.tracks[clipIdx].times[indices[i]] - this.animationClip.tracks[clipIdx].times[indices[i-1]];
2370
+ let delta = clipboardInfo.times[indices[i]] - clipboardInfo.times[indices[i-1]];
2087
2371
  this.currentTime += delta;
2088
-
2089
2372
  }
2090
- this.addKeyFrame( this.clipboard.keyframes[clipIdx].track, value);
2373
+ this.addKeyFrame( clipboardInfo.track, value);
2091
2374
  }
2092
2375
 
2093
- if(!e.multipleSelection)
2094
- return;
2376
+ // if(!e.multipleSelection)
2377
+ // return;
2095
2378
 
2096
- // Don't want anything after this
2097
- this.clearState();
2379
+ // // Don't want anything after this
2380
+ // this.clearState();
2098
2381
 
2099
- // Copy to every selected key
2100
- for(let [name, idx, keyIndex] of this.lastKeyFramesSelected) {
2101
- this.#paste( this.tracksPerItem[name][idx], keyIndex );
2102
- }
2382
+ // // Copy to every selected key
2383
+ // for(let [name, idx, keyIndex] of this.lastKeyFramesSelected) {
2384
+ // this.#paste( this.tracksPerItem[name][idx], keyIndex );
2385
+ // }
2103
2386
  }
2104
2387
 
2388
+ /**
2389
+ * @method updateSelectedKeyframe
2390
+ * @description Updates the value of the current selected keyframe
2391
+ * @param {Number or Array} value is a number or an array depending of the dimensions of the value
2392
+ */
2393
+ updateSelectedKeyframe(value) {
2394
+ if(value.constructor != Array) {
2395
+ value = [value];
2396
+ }
2397
+ if(!this.animationClip || !this.lastKeyFramesSelected.length || this.lastKeyFramesSelected.length > 1) {
2398
+ return;
2399
+ }
2400
+ const trackIdx = this.lastKeyFramesSelected[0][1];
2401
+ const keyframe = this.lastKeyFramesSelected[0][2];
2402
+ const dim = this.animationClip.tracks[trackIdx].dim;
2403
+
2404
+ if(dim != value.length) {
2405
+ return;
2406
+ }
2407
+
2408
+ for(let i = 0; i < dim; i++) {
2409
+ this.animationClip.tracks[trackIdx].values[keyframe*dim + i] = value[i];
2410
+ }
2411
+
2412
+ this.processTrack(trackIdx);
2413
+ }
2414
+
2105
2415
  addKeyFrame( track, value = undefined, time = this.currentTime ) {
2106
2416
 
2417
+ if(!track) {
2418
+ return;
2419
+ }
2420
+
2107
2421
  // Update animationClip information
2108
2422
  let clipIdx = track.clipIdx;
2109
2423
 
2110
- let [name, keyType] = this.getTrackName(track.name)
2424
+ let [name, keyType] = this.processTrackName(track.name)
2111
2425
  let tracks = this.tracksPerItem[name];
2112
2426
  if(!tracks) return;
2113
2427
 
@@ -2177,79 +2491,73 @@ class KeyFramesTimeline extends Timeline {
2177
2491
  }
2178
2492
 
2179
2493
  // Reset this key's properties
2180
- track.hovered[newIdx] = undefined;
2181
- track.selected[newIdx] = undefined;
2494
+ track.hovered[newIdx] = false;
2495
+ track.selected[newIdx] = false;
2182
2496
  track.edited[newIdx] = true;
2183
2497
 
2184
-
2185
2498
  // Update animation action interpolation info
2186
2499
  if(this.onUpdateTrack)
2187
2500
  this.onUpdateTrack( clipIdx );
2188
2501
 
2189
2502
  LX.emit( "@on_current_time_" + this.constructor.name, time);
2190
- if(this.onSetTime)
2191
- this.onSetTime(time);
2192
- this.draw();
2503
+
2193
2504
  return newIdx;
2194
2505
  }
2195
2506
 
2196
2507
  deleteContent() {
2197
-
2198
2508
  this.deleteKeyFrame({ multipleSelection: this.lastKeyFramesSelected.length > 1});
2199
2509
  }
2200
2510
 
2201
- /** Delete a keyframe given the track and the its index
2202
- * @track: track that keyframe belongs to
2203
- * @index: index of the keyframe on the track
2511
+ /**
2512
+ * @method #delete
2513
+ * @description Delete a keyframe given the track and the its index
2514
+ * @param {Number} trackIdx track that keyframe belongs to
2515
+ * @param {Number} index index of the keyframe on the track
2516
+ * @returns
2204
2517
  */
2205
- #delete( track, index ) {
2518
+ #delete( trackIdx, index ) {
2519
+
2520
+ const track = this.animationClip.tracks[trackIdx];
2206
2521
 
2207
- // Don't remove by now the first key
2208
- if(index == 0) {
2209
- console.warn("Operation not supported! [remove first keyframe track]");
2210
- return 0;
2522
+ // Don't remove by now the first key (and avoid impossible indices)
2523
+ if(index < 1 || index >= track.times.length ) {
2524
+ console.warn("Operation not supported! " + (index==0 ?"[removing first keyframe track]":"[removing invalid keyframe " + i + " from " + track.times.length + "]"));
2525
+ return false;
2211
2526
  }
2212
2527
 
2213
- // Update clip information
2214
- const clipIdx = track.clipIdx;
2215
-
2216
- // Don't remove by now the last key
2217
- // if(index == this.animationClip.tracks[clipIdx].times.length - 1) {
2218
- // console.warn("Operation not supported! [remove last keyframe track]");
2219
- // return;
2220
- // }
2221
-
2222
- // Reset this key's properties
2223
- track.hovered[index] = undefined;
2224
- track.selected[index] = undefined;
2225
- track.edited[index] = undefined;
2226
-
2227
- // Delete time key
2228
- this.animationClip.tracks[clipIdx].times = this.animationClip.tracks[clipIdx].times.filter( (v, i) => i != index);
2528
+ // Delete time key (TypedArrays do not have splice )
2529
+ track.times = track.times.filter( (v, i) => i != index);
2530
+ track.edited = track.edited.filter( (v, i) => i != index);
2531
+ track.selected = track.selected.filter( (v, i) => i != index);
2532
+ track.hovered = track.hovered.filter( (v, i) => i != index);
2229
2533
 
2230
2534
  // Delete values
2231
2535
  const indexDim = track.dim * index;
2232
- const slice1 = this.animationClip.tracks[clipIdx].values.slice(0, indexDim);
2233
- const slice2 = this.animationClip.tracks[clipIdx].values.slice(indexDim + track.dim);
2536
+ const slice1 = track.values.slice(0, indexDim);
2537
+ const slice2 = track.values.slice(indexDim + track.dim);
2234
2538
 
2235
- this.animationClip.tracks[clipIdx].values = LX.UTILS.concatTypedArray([slice1, slice2], Float32Array);
2539
+ track.values = LX.UTILS.concatTypedArray([slice1, slice2], Float32Array);
2236
2540
 
2237
- // Move the other's key properties
2238
- for(let i = index; i < this.animationClip.tracks[clipIdx].times.length; ++i) {
2239
- track.edited[i] = track.edited[i + 1];
2240
- }
2541
+ // // Move the other's key properties
2542
+ // for(let i = index; i < track.times.length; ++i) {
2543
+ // track.edited[i] = track.edited[i + 1];
2544
+ // track.hovered[i] = track.hovered[i + 1];
2545
+ // track.selected[i] = track.selected[i + 1];
2546
+ // }
2241
2547
 
2242
2548
  // Update animation action interpolation info
2243
2549
  if(this.onDeleteKeyFrame)
2244
- this.onDeleteKeyFrame( clipIdx, index );
2550
+ this.onDeleteKeyFrame( trackIdx, index );
2245
2551
 
2246
- return 1;
2552
+ return true;
2247
2553
  }
2248
2554
 
2249
- /** Delete one or more keyframes given the triggered event
2250
- * @e: event
2251
- * @track:
2252
- * @index: index of the keyframe on the track
2555
+ /**
2556
+ * @method deleteKeyFrame
2557
+ * @description Delete one or more keyframes given the triggered event
2558
+ * @param {Event} e: event
2559
+ * @param {Object} track:
2560
+ * @param {Number} index: index of the keyframe on the track
2253
2561
  */
2254
2562
  deleteKeyFrame(e, track, index) {
2255
2563
 
@@ -2263,14 +2571,13 @@ class KeyFramesTimeline extends Timeline {
2263
2571
 
2264
2572
  if(!pts) continue;
2265
2573
 
2266
- pts = pts.sort( (a,b) => a[2] - b[2] );
2267
-
2268
- let deletedIndices = 0;
2574
+ pts = pts.sort( (a,b) => b[2] - a[2] ); // sort by keyframe index (descending)
2269
2575
 
2270
- // Delete every selected key
2271
- for(let [name, idx, keyIndex] of pts) {
2272
- this.saveState(this.tracksPerItem[name][idx].clipIdx);
2273
- deletedIndices += this.#delete( this.tracksPerItem[name][idx], keyIndex - deletedIndices );
2576
+ // Delete every selected key starting with the last one in the track
2577
+ for(let [name, localIdx, keyIndex] of pts) {
2578
+ let track = this.tracksPerItem[name][localIdx];
2579
+ this.saveState(track.clipIdx);
2580
+ this.#delete(track.clipIdx, keyIndex);
2274
2581
  }
2275
2582
  }
2276
2583
  }
@@ -2278,25 +2585,20 @@ class KeyFramesTimeline extends Timeline {
2278
2585
 
2279
2586
  // Key pressed
2280
2587
  if(!track && this.lastKeyFramesSelected.length > 0) {
2281
- const [itemName, trackIndex, keyIndex] = this.lastKeyFramesSelected[0];
2282
- track = this.tracksPerItem[itemName][trackIndex];
2588
+ const [itemName, localTrackIndex, keyIndex] = this.lastKeyFramesSelected[0];
2589
+ track = this.tracksPerItem[itemName][localTrackIndex];
2283
2590
  index = keyIndex;
2284
2591
  }
2285
2592
 
2286
2593
  if ( track ){
2287
2594
  this.saveState(track.clipIdx);
2288
- this.#delete( track, index );
2595
+ this.#delete(track.clipIdx, index);
2289
2596
  }
2290
2597
  }
2291
2598
 
2292
2599
  this.unSelectAllKeyFrames();
2293
2600
  }
2294
2601
 
2295
- getNumKeyFramesSelected() {
2296
- return this.lastKeyFramesSelected.length;
2297
- }
2298
-
2299
-
2300
2602
  unSelect() {
2301
2603
 
2302
2604
  if(!this.unSelectAllKeyFrames()) {
@@ -2306,98 +2608,15 @@ class KeyFramesTimeline extends Timeline {
2306
2608
  }
2307
2609
  }
2308
2610
 
2309
- setSelectedItems( itemsName ) {
2310
-
2311
- if(itemsName.constructor !== Array)
2312
- throw("Item name has to be an array!");
2313
-
2314
- this.selectedItems = itemsName;
2315
- this.unSelectAllKeyFrames();
2316
- this.updateLeftPanel();
2317
- this.resize();
2318
- }
2319
-
2320
- getTrack( trackInfo ) {
2321
- const [name, trackIndex] = trackInfo;
2322
- return this.tracksPerItem[ name ][trackIndex];
2323
- }
2324
-
2325
- getTrackName( uglyName ) {
2326
-
2327
- let name, type;
2328
-
2329
- // Support other versions
2330
- if(uglyName.includes("[")) {
2331
- const nameIndex = uglyName.indexOf('['),
2332
- trackNameInfo = uglyName.substr(nameIndex+1).split("].");
2333
- name = trackNameInfo[0];
2334
- type = trackNameInfo[1];
2335
- }else {
2336
- const trackNameInfo = uglyName.split(".");
2337
- name = trackNameInfo[0];
2338
- type = trackNameInfo[1];
2339
- }
2340
-
2341
- return [name, type];
2342
- }
2343
-
2344
- getCurrentKeyFrame( track, time, threshold ) {
2345
-
2346
- if(!track || !track.times.length)
2347
- return;
2348
-
2349
- // Avoid iterating through all timestamps
2350
- if((time + threshold) < track.times[0])
2351
- return;
2352
-
2353
- for(let i = 0; i < track.times.length; ++i) {
2354
- let t = track.times[i];
2355
- if(t >= (time - threshold) &&
2356
- t <= (time + threshold)) {
2357
- return i;
2358
- }
2359
- }
2360
-
2361
- return;
2362
- }
2363
-
2364
- getKeyFramesInRange( track, minTime, maxTime, threshold ) {
2365
-
2366
- if(!track || !track.times.length)
2367
- return;
2368
-
2369
- // Manage negative selection
2370
- if(minTime > maxTime) {
2371
- let aux = minTime;
2372
- minTime = maxTime;
2373
- maxTime = aux;
2374
- }
2375
-
2376
- // Avoid iterating through all timestamps
2377
- if((maxTime + threshold) < track.times[0])
2378
- return;
2379
-
2380
- let indices = [];
2611
+
2381
2612
 
2382
- for(let i = 0; i < track.times.length; ++i) {
2383
- let t = track.times[i];
2384
- if(t >= (minTime - threshold) &&
2385
- t <= (maxTime + threshold)) {
2386
- indices.push(i);
2387
- }
2613
+ unHoverAll(){
2614
+ if(this.lastHovered) {
2615
+ this.tracksPerItem[ this.lastHovered[0] ][ this.lastHovered[1] ].hovered[ this.lastHovered[2] ] = false;
2388
2616
  }
2389
-
2390
- return indices;
2391
- }
2392
-
2393
- getNearestKeyFrame( track, time ) {
2394
-
2395
- if(!track || !track.times.length)
2396
- return;
2397
-
2398
- return track.times.reduce((a, b) => {
2399
- return Math.abs(b - time) < Math.abs(a - time) ? b : a;
2400
- });
2617
+ let h = this.lastHovered;
2618
+ this.lastHovered = null;
2619
+ return h;
2401
2620
  }
2402
2621
 
2403
2622
  unSelectAllKeyFrames() {
@@ -2418,59 +2637,36 @@ class KeyFramesTimeline extends Timeline {
2418
2637
  return;
2419
2638
 
2420
2639
  e.multipleSelection = multiple;
2421
- keyFrameIndex = keyFrameIndex ?? this.getCurrentKeyFrame( track, this.xToTime( localX ), this.pixelsToSeconds * 5 );
2640
+ keyFrameIndex = keyFrameIndex ?? this.getKeyFrame( track, this.xToTime( localX ), this.pixelsToSeconds * 5 );
2422
2641
 
2423
2642
  if(!multiple && e.button != 2) {
2424
2643
  this.unSelectAllKeyFrames();
2425
2644
  }
2426
-
2645
+
2646
+ if(keyFrameIndex == undefined)
2647
+ return;
2648
+
2427
2649
  const name = this.tracksDictionary[track.fullname];
2428
2650
  let t = this.tracksPerItem[ name ][track.idx];
2429
2651
  let currentSelection = [name, track.idx, keyFrameIndex];
2430
- if(this.lastKeyFramesSelected.indexOf(currentSelection) > -1)
2431
- return;
2432
-
2652
+
2433
2653
  if(!multiple)
2434
2654
  this.selectKeyFrame(t, currentSelection, keyFrameIndex);
2435
2655
  else
2436
2656
  this.lastKeyFramesSelected.push( currentSelection );
2437
- if( this.onSelectKeyFrame && this.onSelectKeyFrame(e, currentSelection, keyFrameIndex)) {
2438
- // Event handled
2439
- return;
2440
- }
2441
-
2442
- if(keyFrameIndex == undefined)
2443
- return;
2444
-
2445
- // Select if not handled
2446
-
2447
- t.selected[keyFrameIndex] = true;
2448
-
2449
- if( !multiple ) {
2450
- LX.emit( "@on_current_time_" + this.constructor.name, track.times[ keyFrameIndex ]);
2451
-
2452
- if(this.onSetTime )
2453
- this.onSetTime( track.times[ keyFrameIndex ] );
2454
- }
2455
- }
2456
-
2457
- /**
2458
- * @method addNewTrack
2459
- */
2460
-
2461
- addNewTrack() {
2462
-
2463
- if(!this.animationClip)
2464
- this.animationClip = {tracks:[]};
2465
2657
 
2466
- let trackInfo = {
2467
- idx: this.animationClip.tracks.length,
2468
- values: [], times: [],
2469
- selected: [], edited: [], hovered: []
2470
- };
2658
+
2659
+ if( !multiple ) {
2660
+ LX.emit( "@on_current_time_" + this.constructor.name, track.times[ keyFrameIndex ]);
2661
+ }
2471
2662
 
2472
- this.animationClip.tracks.push(trackInfo);
2473
- return trackInfo.idx;
2663
+ // Select if not handled
2664
+ t.selected[keyFrameIndex] = true;
2665
+
2666
+ if( this.onSelectKeyFrame && this.onSelectKeyFrame(e, currentSelection, keyFrameIndex)) {
2667
+ // Event handled
2668
+ return;
2669
+ }
2474
2670
  }
2475
2671
 
2476
2672
  /**
@@ -2490,7 +2686,7 @@ class KeyFramesTimeline extends Timeline {
2490
2686
  for(let i = count - 1; i >= 0; i--)
2491
2687
  {
2492
2688
  this.saveState(track.clipIdx);
2493
- this.#delete(track, i );
2689
+ this.#delete(track.clipIdx, i );
2494
2690
  }
2495
2691
  if(defaultValue != undefined) {
2496
2692
  if(typeof(defaultValue) == 'number') {
@@ -2509,6 +2705,289 @@ class KeyFramesTimeline extends Timeline {
2509
2705
 
2510
2706
  LX.KeyFramesTimeline = KeyFramesTimeline;
2511
2707
 
2708
+ /**
2709
+ * @class CurvesKeyFramesTimeline
2710
+ */
2711
+
2712
+ class CurvesKeyFramesTimeline extends KeyFramesTimeline {
2713
+
2714
+ /**
2715
+ * @param {string} name
2716
+ * @param {object} options = {animationClip, selectedItems, x, y, width, height, canvas, trackHeight, range}
2717
+ */
2718
+ constructor(name, options = {}) {
2719
+
2720
+ super(name, options);
2721
+ this.range = options.range || [0, 1];
2722
+ }
2723
+
2724
+ drawContent( ctx ) {
2725
+
2726
+ if(this.selectedItems == null || !this.tracksPerItem)
2727
+ return;
2728
+
2729
+ ctx.save();
2730
+ this.scrollableHeight = this.topMargin;
2731
+
2732
+ let offset = this.trackHeight;
2733
+ for(let t = 0; t < this.selectedItems.length; t++) {
2734
+ let tracks = this.tracksPerItem[this.selectedItems[t]] ? this.tracksPerItem[this.selectedItems[t]] : [{name: this.selectedItems[t]}];
2735
+ if(!tracks) continue;
2736
+
2737
+ const height = this.trackHeight;
2738
+ this.scrollableHeight += (tracks.length+1)*height;
2739
+ let scroll_y = - this.currentScrollInPixels;
2740
+
2741
+ let offsetI = 0;
2742
+ for(let i = 0; i < tracks.length; i++) {
2743
+ let track = tracks[i];
2744
+ if(track.hide) {
2745
+ continue;
2746
+ }
2747
+
2748
+ this.#drawTrackWithCurves(ctx, offsetI * height + offset + scroll_y, height, this.animationClip.tracks[track.clipIdx], track);
2749
+ offsetI++;
2750
+ }
2751
+ offset += offsetI * height + height;
2752
+ }
2753
+ ctx.restore();
2754
+
2755
+ };
2756
+
2757
+ /**
2758
+ * @method drawTrackWithCurves
2759
+ * @param {*} ctx
2760
+ * @description helper function, you can call it from drawContent to render all the keyframes
2761
+ * TODO
2762
+ */
2763
+ #drawTrackWithCurves (ctx, y, trackHeight, track, trackInfo) {
2764
+ const keyframes = track.times;
2765
+ let values = track.values;
2766
+
2767
+ if(keyframes) {
2768
+ ctx.globalAlpha = 0.2;
2769
+ ctx.fillStyle = Timeline.TRACK_SELECTED_LIGHT//"#2c303570";
2770
+ if(trackInfo.isSelected) {
2771
+ ctx.fillRect(0, y - 3, ctx.canvas.width, trackHeight );
2772
+ }
2773
+
2774
+ ctx.globalAlpha = 1;
2775
+ this.tracksDrawn.push([track,y+this.topMargin,trackHeight]);
2776
+
2777
+ //draw lines
2778
+ ctx.strokeStyle = "white";
2779
+ ctx.beginPath();
2780
+ for(var j = 0; j < keyframes.length; ++j)
2781
+ {
2782
+ let time = keyframes[j];
2783
+ let value = values[j];
2784
+
2785
+ //convert to timeline track range
2786
+ value = (((value - this.range[0]) * ( -this.trackHeight) ) / (this.range[1] - this.range[0])) + this.trackHeight;
2787
+
2788
+ if( time < this.startTime || time > this.endTime )
2789
+ continue;
2790
+ let keyframePosX = this.timeToX( time );
2791
+
2792
+ ctx.save();
2793
+ ctx.translate(keyframePosX, y );
2794
+ ctx.lineTo( 0, value );
2795
+ ctx.restore()
2796
+ }
2797
+ ctx.stroke();
2798
+ ctx.closePath();
2799
+ ctx.fillStyle = Timeline.COLOR;
2800
+ //draw points
2801
+ for(let j = 0; j < keyframes.length; ++j)
2802
+ {
2803
+ let time = keyframes[j];
2804
+ let selected = trackInfo.selected[j];
2805
+ let margin = 0;
2806
+ let size = 5;
2807
+ if( time < this.startTime || time > this.endTime )
2808
+ continue;
2809
+ const keyframePosX = this.timeToX( time );
2810
+ ctx.save();
2811
+
2812
+
2813
+ if(trackInfo.edited[j]) {
2814
+ ctx.fillStyle = Timeline.COLOR_EDITED;
2815
+ }
2816
+ if(selected) {
2817
+ ctx.fillStyle = Timeline.COLOR_SELECTED;
2818
+ //size = 7;
2819
+ margin = -2;
2820
+ }
2821
+ if(trackInfo.hovered[j]) {
2822
+ //size = 7;
2823
+ ctx.fillStyle = Timeline.COLOR_HOVERED;
2824
+ margin = -2;
2825
+ }
2826
+ if(trackInfo.locked)
2827
+ ctx.fillStyle = Timeline.COLOR_LOCK;
2828
+
2829
+ if(!this.active || trackInfo.active == false)
2830
+ ctx.fillStyle = Timeline.COLOR_UNACTIVE;
2831
+
2832
+ ctx.translate(keyframePosX, y);
2833
+
2834
+ let value = values[j];
2835
+ value = (((value - this.range[0]) * ( -this.trackHeight) ) / (this.range[1] - this.range[0])) + this.trackHeight;
2836
+
2837
+ ctx.beginPath();
2838
+ ctx.arc( 0, value, size, 0, Math.PI * 2);
2839
+ ctx.fill();
2840
+ ctx.closePath();
2841
+
2842
+ if(trackInfo.selected[j]) {
2843
+ ctx.fillStyle = Timeline.COLOR_SELECTED;
2844
+ ctx.beginPath();
2845
+ ctx.arc( 0, value, size - margin, 0, Math.PI * 2);
2846
+ ctx.fill();
2847
+ ctx.closePath();
2848
+ }
2849
+ ctx.restore();
2850
+ }
2851
+ }
2852
+ }
2853
+
2854
+ onMouseDown( e, ) {
2855
+
2856
+ const localX = e.localX;
2857
+ const localY = e.localY;
2858
+ let track = e.track;
2859
+
2860
+ if(e.shiftKey) {
2861
+ this.boxSelection = true;
2862
+ this.boxSelectionStart = [localX, localY - this.topMargin];
2863
+ e.multipleSelection = true;
2864
+
2865
+ }
2866
+ else if(track && !track.locked) {
2867
+
2868
+ const keyFrameIndex = this.getKeyFrame( track, this.xToTime( localX ), this.pixelsToSeconds * 5 );
2869
+ if( keyFrameIndex != undefined ) {
2870
+ this.processCurrentKeyFrame( e, keyFrameIndex, track, null, his.lastKeyFramesSelected.length > 1 || e.multipleSelection ); // Settings this as multiple so time is not being set
2871
+ if(e.ctrlKey || e.altKey) {
2872
+ this.movingKeys = true;
2873
+ this.canvas.style.cursor = "grab";
2874
+
2875
+ }
2876
+ // Set pre-move state
2877
+ for(let selectedKey of this.lastKeyFramesSelected) {
2878
+ const [name, idx, keyIndex] = selectedKey;
2879
+ const trackInfo = this.tracksPerItem[name][idx];
2880
+ selectedKey[3] = this.animationClip.tracks[ trackInfo.clipIdx ].times[ keyIndex ];
2881
+ }
2882
+
2883
+ this.timeBeforeMove = track.times[ keyFrameIndex ];
2884
+ this.valueBeforeMove = localY;
2885
+ }
2886
+ }
2887
+ else if(!track) {
2888
+ this.unSelectAllKeyFrames()
2889
+ }
2890
+ }
2891
+
2892
+ onMouseMove( e ) {
2893
+
2894
+ const localX = e.localX;
2895
+ let localY = e.localY;
2896
+ let track = e.track;
2897
+
2898
+ // Manage keyframe movement
2899
+ if(this.movingKeys) {
2900
+ this.clearState();
2901
+ const newTime = this.xToTime( localX );
2902
+
2903
+ for( let i = 0; i < this.lastKeyFramesSelected.length; ++i ){
2904
+ let s = this.lastKeyFramesSelected[i]; // pointer
2905
+ const name = s[0], idx = s[1], keyIndex = s[2], keyTime = s[3];
2906
+ track = this.tracksPerItem[name][idx];
2907
+ if(track && track.locked)
2908
+ return;
2909
+
2910
+ this.canvas.style.cursor = "grabbing";
2911
+
2912
+ if(e.ctrlKey) {
2913
+ const delta = this.timeBeforeMove - keyTime;
2914
+ this.animationClip.tracks[ track.clipIdx ].times[ keyIndex ] = Math.min( this.animationClip.duration, Math.max(0, newTime - delta) );
2915
+
2916
+ const times = track.times;
2917
+ let k = s[2];
2918
+ for( ; k > 0; --k ){
2919
+ if ( times[k-1] < times[k] ){
2920
+ break;
2921
+ }
2922
+ this.swapKeyFrames(track, k-1, k);
2923
+ }
2924
+ for( ; k < track.times.length-1; ++k ){
2925
+ if ( times[k+1] > times[k] ){
2926
+ break;
2927
+ }
2928
+ this.swapKeyFrames(track, k+1, k);
2929
+ }
2930
+ s[2] = k; // "s" is a pointer. Modify selected keyFrame index
2931
+ }
2932
+
2933
+ if(e.altKey) {
2934
+ const trackRange = [this.tracksDrawn[track.idx][1], this.tracksDrawn[track.idx][1] + this.trackHeight];
2935
+ localY = Math.min( trackRange[1], Math.max(trackRange[0], localY) );
2936
+
2937
+ //convert to range track values
2938
+ const value = (((localY - trackRange[1]) * (this.range[1] - this.range[0])) / (trackRange[0] - trackRange[1])) + this.range[0];
2939
+ track.edited[keyIndex] = true;
2940
+ this.animationClip.tracks[ track.clipIdx ].values[ keyIndex ] = value;
2941
+ LX.emit( "@on_change_" + this.tracksDrawn[track.idx][0].type, value );
2942
+ }
2943
+ }
2944
+ return;
2945
+ }
2946
+
2947
+ if( this.grabbing && e.button != 2) {
2948
+
2949
+ // fix this
2950
+ if(e.shiftKey && track) {
2951
+
2952
+ const keyFrameIndex = this.getNearestKeyFrame( track, this.currentTime);
2953
+
2954
+ if(keyFrameIndex != this.snappedKeyFrameIndex){
2955
+ this.snappedKeyFrameIndex = keyFrameIndex;
2956
+ LX.emit( "@on_current_time_" + this.constructor.name, this.currentTime);
2957
+ }
2958
+ }
2959
+ else {
2960
+ LX.emit( "@on_current_time_" + this.constructor.name, this.currentTime);
2961
+ }
2962
+
2963
+ }
2964
+ else if(track) {
2965
+
2966
+ const keyFrameIndex = this.getKeyFrame( track, this.xToTime( localX ), this.pixelsToSeconds * 5 );
2967
+ if(keyFrameIndex != undefined) {
2968
+
2969
+ const name = this.tracksDictionary[track.fullname];
2970
+ let t = this.tracksPerItem[ name ][track.idx];
2971
+ if(t && t.locked)
2972
+ return;
2973
+ this.unHoverAll();
2974
+
2975
+ this.lastHovered = [name, track.idx, keyFrameIndex];
2976
+ t.hovered[keyFrameIndex] = true;
2977
+
2978
+ }
2979
+ else {
2980
+ this.unHoverAll();
2981
+ }
2982
+ }
2983
+ else {
2984
+ this.unHoverAll();
2985
+ }
2986
+ }
2987
+ }
2988
+
2989
+ LX.CurvesKeyFramesTimeline = CurvesKeyFramesTimeline;
2990
+
2512
2991
  /**
2513
2992
  * @class ClipsTimeline
2514
2993
  */
@@ -2672,6 +3151,15 @@ class ClipsTimeline extends Timeline {
2672
3151
  }
2673
3152
  }
2674
3153
 
3154
+ unHoverAll(){
3155
+ if(this.lastHovered){
3156
+ this.animationClip.tracks[ this.lastHovered[0] ].hovered[ this.lastHovered[1] ] = false;
3157
+ }
3158
+ let h = this.lastHovered;
3159
+ this.lastHovered = null;
3160
+ return h;
3161
+ }
3162
+
2675
3163
  onMouseUp( e ) {
2676
3164
 
2677
3165
  let track = e.track;
@@ -2722,8 +3210,7 @@ class ClipsTimeline extends Timeline {
2722
3210
  this.boxSelection = false;
2723
3211
  this.boxSelectionStart = null;
2724
3212
  this.boxSelectionEnd = null;
2725
-
2726
- }
3213
+ }
2727
3214
 
2728
3215
  onMouseDown( e, time ) {
2729
3216
 
@@ -2745,7 +3232,7 @@ class ClipsTimeline extends Timeline {
2745
3232
  selectedClips = this.lastClipsSelected;
2746
3233
  }
2747
3234
  else {
2748
- let clipIndex = this.getCurrentClip( track, this.xToTime( localX ), this.pixelsToSeconds * 5 );
3235
+ let clipIndex = this.getClip( track, this.xToTime( localX ), this.pixelsToSeconds * 5 );
2749
3236
  if(clipIndex != undefined)
2750
3237
  {
2751
3238
  this.lastClipsSelected = selectedClips = [[track.idx, clipIndex]];
@@ -2791,7 +3278,7 @@ class ClipsTimeline extends Timeline {
2791
3278
  }
2792
3279
 
2793
3280
  }
2794
- else if(!track || track && this.getCurrentContent(track, time, 0.001) == undefined) {
3281
+ else if(!track || track && this.getContent(track, time, 0.001) == undefined) {
2795
3282
 
2796
3283
  if( this.timelineClickedClips )
2797
3284
  {
@@ -2823,15 +3310,10 @@ class ClipsTimeline extends Timeline {
2823
3310
 
2824
3311
  const innerSetTime = (t) => {
2825
3312
  LX.emit( "@on_current_time_" + this.constructor.name, t);
2826
- if( this.onSetTime )
2827
- this.onSetTime( t );
3313
+ // if( this.onSetTime )
3314
+ // this.onSetTime( t );
2828
3315
  }
2829
3316
 
2830
- const removeHover = () => {
2831
- if(this.lastHovered)
2832
- this.animationClip.tracks[ this.lastHovered[0] ].hovered[ this.lastHovered[1] ] = undefined;
2833
- };
2834
-
2835
3317
  if(e.shiftKey) {
2836
3318
  if(this.boxSelection) {
2837
3319
  this.boxSelectionEnd = [localX,localY - this.topMargin];
@@ -2935,7 +3417,7 @@ class ClipsTimeline extends Timeline {
2935
3417
  let clips = this.getClipsInRange(e.track, time, time, 0.1)
2936
3418
  if(!e.track.locked && clips != undefined) {
2937
3419
 
2938
- removeHover();
3420
+ this.unHoverAll();
2939
3421
  this.lastHovered = [e.track.idx, clips[0]];
2940
3422
  e.track.hovered[clips[0]] = true;
2941
3423
 
@@ -2964,11 +3446,11 @@ class ClipsTimeline extends Timeline {
2964
3446
  }
2965
3447
  }
2966
3448
  else {
2967
- removeHover();
3449
+ this.unHoverAll();
2968
3450
  }
2969
3451
  }
2970
3452
  else {
2971
- removeHover();
3453
+ this.unHoverAll();
2972
3454
  }
2973
3455
 
2974
3456
  }
@@ -2978,7 +3460,7 @@ class ClipsTimeline extends Timeline {
2978
3460
  let track = e.track;
2979
3461
  let localX = e.localX;
2980
3462
 
2981
- let clipIndex = this.getCurrentClip( track, this.xToTime( localX ), this.pixelsToSeconds * 5 );
3463
+ let clipIndex = this.getClip( track, this.xToTime( localX ), this.pixelsToSeconds * 5 );
2982
3464
  if(clipIndex != undefined) {
2983
3465
  this.lastClipsSelected = [[track.idx, clipIndex]];
2984
3466
 
@@ -3033,7 +3515,7 @@ class ClipsTimeline extends Timeline {
3033
3515
 
3034
3516
  }
3035
3517
 
3036
- onDrawContent( ctx, timeStart, timeEnd ) {
3518
+ drawContent( ctx, timeStart, timeEnd ) {
3037
3519
 
3038
3520
  if(!this.animationClip)
3039
3521
  return;
@@ -3059,7 +3541,7 @@ class ClipsTimeline extends Timeline {
3059
3541
  // Creates a map for each item -> tracks
3060
3542
  processTracks(animation) {
3061
3543
 
3062
- this.tracksPerItem = {};
3544
+ this.tracksPerItem = {}; // maps ( the name of item and local track index ) to ( animation tracks ). Holds pointers to tracks of animationClip.tracks
3063
3545
  this.tracksDictionary = {};
3064
3546
  this.animationClip = {
3065
3547
  name: (animation && animation.name) ? animation.name : "animationClip",
@@ -3230,9 +3712,9 @@ class ClipsTimeline extends Timeline {
3230
3712
  }
3231
3713
 
3232
3714
  // Reset this clip's properties
3233
- track.hovered[newIdx] = undefined;
3715
+ track.hovered[newIdx] = false;
3234
3716
  track.selected[newIdx] = true;
3235
- track.edited[newIdx] = undefined;
3717
+ track.edited[newIdx] = false;
3236
3718
 
3237
3719
  this.lastClipsSelected.push( [track.idx, newIdx] );
3238
3720
 
@@ -3248,8 +3730,8 @@ class ClipsTimeline extends Timeline {
3248
3730
  this.onUpdateTrack( trackIdx );
3249
3731
 
3250
3732
  LX.emit( "@on_current_time_" + this.constructor.name, this.currentTime);
3251
- if(this.onSetTime)
3252
- this.onSetTime(this.currentTime);
3733
+ // if(this.onSetTime)
3734
+ // this.onSetTime(this.currentTime);
3253
3735
 
3254
3736
  if(this.onSelectClip)
3255
3737
  this.onSelectClip(clip);
@@ -3301,9 +3783,9 @@ class ClipsTimeline extends Timeline {
3301
3783
  }
3302
3784
 
3303
3785
  // Reset this clip's properties
3304
- track.hovered[newIdx] = undefined;
3305
- track.selected[newIdx] = undefined;
3306
- track.edited[newIdx] = undefined;
3786
+ track.hovered[newIdx] = false;
3787
+ track.selected[newIdx] = false;
3788
+ track.edited[newIdx] = false;
3307
3789
 
3308
3790
 
3309
3791
  let end = clip.start + clip.duration;
@@ -3318,8 +3800,8 @@ class ClipsTimeline extends Timeline {
3318
3800
  this.onUpdateTrack( trackIdx );
3319
3801
 
3320
3802
  LX.emit( "@on_current_time_" + this.constructor.name, this.currentTime);
3321
- if(this.onSetTime)
3322
- this.onSetTime(this.currentTime);
3803
+ // if(this.onSetTime)
3804
+ // this.onSetTime(this.currentTime);
3323
3805
 
3324
3806
  if(callback)
3325
3807
  callback();
@@ -3479,14 +3961,23 @@ class ClipsTimeline extends Timeline {
3479
3961
  this.setDuration(end);
3480
3962
  }
3481
3963
 
3482
- // Update animation action interpolation info
3483
- if(this.onUpdateTrack)
3484
- this.onUpdateTrack( trackIdx );
3485
3964
  }
3486
3965
 
3966
+ // Update animation action interpolation info
3967
+ if(this.onUpdateTrack && Object.keys(trackIdxs).length){
3968
+ let tracksChanged = [];
3969
+ for( let c in trackIdxs ) {
3970
+ if ( tracksChanged.includes(trackIdxs[c].trackIdx) ) {
3971
+ continue;
3972
+ }
3973
+ tracksChanged.push(trackIdxs[c].trackIdx);
3974
+ }
3975
+ this.onUpdateTrack( tracksChanged );
3976
+ }
3977
+
3487
3978
  LX.emit( "@on_current_time_" + this.constructor.name, this.currentTime);
3488
- if(this.onSetTime)
3489
- this.onSetTime(this.currentTime);
3979
+ // if(this.onSetTime)
3980
+ // this.onSetTime(this.currentTime);
3490
3981
 
3491
3982
  if(callback)
3492
3983
  callback();
@@ -3558,9 +4049,9 @@ class ClipsTimeline extends Timeline {
3558
4049
  {
3559
4050
  clips = [...clips.slice(0, clipIdx), ...clips.slice(clipIdx + 1, clips.length)];
3560
4051
  this.animationClip.tracks[trackIdx].clips = clips;
3561
- this.animationClip.tracks[trackIdx].hovered[clipIdx] = undefined;
3562
- this.animationClip.tracks[trackIdx].selected[clipIdx] = undefined;
3563
- this.animationClip.tracks[trackIdx].edited[clipIdx] = undefined;
4052
+ this.animationClip.tracks[trackIdx].hovered[clipIdx] = false;
4053
+ this.animationClip.tracks[trackIdx].selected[clipIdx] = false;
4054
+ this.animationClip.tracks[trackIdx].edited[clipIdx] = false;
3564
4055
 
3565
4056
  if(clips.length)
3566
4057
  {
@@ -3581,7 +4072,7 @@ class ClipsTimeline extends Timeline {
3581
4072
  this.lastClipsSelected = [...this.lastClipsSelected.slice(0, selectedIdx), ...this.lastClipsSelected.slice(selectedIdx + 1, this.lastClipsSelected.length)];
3582
4073
  }
3583
4074
  }
3584
-
4075
+ return true;
3585
4076
  }
3586
4077
 
3587
4078
  copyContent() {
@@ -3687,7 +4178,7 @@ class ClipsTimeline extends Timeline {
3687
4178
  this.onUpdateTrack( state.t.idx );
3688
4179
  }
3689
4180
 
3690
- getCurrentClip( track, time, threshold ) {
4181
+ getClip( track, time, threshold ) {
3691
4182
 
3692
4183
  if(!track || !track.clips.length)
3693
4184
  return;
@@ -3737,7 +4228,7 @@ class ClipsTimeline extends Timeline {
3737
4228
  processCurrentClip( e, clipIndex, track, localX, multiple ) {
3738
4229
 
3739
4230
  e.multipleSelection = multiple;
3740
- clipIndex = clipIndex ?? this.getCurrentClip( track, this.xToTime( localX ), this.pixelsToSeconds * 5 );
4231
+ clipIndex = clipIndex ?? this.getClip( track, this.xToTime( localX ), this.pixelsToSeconds * 5 );
3741
4232
 
3742
4233
  if(!multiple && e.button != 2) {
3743
4234
  this.unSelectAllClips();
@@ -3850,14 +4341,7 @@ class CurvesTimeline extends Timeline {
3850
4341
  this.range = options.range || [0, 1];
3851
4342
 
3852
4343
  if(this.animationClip && this.animationClip.tracks.length)
3853
- this.processTracks(animation);
3854
-
3855
- // Add button data
3856
- let offset = 25;
3857
- if(this.active)
3858
- {
3859
-
3860
- }
4344
+ this.processTracks(this.animationClip);
3861
4345
  }
3862
4346
 
3863
4347
  onMouseUp( e, time) {
@@ -3894,9 +4378,10 @@ class CurvesTimeline extends Timeline {
3894
4378
 
3895
4379
  }else {
3896
4380
 
3897
- let boundingBox = this.canvas.getBoundingClientRect()
3898
- if(e.y < boundingBox.top || e.y > boundingBox.bottom)
4381
+ let boundingBox = this.canvas.getBoundingClientRect();
4382
+ if(e.y < boundingBox.top || e.y > boundingBox.bottom) {
3899
4383
  return;
4384
+ }
3900
4385
  // Check exact track keyframe
3901
4386
  if(!discard && track) {
3902
4387
  this.processCurrentKeyFrame( e, null, track, localX );
@@ -3924,7 +4409,7 @@ class CurvesTimeline extends Timeline {
3924
4409
  }
3925
4410
  else if(track && !track.locked) {
3926
4411
 
3927
- const keyFrameIndex = this.getCurrentKeyFrame( track, this.xToTime( localX ), this.pixelsToSeconds * 5 );
4412
+ const keyFrameIndex = this.getKeyFrame( track, this.xToTime( localX ), this.pixelsToSeconds * 5 );
3928
4413
  if( keyFrameIndex != undefined ) {
3929
4414
  this.processCurrentKeyFrame( e, keyFrameIndex, track, null, e.multipleSelection ); // Settings this as multiple so time is not being set
3930
4415
  if(e.ctrlKey || e.altKey) {
@@ -3956,15 +4441,17 @@ class CurvesTimeline extends Timeline {
3956
4441
 
3957
4442
  const innerSetTime = (t) => {
3958
4443
  LX.emit( "@on_current_time_" + this.constructor.name, t);
3959
- if( this.onSetTime )
3960
- this.onSetTime( t );
4444
+ // if( this.onSetTime )
4445
+ // this.onSetTime( t );
3961
4446
  }
3962
4447
  // Manage keyframe movement
3963
4448
  if(this.movingKeys) {
3964
4449
  this.clearState();
3965
4450
  const newTime = this.xToTime( localX );
3966
4451
 
3967
- for(let [name, idx, keyIndex, keyTime] of this.lastKeyFramesSelected) {
4452
+ for( let i = 0; i < this.lastKeyFramesSelected.length; ++i ){
4453
+ let s = this.lastKeyFramesSelected[i]; // pointer
4454
+ let name = s[0], idx = s[1], keyIndex = s[2], keyTime = s[3];
3968
4455
  track = this.tracksPerItem[name][idx];
3969
4456
  if(track && track.locked)
3970
4457
  return;
@@ -3974,6 +4461,22 @@ class CurvesTimeline extends Timeline {
3974
4461
  if(e.ctrlKey) {
3975
4462
  const delta = this.timeBeforeMove - keyTime;
3976
4463
  this.animationClip.tracks[ track.clipIdx ].times[ keyIndex ] = Math.min( this.animationClip.duration, Math.max(0, newTime - delta) );
4464
+
4465
+ let times = track.times;
4466
+ let k = s[2];
4467
+ for( ; k > 0; --k ){
4468
+ if ( times[k-1] < times[k] ){
4469
+ break;
4470
+ }
4471
+ this.swapKeyFrames(track, k-1, k);
4472
+ }
4473
+ for( ; k < track.times.length-1; ++k ){
4474
+ if ( times[k+1] > times[k] ){
4475
+ break;
4476
+ }
4477
+ this.swapKeyFrames(track, k+1, k);
4478
+ }
4479
+ s[2] = k; // "s" is a pointer. Modify selected keyFrame index
3977
4480
  }
3978
4481
 
3979
4482
  if(e.altKey) {
@@ -3992,17 +4495,8 @@ class CurvesTimeline extends Timeline {
3992
4495
 
3993
4496
  }
3994
4497
 
3995
- const removeHover = () => {
3996
- if(this.lastHovered)
3997
- this.tracksPerItem[ this.lastHovered[0] ][ this.lastHovered[1] ].hovered[ this.lastHovered[2] ] = undefined;
3998
- };
3999
-
4000
4498
  if( this.grabbing && e.button != 2) {
4001
4499
 
4002
- var curr = time - this.currentTime;
4003
- var delta = curr - this.grabTime;
4004
- this.grabTime = curr;
4005
-
4006
4500
  // fix this
4007
4501
  if(e.shiftKey && track) {
4008
4502
 
@@ -4020,24 +4514,24 @@ class CurvesTimeline extends Timeline {
4020
4514
  }
4021
4515
  else if(track) {
4022
4516
 
4023
- let keyFrameIndex = this.getCurrentKeyFrame( track, this.xToTime( localX ), this.pixelsToSeconds * 5 );
4517
+ let keyFrameIndex = this.getKeyFrame( track, this.xToTime( localX ), this.pixelsToSeconds * 5 );
4024
4518
  if(keyFrameIndex != undefined) {
4025
4519
 
4026
4520
  const name = this.tracksDictionary[track.fullname];
4027
4521
  let t = this.tracksPerItem[ name ][track.idx];
4028
- removeHover();
4029
4522
  if(t && t.locked)
4030
4523
  return;
4031
-
4524
+ this.unHoverAll();
4525
+
4032
4526
  this.lastHovered = [name, track.idx, keyFrameIndex];
4033
4527
  t.hovered[keyFrameIndex] = true;
4034
4528
 
4035
4529
  }else {
4036
- removeHover();
4530
+ this.unHoverAll();
4037
4531
  }
4038
4532
  }
4039
4533
  else {
4040
- removeHover();
4534
+ this.unHoverAll();
4041
4535
  }
4042
4536
  }
4043
4537
 
@@ -4108,20 +4602,21 @@ class CurvesTimeline extends Timeline {
4108
4602
 
4109
4603
  }
4110
4604
 
4111
- onDrawContent( ctx, timeStart, timeEnd ) {
4605
+ drawContent( ctx, timeStart, timeEnd ) {
4112
4606
 
4113
4607
  if(this.selectedItems == null || !this.tracksPerItem)
4114
4608
  return;
4609
+
4115
4610
  ctx.save();
4116
- // this.canvasArea.root.innerHtml = "";
4117
- let offset = this.trackHeight;
4118
4611
  this.scrollableHeight = this.topMargin;
4119
- for(let t = 0; t < this.selectedItems.length; t++) {
4612
+
4613
+ let offset = this.trackHeight;
4614
+ for(let t = 0; t < this.selectedItems.length; t++) {
4120
4615
  let tracks = this.tracksPerItem[this.selectedItems[t]] ? this.tracksPerItem[this.selectedItems[t]] : [{name: this.selectedItems[t]}];
4121
4616
  if(!tracks) continue;
4122
4617
 
4123
4618
  const height = this.trackHeight;
4124
- this.scrollableHeight += (tracks.length+1)*height ;
4619
+ this.scrollableHeight += (tracks.length+1)*height;
4125
4620
  let scroll_y = - this.currentScrollInPixels;
4126
4621
 
4127
4622
  let offsetI = 0;
@@ -4171,21 +4666,14 @@ class CurvesTimeline extends Timeline {
4171
4666
 
4172
4667
  ctx.save();
4173
4668
  ctx.translate(keyframePosX, y );
4174
-
4175
- if( keyframePosX <= this.sidebarWidth ){
4176
- ctx.moveTo( 0, value );
4177
- }
4178
- else {
4179
- ctx.lineTo( 0, value );
4180
- }
4181
- ctx.restore()
4182
-
4669
+ ctx.lineTo( 0, value );
4670
+ ctx.restore()
4183
4671
  }
4184
4672
  ctx.stroke();
4185
4673
  ctx.closePath();
4186
4674
  ctx.fillStyle = Timeline.COLOR;
4187
4675
  //draw points
4188
- for(var j = 0; j < keyframes.length; ++j)
4676
+ for(let j = 0; j < keyframes.length; ++j)
4189
4677
  {
4190
4678
  let time = keyframes[j];
4191
4679
  let selected = trackInfo.selected[j];
@@ -4193,48 +4681,46 @@ class CurvesTimeline extends Timeline {
4193
4681
  let size = 5;
4194
4682
  if( time < this.startTime || time > this.endTime )
4195
4683
  continue;
4196
- var keyframePosX = this.timeToX( time );
4197
- if( keyframePosX > this.sidebarWidth ){
4198
- ctx.save();
4684
+ const keyframePosX = this.timeToX( time );
4685
+ ctx.save();
4199
4686
 
4200
-
4201
- if(trackInfo.edited[j])
4202
- ctx.fillStyle = Timeline.COLOR_EDITED;
4203
- if(selected) {
4204
- ctx.fillStyle = Timeline.COLOR_SELECTED;
4205
- //size = 7;
4206
- margin = -2;
4207
- }
4208
- if(trackInfo.hovered[j]) {
4209
- //size = 7;
4210
- ctx.fillStyle = Timeline.COLOR_HOVERED;
4211
- margin = -2;
4212
- }
4213
- if(trackInfo.locked)
4214
- ctx.fillStyle = Timeline.COLOR_LOCK;
4687
+
4688
+ if(trackInfo.edited[j])
4689
+ ctx.fillStyle = Timeline.COLOR_EDITED;
4690
+ if(selected) {
4691
+ ctx.fillStyle = Timeline.COLOR_SELECTED;
4692
+ //size = 7;
4693
+ margin = -2;
4694
+ }
4695
+ if(trackInfo.hovered[j]) {
4696
+ //size = 7;
4697
+ ctx.fillStyle = Timeline.COLOR_HOVERED;
4698
+ margin = -2;
4699
+ }
4700
+ if(trackInfo.locked)
4701
+ ctx.fillStyle = Timeline.COLOR_LOCK;
4215
4702
 
4216
- if(!this.active || trackInfo.active == false)
4217
- ctx.fillStyle = Timeline.COLOR_UNACTIVE;
4218
-
4219
- ctx.translate(keyframePosX, y);
4703
+ if(!this.active || trackInfo.active == false)
4704
+ ctx.fillStyle = Timeline.COLOR_UNACTIVE;
4220
4705
 
4221
- let value = values[j];
4222
- value = (((value - this.range[0]) * ( -this.trackHeight) ) / (this.range[1] - this.range[0])) + this.trackHeight;
4706
+ ctx.translate(keyframePosX, y);
4707
+
4708
+ let value = values[j];
4709
+ value = (((value - this.range[0]) * ( -this.trackHeight) ) / (this.range[1] - this.range[0])) + this.trackHeight;
4223
4710
 
4711
+ ctx.beginPath();
4712
+ ctx.arc( 0, value, size, 0, Math.PI * 2);
4713
+ ctx.fill();
4714
+ ctx.closePath();
4715
+
4716
+ if(trackInfo.selected[j]) {
4717
+ ctx.fillStyle = Timeline.COLOR_SELECTED;
4224
4718
  ctx.beginPath();
4225
- ctx.arc( 0, value, size, 0, Math.PI * 2);
4719
+ ctx.arc( 0, value, size - margin, 0, Math.PI * 2);
4226
4720
  ctx.fill();
4227
4721
  ctx.closePath();
4228
-
4229
- if(trackInfo.selected[j]) {
4230
- ctx.fillStyle = Timeline.COLOR_SELECTED;
4231
- ctx.beginPath();
4232
- ctx.arc( 0, value, size - margin, 0, Math.PI * 2);
4233
- ctx.fill();
4234
- ctx.closePath();
4235
- }
4236
- ctx.restore();
4237
4722
  }
4723
+ ctx.restore();
4238
4724
  }
4239
4725
  }
4240
4726
  }
@@ -4267,8 +4753,8 @@ class CurvesTimeline extends Timeline {
4267
4753
 
4268
4754
  LX.emit( "@on_current_time_" + this.constructor.name, this.currentTime);
4269
4755
  // Update time
4270
- if(this.onSetTime)
4271
- this.onSetTime(this.currentTime);
4756
+ // if(this.onSetTime)
4757
+ // this.onSetTime(this.currentTime);
4272
4758
 
4273
4759
  return true; // Handled
4274
4760
  }
@@ -4286,21 +4772,32 @@ class CurvesTimeline extends Timeline {
4286
4772
  };
4287
4773
 
4288
4774
  if (animation && animation.tracks) {
4289
-
4290
4775
  for( let i = 0; i < animation.tracks.length; ++i ) {
4291
4776
 
4292
4777
  let track = animation.tracks[i];
4293
4778
 
4294
- const [name, type] = this.getTrackName(track.name);
4295
-
4779
+ const [name, type] = this.processTrackName(track.name);
4780
+
4781
+ let valueDim = track.dim;
4782
+ if ( !valueDim || valueDim < 0 ){
4783
+ if ( track.times.length && track.values.length ){ valueDim = track.values.length/track.times.length; }
4784
+ else{ valueDim = 1; }
4785
+ }
4786
+
4787
+ let leftOver = track.values.length % valueDim; // just in case values has an incorrect length
4788
+ let amounEntries = Math.min( track.times.length, track.values.length - leftOver );
4789
+ let times = track.times.slice(0, amounEntries);
4790
+ let values = track.values.slice(0, amounEntries * valueDim);
4791
+ let boolArray = (new Array(amounEntries)).fill(false);
4792
+
4296
4793
  let trackInfo = {
4297
4794
  fullname: track.name,
4298
4795
  name: name, type: type,
4299
- dim: track.values.length/track.times.length,
4300
- selected: [], edited: [], hovered: [], active: true,
4301
- values: track.values,
4302
- times: track.times
4303
-
4796
+ active: true,
4797
+ dim: valueDim,
4798
+ selected: boolArray.slice(), edited: boolArray.slice(), hovered: boolArray.slice(),
4799
+ times: times,
4800
+ values: values
4304
4801
  };
4305
4802
 
4306
4803
  if(!this.tracksPerItem[name]) {
@@ -4311,11 +4808,11 @@ class CurvesTimeline extends Timeline {
4311
4808
 
4312
4809
 
4313
4810
  const trackIndex = this.tracksPerItem[name].length - 1;
4314
- this.tracksPerItem[name][trackIndex].idx = trackIndex;
4315
- this.tracksPerItem[name][trackIndex].clipIdx = i;
4811
+ trackInfo.idx = trackIndex; // index of track in "name"
4812
+ trackInfo.clipIdx = i; // index of track in the entire animation
4316
4813
 
4317
4814
  // Save index also in original track
4318
- trackInfo.idx = trackIndex;
4815
+ track.idx = trackIndex;
4319
4816
  this.tracksDictionary[track.name] = name;
4320
4817
  this.animationClip.tracks.push(trackInfo);
4321
4818
 
@@ -4360,28 +4857,19 @@ class CurvesTimeline extends Timeline {
4360
4857
  });
4361
4858
  }
4362
4859
 
4363
- onPreProcessTrack( track, idx ) {
4364
- const name = this.tracksDictionary[track.name];
4365
- let trackInfo = this.tracksPerItem[track.name][idx];
4366
- trackInfo.selected = [];
4367
- trackInfo.edited = [];
4368
- trackInfo.hovered = [];
4369
- }
4370
-
4371
4860
  isKeyFrameSelected( track, index ) {
4372
4861
  return track.selected[ index ];
4373
4862
  }
4374
4863
 
4375
- saveState( clipIdx ) {
4376
-
4377
- const localIdx = this.animationClip.tracks[clipIdx].idx;
4378
- const name = this.getTrackName(this.animationClip.tracks[clipIdx].name)[0];
4379
- const trackInfo = this.tracksPerItem[name][localIdx];
4380
-
4864
+ /**
4865
+ * @param {Number} trackIdx index of track in the animation (not local index)
4866
+ */
4867
+ saveState( trackIdx ) {
4868
+ const trackInfo = this.animationClip.tracks[trackIdx];
4381
4869
  this.trackState.push({
4382
- idx: clipIdx,
4383
- t: this.animationClip.tracks[clipIdx].times.slice(),
4384
- v: this.animationClip.tracks[clipIdx].values.slice(),
4870
+ idx: trackIdx,
4871
+ t: trackInfo.times.slice(),
4872
+ v: trackInfo.values.slice(),
4385
4873
  editedTracks: [].concat(trackInfo.edited)
4386
4874
  });
4387
4875
  }
@@ -4396,7 +4884,7 @@ class CurvesTimeline extends Timeline {
4396
4884
  this.animationClip.tracks[state.idx].values = state.v;
4397
4885
 
4398
4886
  const localIdx = this.animationClip.tracks[state.idx].idx;
4399
- const name = this.getTrackName(this.animationClip.tracks[state.idx].name)[0];
4887
+ const name = this.processTrackName(this.animationClip.tracks[state.idx].name)[0];
4400
4888
  this.tracksPerItem[name][localIdx].edited = state.editedTracks;
4401
4889
 
4402
4890
  // Update animation action interpolation info
@@ -4404,6 +4892,43 @@ class CurvesTimeline extends Timeline {
4404
4892
  this.onUpdateTrack( state.idx );
4405
4893
  }
4406
4894
 
4895
+ /**
4896
+ *
4897
+ * @param {*} track
4898
+ * @param {Number} srcIdx keyFrame index
4899
+ * @param {Number} trgIdx keyFrame index
4900
+ */
4901
+ swapKeyFrames(track, srcIdx, trgIdx){
4902
+ let times = track.times;
4903
+ let values = track.values;
4904
+
4905
+ let tmp = times[srcIdx];
4906
+ times[srcIdx] = times[trgIdx];
4907
+ times[trgIdx] = tmp;
4908
+
4909
+ tmp = track.hovered[srcIdx];
4910
+ track.hovered[srcIdx] = track.hovered[trgIdx];
4911
+ track.hovered[trgIdx] = tmp;
4912
+
4913
+ tmp = track.edited[srcIdx];
4914
+ track.edited[srcIdx] = track.edited[trgIdx];
4915
+ track.edited[trgIdx] = tmp;
4916
+
4917
+ tmp = track.selected[srcIdx];
4918
+ track.selected[srcIdx] = track.selected[trgIdx];
4919
+ track.selected[trgIdx] = tmp;
4920
+
4921
+ let src = srcIdx * track.dim;
4922
+ let end = src + track.dim;
4923
+ let trg = trgIdx * track.dim;
4924
+ for( ; src < end; ++src ){
4925
+ tmp = values[ src ];
4926
+ values[ src ] = values[ trg ];
4927
+ values[ trg ] = tmp;
4928
+ ++trg;
4929
+ }
4930
+ }
4931
+
4407
4932
  selectKeyFrame( track, selectionInfo, index ) {
4408
4933
 
4409
4934
  if(index == undefined || !track)
@@ -4416,38 +4941,48 @@ class CurvesTimeline extends Timeline {
4416
4941
  this.currentTime = this.animationClip.tracks[track.clipIdx].times[ index ];
4417
4942
 
4418
4943
  LX.emit( "@on_current_time_" + this.constructor.name, this.currentTime );
4419
- if( this.onSetTime )
4420
- this.onSetTime( this.currentTime );
4944
+ // if( this.onSetTime )
4945
+ // this.onSetTime( this.currentTime );
4421
4946
  }
4422
4947
 
4423
4948
  copyContent() {
4949
+ if (!this.lastKeyFramesSelected.length){
4950
+ return;
4951
+ }
4952
+
4953
+ // sort keyframes selected by track
4424
4954
  let toCopy = {};
4425
4955
  for(let i = 0; i < this.lastKeyFramesSelected.length; i++){
4426
- let [id, trackIdx, keyIdx] = this.lastKeyFramesSelected[i];
4427
- if(toCopy[this.tracksPerItem[id][trackIdx].clipIdx]) {
4428
- toCopy[this.tracksPerItem[id][trackIdx].clipIdx].idxs.push(keyIdx);
4956
+ let [id, localTrackIdx, keyIdx] = this.lastKeyFramesSelected[i];
4957
+ let track = this.tracksPerItem[id][localTrackIdx];
4958
+ let trackIdx = track.clipIdx;
4959
+
4960
+ if(toCopy[trackIdx]) {
4961
+ toCopy[trackIdx].idxs.push(keyIdx);
4429
4962
  } else {
4430
- toCopy[this.tracksPerItem[id][trackIdx].clipIdx] = {idxs : [keyIdx]};
4431
- toCopy[this.tracksPerItem[id][trackIdx].clipIdx].track = this.tracksPerItem[id][trackIdx]
4432
- }
4963
+ toCopy[trackIdx] = {track: track, idxs : [keyIdx]};
4964
+ }
4433
4965
  if(i == 0) {
4434
- this.copyKeyFrameValue(this.tracksPerItem[id][trackIdx], keyIdx)
4966
+ this.copyKeyFrameValue(track, keyIdx);
4435
4967
  }
4436
4968
  }
4437
- for(let clipIdx in toCopy) {
4438
-
4439
- this.copyKeyFrames(toCopy[clipIdx].track, toCopy[clipIdx].idxs)
4969
+
4970
+ // for each track selected, copy its values
4971
+ for(let trackIdx in toCopy) {
4972
+ this.copyKeyFrames(toCopy[trackIdx].track, toCopy[trackIdx].idxs);
4440
4973
  }
4441
4974
  }
4442
-
4443
- copyKeyFrame( track, index ) {
4975
+
4976
+ // copies the current value of the keyframe. This value can be pasted across any track (as long as they are of the same type)
4977
+ copyKeyFrameValue( track, index ) {
4444
4978
 
4445
4979
  // 1 element clipboard by now
4446
4980
 
4447
- let values = [];
4448
4981
  let start = index * track.dim;
4449
- for(let i = start; i < start + track.dim; ++i)
4450
- values.push( this.animationClip.tracks[ track.clipIdx ].values[i] );
4982
+ let values = this.animationClip.tracks[ track.clipIdx ].values.slice(start, start + track.dim);
4983
+
4984
+ if(!this.clipboard)
4985
+ this.clipboard = {};
4451
4986
 
4452
4987
  this.clipboard = {
4453
4988
  type: track.type,
@@ -4455,23 +4990,49 @@ class CurvesTimeline extends Timeline {
4455
4990
  };
4456
4991
  }
4457
4992
 
4458
- pasteContent() {
4993
+ // each track will have its own entry of copied keyframes. When pasting, only the apropiate track's keyframes are pasted
4994
+ copyKeyFrames( track, indices ) {
4995
+
4996
+ let trackIdx = track.clipIdx;
4459
4997
  if(!this.clipboard)
4998
+ this.clipboard = {};
4999
+ if(!this.clipboard.keyframes) {
5000
+ this.clipboard.keyframes = {};
5001
+ }
5002
+
5003
+ this.clipboard.keyframes[trackIdx] = { track: track, values:{}, times:{} };
5004
+
5005
+ // 1 element clipboard by now
5006
+ for(let i = 0; i < indices.length; i++ ){
5007
+ let keyIdx = indices[i];
5008
+ let start = keyIdx * track.dim;
5009
+ let keyValues = track.values.slice(start, start + track.dim); // copy values into a new array
5010
+ this.clipboard.keyframes[trackIdx].values[keyIdx] = keyValues; // save to clipboard
5011
+ this.clipboard.keyframes[trackIdx].times[keyIdx] = track.times[keyIdx]; // save to clipboard
5012
+ };
5013
+ }
5014
+
5015
+ pasteContent() {
5016
+ if(!this.clipboard) {
4460
5017
  return;
5018
+ }
4461
5019
 
5020
+ // copy the value into the only selected keyframe
4462
5021
  if(this.clipboard.value && this.lastKeyFramesSelected.length == 1) {
4463
5022
 
4464
- let [id, trackIdx, keyIdx] = this.lastKeyFramesSelected[0];
4465
- this.pasteKeyFrameValue({}, this.tracksPerItem[id][trackIdx], keyIdx);
5023
+ let [id, localTrackIdx, keyIdx] = this.lastKeyFramesSelected[0];
5024
+ this.pasteKeyFrameValue({}, this.tracksPerItem[id][localTrackIdx], keyIdx);
4466
5025
  }
5026
+
5027
+ // create new keyframes from the ones copied
4467
5028
  if(this.clipboard.keyframes) {
4468
5029
  let currentTime = this.currentTime;
4469
- for(let clipIdx in this.clipboard.keyframes) {
4470
- let indices = Object.keys( this.clipboard.keyframes[clipIdx].values)
4471
- this.pasteKeyFrames({multipleSelection: this.clipboard.keyframes.length}, clipIdx, indices);
5030
+ for(let trackIdx in this.clipboard.keyframes) {
5031
+ this.pasteKeyFrames({multipleSelection: this.clipboard.keyframes.length}, trackIdx);
4472
5032
  this.currentTime = currentTime;
4473
5033
  }
4474
5034
  }
5035
+ this.clipboard = {};
4475
5036
  }
4476
5037
 
4477
5038
  canPasteKeyFrame () {
@@ -4495,13 +5056,13 @@ class CurvesTimeline extends Timeline {
4495
5056
  }
4496
5057
 
4497
5058
  LX.emit( "@on_current_time_" + this.constructor.name, this.currentTime);
4498
- if(this.onSetTime)
4499
- this.onSetTime(this.currentTime);
5059
+ // if(this.onSetTime)
5060
+ // this.onSetTime(this.currentTime);
4500
5061
 
4501
5062
  track.edited[ index ] = true;
4502
5063
  }
4503
5064
 
4504
- pasteKeyFrame( e, track, index ) {
5065
+ pasteKeyFrameValue( e, track, index ) {
4505
5066
 
4506
5067
  this.saveState(track.clipIdx);
4507
5068
 
@@ -4520,32 +5081,52 @@ class CurvesTimeline extends Timeline {
4520
5081
  }
4521
5082
  }
4522
5083
 
4523
- pasteKeyFrameValue( e, track, index ) {
5084
+ pasteKeyFrames( e, trackIdx ) {
5085
+
5086
+ if ( !this.clipboard.keyframes[trackIdx] ){
5087
+ return;
5088
+ }
5089
+ this.saveState(trackIdx);
4524
5090
 
4525
- this.saveState(track.clipIdx);
5091
+ let clipboardInfo = this.clipboard.keyframes[trackIdx];
5092
+ let indices = Object.keys(clipboardInfo.values);
5093
+ indices.sort(); // just in case
4526
5094
 
4527
5095
  // Copy to current key
4528
- this.#paste( track, index );
5096
+ for(let i = 0; i < indices.length; i++) {
5097
+ let value = clipboardInfo.values[indices[i]];
5098
+ if(typeof value == 'number')
5099
+ value = [value];
5100
+ if(i > 0) {
5101
+ let delta = clipboardInfo.times[indices[i]] - clipboardInfo.times[indices[i-1]];
5102
+ this.currentTime += delta;
5103
+ }
5104
+ this.addKeyFrame( clipboardInfo.track, value);
5105
+ }
4529
5106
 
4530
- if(!e.multipleSelection)
4531
- return;
5107
+ // if(!e.multipleSelection)
5108
+ // return;
4532
5109
 
4533
- // Don't want anything after this
4534
- this.clearState();
5110
+ // // Don't want anything after this
5111
+ // this.clearState();
4535
5112
 
4536
- // Copy to every selected key
4537
- for(let [name, idx, keyIndex] of this.lastKeyFramesSelected) {
4538
- this.#paste( this.tracksPerItem[name][idx], keyIndex );
4539
- }
5113
+ // // Copy to every selected key
5114
+ // for(let [name, idx, keyIndex] of this.lastKeyFramesSelected) {
5115
+ // this.#paste( this.tracksPerItem[name][idx], keyIndex );
5116
+ // }
4540
5117
  }
4541
5118
 
4542
5119
  addKeyFrame( track, value = undefined, time = this.currentTime ) {
4543
-
5120
+
5121
+ if(!track) {
5122
+ return;
5123
+ }
5124
+
4544
5125
  // Update animationClip information
4545
5126
  const clipIdx = track.clipIdx;
4546
5127
 
4547
5128
  // Time slot with other key?
4548
- const keyInCurrentSlot = this.animationClip.tracks[clipIdx].times.find( t => { return !LX.UTILS.compareThreshold(this.currentTime, t, t, 0.001 ); });
5129
+ const keyInCurrentSlot = this.animationClip.tracks[clipIdx].times.find( t => { return !LX.UTILS.compareThreshold(time, t, t, 0.001 ); });
4549
5130
  if( keyInCurrentSlot ) {
4550
5131
  console.warn("There is already a keyframe stored in time slot ", keyInCurrentSlot)
4551
5132
  return;
@@ -4604,8 +5185,8 @@ class CurvesTimeline extends Timeline {
4604
5185
  // }
4605
5186
 
4606
5187
  // Reset this key's properties
4607
- track.hovered[newIdx] = undefined;
4608
- track.selected[newIdx] = undefined;
5188
+ track.hovered[newIdx] = false;
5189
+ track.selected[newIdx] = false;
4609
5190
  track.edited[newIdx] = true;
4610
5191
 
4611
5192
 
@@ -4614,8 +5195,8 @@ class CurvesTimeline extends Timeline {
4614
5195
  this.onUpdateTrack( clipIdx );
4615
5196
 
4616
5197
  LX.emit( "@on_current_time_" + this.constructor.name, this.currentTime);
4617
- if(this.onSetTime)
4618
- this.onSetTime(this.currentTime);
5198
+ // if(this.onSetTime)
5199
+ // this.onSetTime(this.currentTime);
4619
5200
 
4620
5201
  return newIdx;
4621
5202
  }
@@ -4625,51 +5206,46 @@ class CurvesTimeline extends Timeline {
4625
5206
  this.deleteKeyFrame({ multipleSelection: this.lastKeyFramesSelected.length > 1});
4626
5207
  }
4627
5208
 
4628
- /** Delete a keyframe given the track and the its index
4629
- * @track: track that keyframe belongs to
4630
- * @index: index of the keyframe on the track
5209
+ /**
5210
+ * Delete a keyframe given the track and the its index
5211
+ * @param {Number} trackIdx track that keyframe belongs to
5212
+ * @param {Number} index index of the keyframe on the track
5213
+ * @returns
4631
5214
  */
4632
- #delete( track, index ) {
5215
+ #delete( trackIdx, index ) {
4633
5216
 
4634
- // Don't remove by now the first key
4635
- if(index == 0) {
4636
- console.warn("Operation not supported! [remove first keyframe track]");
4637
- return;
5217
+ const track = this.animationClip.tracks[trackIdx];
5218
+
5219
+ // Don't remove by now the first key (and avoid impossible indices)
5220
+ if(index < 1 || index >= track.times.length ) {
5221
+ console.warn("Operation not supported! " + (index==0 ?"[removing first keyframe track]":"[removing invalid keyframe " + i + " from " + track.times.length + "]"));
5222
+ return false;
4638
5223
  }
4639
5224
 
4640
- // Update clip information
4641
- const clipIdx = track.clipIdx;
4642
-
4643
- // Don't remove by now the last key
4644
- // if(index == this.animationClip.tracks[clipIdx].times.length - 1) {
4645
- // console.warn("Operation not supported! [remove last keyframe track]");
4646
- // return;
4647
- // }
4648
-
4649
- // Reset this key's properties
4650
- track.hovered[index] = undefined;
4651
- track.selected[index] = undefined;
4652
- track.edited[index] = undefined;
4653
-
4654
- // Delete time key
4655
- this.animationClip.tracks[clipIdx].times = this.animationClip.tracks[clipIdx].times.filter( (v, i) => i != index);
5225
+ // Delete time key (TypedArrays do not have splice )
5226
+ track.times = track.times.filter( (v, i) => i != index);
5227
+ track.edited = track.edited.filter( (v, i) => i != index);
5228
+ track.selected = track.selected.filter( (v, i) => i != index);
5229
+ track.hovered = track.hovered.filter( (v, i) => i != index);
4656
5230
 
4657
5231
  // Delete values
4658
5232
  const indexDim = track.dim * index;
4659
- const slice1 = this.animationClip.tracks[clipIdx].values.slice(0, indexDim);
4660
- const slice2 = this.animationClip.tracks[clipIdx].values.slice(indexDim + track.dim);
5233
+ const slice1 = track.values.slice(0, indexDim);
5234
+ const slice2 = track.values.slice(indexDim + track.dim);
4661
5235
 
4662
- this.animationClip.tracks[clipIdx].values = LX.UTILS.concatTypedArray([slice1, slice2], Float32Array);
5236
+ track.values = LX.UTILS.concatTypedArray([slice1, slice2], Float32Array);
4663
5237
 
4664
- // Move the other's key properties
4665
- for(let i = index; i < this.animationClip.tracks[clipIdx].times.length; ++i) {
4666
- track.edited[i] = track.edited[i + 1];
4667
- }
5238
+ // // Move the other's key properties
5239
+ // for(let i = index; i < track.times.length; ++i) {
5240
+ // track.edited[i] = track.edited[i + 1];
5241
+ // track.hovered[i] = track.hovered[i + 1];
5242
+ // track.selected[i] = track.selected[i + 1];
5243
+ // }
4668
5244
 
4669
5245
  // Update animation action interpolation info
4670
5246
  if(this.onDeleteKeyFrame)
4671
- this.onDeleteKeyFrame( clipIdx, index );
4672
-
5247
+ this.onDeleteKeyFrame( trackIdx, index );
5248
+
4673
5249
  return true;
4674
5250
  }
4675
5251
 
@@ -4690,15 +5266,13 @@ class CurvesTimeline extends Timeline {
4690
5266
 
4691
5267
  if(!pts) continue;
4692
5268
 
4693
- pts = pts.sort( (a,b) => a[2] - b[2] );
4694
-
4695
- let deletedIndices = 0;
5269
+ pts = pts.sort( (a,b) => b[2] - a[2] ); // sort by keyframe index (descending)
4696
5270
 
4697
- // Delete every selected key
4698
- for(let [name, idx, keyIndex] of pts) {
4699
- this.saveState(this.tracksPerItem[name][idx].clipIdx);
4700
- let deleted = this.#delete( this.tracksPerItem[name][idx], keyIndex - deletedIndices );
4701
- deletedIndices += deleted ? 1 : 0;
5271
+ // Delete every selected key starting with the last one in the track
5272
+ for(let [name, localIdx, keyIndex] of pts) {
5273
+ let track = this.tracksPerItem[name][localIdx];
5274
+ this.saveState(track.clipIdx);
5275
+ this.#delete(track.clipIdx, keyIndex);
4702
5276
  }
4703
5277
  }
4704
5278
  }
@@ -4706,53 +5280,20 @@ class CurvesTimeline extends Timeline {
4706
5280
 
4707
5281
  // Key pressed
4708
5282
  if(!track && this.lastKeyFramesSelected.length > 0) {
4709
- const [itemName, trackIndex, keyIndex] = this.lastKeyFramesSelected[0];
4710
- track = this.tracksPerItem[itemName][trackIndex];
5283
+ const [itemName, localTrackIndex, keyIndex] = this.lastKeyFramesSelected[0];
5284
+ track = this.tracksPerItem[itemName][localTrackIndex];
4711
5285
  index = keyIndex;
4712
5286
  }
4713
5287
 
4714
5288
  if ( track ){
4715
5289
  this.saveState(track.clipIdx);
4716
- this.#delete( track, index );
5290
+ this.#delete(track.clipIdx, index);
4717
5291
  }
4718
5292
  }
4719
5293
 
4720
5294
  this.unSelectAllKeyFrames();
4721
5295
  }
4722
5296
 
4723
- /**
4724
- * @method clearTrack
4725
- */
4726
-
4727
- clearTrack(idx, defaultValue) {
4728
-
4729
- let track = this.animationClip.tracks[idx];
4730
-
4731
- if(track.locked )
4732
- {
4733
- return;
4734
- }
4735
-
4736
- const count = track.times.length;
4737
- for(let i = count - 1; i >= 0; i--)
4738
- {
4739
- this.saveState(track.clipIdx);
4740
- this.#delete(track, i );
4741
- }
4742
- if(defaultValue != undefined) {
4743
- if(typeof(defaultValue) == 'number') {
4744
- track.values[0] = defaultValue;
4745
- }
4746
- else {
4747
- for(let i = 0; i < defaultValue.length; i++) {
4748
- track.values[i] = defaultValue[i];
4749
- }
4750
- }
4751
-
4752
- }
4753
- return idx;
4754
- }
4755
-
4756
5297
  getNumKeyFramesSelected() {
4757
5298
  return this.lastKeyFramesSelected.length;
4758
5299
  }
@@ -4766,6 +5307,9 @@ class CurvesTimeline extends Timeline {
4766
5307
  }
4767
5308
  }
4768
5309
 
5310
+ /**
5311
+ * @param {Array} itemsName
5312
+ */
4769
5313
  setSelectedItems( itemsName ) {
4770
5314
 
4771
5315
  if(itemsName.constructor !== Array)
@@ -4781,7 +5325,7 @@ class CurvesTimeline extends Timeline {
4781
5325
  return this.tracksPerItem[ name ][trackIndex];
4782
5326
  }
4783
5327
 
4784
- getTrackName( uglyName ) {
5328
+ processTrackName( uglyName ) {
4785
5329
 
4786
5330
  let name, type;
4787
5331
 
@@ -4802,7 +5346,7 @@ class CurvesTimeline extends Timeline {
4802
5346
  return [name, type];
4803
5347
  }
4804
5348
 
4805
- getCurrentKeyFrame( track, time, threshold ) {
5349
+ getKeyFrame( track, time, threshold ) {
4806
5350
 
4807
5351
  if(!track || !track.times.length)
4808
5352
  return;
@@ -4861,6 +5405,15 @@ class CurvesTimeline extends Timeline {
4861
5405
  });
4862
5406
  }
4863
5407
 
5408
+ unHoverAll() {
5409
+ if(this.lastHovered) {
5410
+ this.tracksPerItem[ this.lastHovered[0] ][ this.lastHovered[1] ].hovered[ this.lastHovered[2] ] = false;
5411
+ }
5412
+ let h = this.lastHovered;
5413
+ this.lastHovered = null;
5414
+ return h;
5415
+ }
5416
+
4864
5417
  unSelectAllKeyFrames() {
4865
5418
 
4866
5419
  for(let [name, idx, keyIndex] of this.lastKeyFramesSelected) {
@@ -4875,8 +5428,11 @@ class CurvesTimeline extends Timeline {
4875
5428
 
4876
5429
  processCurrentKeyFrame( e, keyFrameIndex, track, localX, multiple ) {
4877
5430
 
5431
+ if(track.locked)
5432
+ return;
5433
+
4878
5434
  e.multipleSelection = multiple;
4879
- keyFrameIndex = keyFrameIndex ?? this.getCurrentKeyFrame( track, this.xToTime( localX ), this.pixelsToSeconds * 5 );
5435
+ keyFrameIndex = keyFrameIndex ?? this.getKeyFrame( track, this.xToTime( localX ), this.pixelsToSeconds * 5 );
4880
5436
 
4881
5437
  if(!multiple && e.button != 2) {
4882
5438
  this.unSelectAllKeyFrames();
@@ -4885,7 +5441,8 @@ class CurvesTimeline extends Timeline {
4885
5441
  const name = this.tracksDictionary[track.fullname];
4886
5442
  let t = this.tracksPerItem[ name ][track.idx];
4887
5443
  let currentSelection = [name, track.idx, keyFrameIndex];
4888
- if(!multiple)
5444
+
5445
+ if(!multiple)
4889
5446
  this.selectKeyFrame(t, currentSelection, keyFrameIndex);
4890
5447
  else
4891
5448
  this.lastKeyFramesSelected.push( currentSelection );
@@ -4896,7 +5453,7 @@ class CurvesTimeline extends Timeline {
4896
5453
  }
4897
5454
 
4898
5455
  if(keyFrameIndex == undefined)
4899
- return;
5456
+ return;
4900
5457
 
4901
5458
  // Select if not handled
4902
5459
  t.selected[keyFrameIndex] = true;
@@ -4904,9 +5461,61 @@ class CurvesTimeline extends Timeline {
4904
5461
  if( !multiple) {
4905
5462
 
4906
5463
  LX.emit( "@on_current_time_" + this.constructor.name, track.times[ keyFrameIndex]);
4907
- if(this.onSetTime)
4908
- this.onSetTime( track.times[ keyFrameIndex ] );
5464
+ // if(this.onSetTime)
5465
+ // this.onSetTime( track.times[ keyFrameIndex ] );
5466
+ }
5467
+ }
5468
+
5469
+ /**
5470
+ * @method addNewTrack
5471
+ */
5472
+
5473
+ addNewTrack() {
5474
+
5475
+ if(!this.animationClip)
5476
+ this.animationClip = {tracks:[]};
5477
+
5478
+ let trackInfo = {
5479
+ idx: this.animationClip.tracks.length,
5480
+ values: [], times: [],
5481
+ selected: [], edited: [], hovered: []
5482
+ };
5483
+
5484
+ this.animationClip.tracks.push(trackInfo);
5485
+ this.updateLeftPanel();
5486
+ return trackInfo.idx;
5487
+ }
5488
+
5489
+ /**
5490
+ * @method clearTrack
5491
+ */
5492
+ clearTrack(idx, defaultValue) {
5493
+
5494
+ let track = this.animationClip.tracks[idx];
5495
+
5496
+ if(track.locked )
5497
+ {
5498
+ return;
5499
+ }
5500
+
5501
+ const count = track.times.length;
5502
+ for(let i = count - 1; i >= 0; i--)
5503
+ {
5504
+ this.saveState(track.clipIdx);
5505
+ this.#delete(track, i );
5506
+ }
5507
+ if(defaultValue != undefined) {
5508
+ if(typeof(defaultValue) == 'number') {
5509
+ track.values[0] = defaultValue;
5510
+ }
5511
+ else {
5512
+ for(let i = 0; i < defaultValue.length; i++) {
5513
+ track.values[i] = defaultValue[i];
5514
+ }
5515
+ }
5516
+
4909
5517
  }
5518
+ return idx;
4910
5519
  }
4911
5520
  }
4912
5521
 
@@ -4974,8 +5583,15 @@ LX.UTILS.HexToRgb = (hex) => {
4974
5583
  throw new Error('Bad Hex');
4975
5584
  }
4976
5585
 
4977
- LX.UTILS.concatTypedArray = (Arrays, ArrayType) => {
4978
- return Arrays.reduce((acc, arr) => new ArrayType([...acc, ...arr]), []);
5586
+ LX.UTILS.concatTypedArray = (arrays, ArrayType) => {
5587
+ let size = arrays.reduce((acc,arr) => acc + arr.length, 0);
5588
+ let result = new ArrayType( size ); // generate just one array
5589
+ let offset = 0;
5590
+ for( let i = 0; i < arrays.length; ++i ){
5591
+ result.set(arrays[i], offset ); // copy values
5592
+ offset += arrays[i].length;
5593
+ }
5594
+ return result;
4979
5595
  }
4980
5596
 
4981
- export { Timeline, KeyFramesTimeline, ClipsTimeline, CurvesTimeline };
5597
+ export { Timeline, KeyFramesTimeline, ClipsTimeline, CurvesTimeline, CurvesKeyFramesTimeline };