@checksub_team/peaks_timeline 2.2.1 → 2.3.0-alpha.1

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.
@@ -8,8 +8,9 @@
8
8
 
9
9
  define([
10
10
  '../utils',
11
+ './invoker',
11
12
  'konva'
12
- ], function(Utils, Konva) {
13
+ ], function(Utils, Invoker, Konva) {
13
14
  'use strict';
14
15
 
15
16
  var LEFT_PADDING = 4;
@@ -29,52 +30,62 @@ define([
29
30
  function Axis(peaks, view, options) {
30
31
  this._view = view;
31
32
 
33
+ this._invoker = new Invoker();
34
+
32
35
  var self = this;
33
36
 
34
37
  peaks.on('playhead.moved', this._onPlayheadMoved.bind(this));
35
38
  peaks.on('playhead.hidden', this._onPlayheadHidden.bind(this));
36
39
 
37
- self._axisGridlineColor = options.axisGridlineColor;
38
- self._axisLabelColor = options.axisLabelColor;
40
+ this._axisGridlineColor = options.axisGridlineColor;
41
+ this._axisLabelColor = options.axisLabelColor;
39
42
 
40
- self._backLayer = new Konva.Layer({
43
+ this._backLayer = new Konva.Layer({
41
44
  listening: false
42
45
  });
43
46
 
44
- self._frontLayer = new Konva.Layer({
47
+ this._frontLayer = new Konva.Layer({
45
48
  listening: false
46
49
  });
47
50
 
48
- self._axisShape = new Konva.Shape({
51
+ this._axisShape = new Konva.Shape({
49
52
  sceneFunc: function(context) {
50
53
  self.drawAxis(context, view);
51
54
  }
52
55
  });
53
- self._backLayer.add(self._axisShape);
56
+ this._backLayer.add(this._axisShape);
54
57
 
55
- self._timesShape = new Konva.Shape({
58
+ this._timesShape = new Konva.Shape({
56
59
  sceneFunc: function(context) {
57
60
  self.drawTimes(context, view);
58
61
  }
59
62
  });
60
- self._frontLayer.add(self._timesShape);
63
+ this._frontLayer.add(this._timesShape);
64
+
65
+ // Throttled draws to prevent excessive redraws.
66
+ this._throttledBackDraw = this._invoker.throttleTrailing(
67
+ this._backLayer.batchDraw.bind(this._backLayer)
68
+ );
69
+ this._throttledFrontDraw = this._invoker.throttleTrailing(
70
+ this._frontLayer.batchDraw.bind(this._frontLayer)
71
+ );
61
72
  }
62
73
 
63
74
  Axis.prototype._onPlayheadMoved = function(playheadX, playheadWidth) {
64
75
  this._maskStart = playheadX + this._view.getFrameOffset();
65
76
  this._maskEnd = playheadX + this._view.getFrameOffset() + playheadWidth;
66
- this._frontLayer.batchDraw();
77
+ this._throttledFrontDraw();
67
78
  };
68
79
 
69
80
  Axis.prototype._onPlayheadHidden = function() {
70
81
  this._maskStart = null;
71
82
  this._maskEnd = null;
72
- this._frontLayer.batchDraw();
83
+ this._throttledFrontDraw();
73
84
  };
74
85
 
75
86
  Axis.prototype.batchDraw = function() {
76
- this._backLayer.batchDraw();
77
- this._frontLayer.batchDraw();
87
+ this._throttledBackDraw();
88
+ this._throttledFrontDraw();
78
89
  };
79
90
 
80
91
  Axis.prototype.addBackToStage = function(stage) {
@@ -6,12 +6,22 @@
6
6
  * @module invoker
7
7
  */
8
8
 
9
- define([
9
+ define([
10
10
  ], function() {
11
11
  'use strict';
12
12
 
13
+ // Use requestAnimationFrame for smoother animations when available
14
+ var raf = (typeof window !== 'undefined' && window.requestAnimationFrame) ||
15
+ function(fn) {
16
+ return setTimeout(fn, 16);
17
+ };
18
+ var caf = (typeof window !== 'undefined' && window.cancelAnimationFrame) ||
19
+ function(id) {
20
+ clearTimeout(id);
21
+ };
22
+
13
23
  /**
14
- * An invoker class for throttling.
24
+ * An invoker class for throttling and performance optimization.
15
25
  *
16
26
  * @class
17
27
  * @alias Invoker
@@ -19,8 +29,14 @@
19
29
 
20
30
  function Invoker() {
21
31
  this._throttledFunc = {};
32
+ this._rafThrottled = {};
33
+ this._frameCallbacks = [];
34
+ this._frameScheduled = false;
22
35
  }
23
36
 
37
+ /**
38
+ * Throttle using setInterval - good for consistent timing
39
+ */
24
40
  Invoker.prototype.throttle = function(id, func, wait) {
25
41
  var self = this;
26
42
 
@@ -50,6 +66,78 @@
50
66
  }
51
67
  };
52
68
 
69
+ /**
70
+ * Throttle using requestAnimationFrame - ideal for visual updates
71
+ * Ensures function runs at most once per animation frame
72
+ */
73
+ Invoker.prototype.throttleRAF = function(id, func) {
74
+ var self = this;
75
+
76
+ if (this._rafThrottled[id]) {
77
+ // Update the function but don't schedule again
78
+ this._rafThrottled[id].func = func;
79
+ this._rafThrottled[id].pending = true;
80
+ return;
81
+ }
82
+
83
+ this._rafThrottled[id] = {
84
+ func: func,
85
+ pending: true,
86
+ rafId: null
87
+ };
88
+
89
+ function frame() {
90
+ if (self._rafThrottled[id] && self._rafThrottled[id].pending) {
91
+ self._rafThrottled[id].pending = false;
92
+ self._rafThrottled[id].func();
93
+ self._rafThrottled[id].rafId = raf(frame);
94
+ }
95
+ else if (self._rafThrottled[id]) {
96
+ delete self._rafThrottled[id];
97
+ }
98
+ }
99
+
100
+ this._rafThrottled[id].rafId = raf(frame);
101
+ };
102
+
103
+ /**
104
+ * Cancel a RAF-throttled function
105
+ */
106
+ Invoker.prototype.cancelThrottleRAF = function(id) {
107
+ if (this._rafThrottled[id]) {
108
+ if (this._rafThrottled[id].rafId) {
109
+ caf(this._rafThrottled[id].rafId);
110
+ }
111
+ delete this._rafThrottled[id];
112
+ }
113
+ };
114
+
115
+ /**
116
+ * Schedule a callback for the next animation frame
117
+ * Multiple callbacks are batched together
118
+ */
119
+ Invoker.prototype.scheduleFrame = function(callback) {
120
+ var self = this;
121
+
122
+ this._frameCallbacks.push(callback);
123
+
124
+ if (!this._frameScheduled) {
125
+ this._frameScheduled = true;
126
+ raf(function() {
127
+ self._frameScheduled = false;
128
+ var callbacks = self._frameCallbacks;
129
+
130
+ self._frameCallbacks = [];
131
+ for (var i = 0; i < callbacks.length; i++) {
132
+ callbacks[i]();
133
+ }
134
+ });
135
+ }
136
+ };
137
+
138
+ /**
139
+ * Creates a debounced function
140
+ */
53
141
  Invoker.prototype.debounce = function(func, wait, immediate) {
54
142
  var timeout;
55
143
 
@@ -77,5 +165,133 @@
77
165
  };
78
166
  };
79
167
 
168
+ /**
169
+ * Creates a leading-edge throttled function using RAF
170
+ * Executes immediately, then throttles subsequent calls
171
+ */
172
+ Invoker.prototype.throttleLeading = function(func) {
173
+ var scheduled = false;
174
+ var savedArgs = null;
175
+ var savedThis = null;
176
+
177
+ return function throttled() {
178
+ if (scheduled) {
179
+ savedArgs = arguments;
180
+ // eslint-disable-next-line consistent-this
181
+ savedThis = this;
182
+ return;
183
+ }
184
+
185
+ func.apply(this, arguments);
186
+ scheduled = true;
187
+
188
+ raf(function() {
189
+ scheduled = false;
190
+ if (savedArgs) {
191
+ func.apply(savedThis, savedArgs);
192
+ savedArgs = null;
193
+ savedThis = null;
194
+ }
195
+ });
196
+ };
197
+ };
198
+
199
+ /**
200
+ * Creates a trailing-edge throttled function using RAF
201
+ * Collects calls and executes only the last one per frame
202
+ */
203
+ Invoker.prototype.throttleTrailing = function(func) {
204
+ var rafId = null;
205
+ var savedArgs = null;
206
+ var savedThis = null;
207
+
208
+ return function throttled() {
209
+ savedArgs = arguments;
210
+ // eslint-disable-next-line consistent-this
211
+ savedThis = this;
212
+
213
+ if (rafId === null) {
214
+ rafId = raf(function() {
215
+ rafId = null;
216
+ func.apply(savedThis, savedArgs);
217
+ });
218
+ }
219
+ };
220
+ };
221
+
222
+ /**
223
+ * Batch multiple function calls into a single execution
224
+ * Useful for accumulating updates before rendering
225
+ */
226
+ Invoker.prototype.createBatcher = function(processFn, options) {
227
+ options = options || {};
228
+ var maxWait = options.maxWait || 100;
229
+ var items = [];
230
+ var timeout = null;
231
+ var lastFlush = 0;
232
+
233
+ function flush() {
234
+ if (items.length === 0) {
235
+ return;
236
+ }
237
+ var batch = items;
238
+
239
+ items = [];
240
+ timeout = null;
241
+ lastFlush = Date.now();
242
+ processFn(batch);
243
+ }
244
+
245
+ return {
246
+ add: function(item) {
247
+ items.push(item);
248
+
249
+ if (timeout === null) {
250
+ var timeSinceLastFlush = Date.now() - lastFlush;
251
+
252
+ if (timeSinceLastFlush >= maxWait) {
253
+ raf(flush);
254
+ }
255
+ else {
256
+ timeout = setTimeout(flush, Math.min(16, maxWait - timeSinceLastFlush));
257
+ }
258
+ }
259
+ },
260
+ flush: flush,
261
+ size: function() {
262
+ return items.length;
263
+ }
264
+ };
265
+ };
266
+
267
+ /**
268
+ * Destroy all pending operations
269
+ */
270
+ Invoker.prototype.destroy = function() {
271
+ var id;
272
+
273
+ // Clear interval-based throttles
274
+ for (id in this._throttledFunc) {
275
+ if (Object.prototype.hasOwnProperty.call(this._throttledFunc, id)) {
276
+ clearInterval(this._throttledFunc[id].timer);
277
+ }
278
+ }
279
+ this._throttledFunc = {};
280
+
281
+ // Clear RAF-based throttles
282
+ for (id in this._rafThrottled) {
283
+ if (Object.prototype.hasOwnProperty.call(this._rafThrottled, id)) {
284
+ if (this._rafThrottled[id].rafId) {
285
+ caf(this._rafThrottled[id].rafId);
286
+ }
287
+ }
288
+ }
289
+ this._rafThrottled = {};
290
+
291
+ // Clear frame callbacks
292
+ this._frameCallbacks = [];
293
+ this._frameScheduled = false;
294
+ };
295
+
80
296
  return Invoker;
81
297
  });
@@ -195,8 +195,12 @@ define([
195
195
  LineGroup.prototype.addSource = function(source, sourceGroup, sourcesAround) {
196
196
  if (sourceGroup) {
197
197
  this._sourcesGroup[source.id] = sourceGroup;
198
- if (!sourceGroup.getParent() || !sourceGroup.isDescendantOf(this._group)) {
199
- sourceGroup.moveTo(this._group);
198
+ // Only move to this group if not currently being dragged
199
+ // (during drag, source stays in drag layer for z-order)
200
+ if (!sourceGroup.isActive()) {
201
+ if (!sourceGroup.getParent() || !sourceGroup.isDescendantOf(this._group)) {
202
+ sourceGroup.moveTo(this._group);
203
+ }
200
204
  }
201
205
  }
202
206
 
@@ -342,10 +346,6 @@ define([
342
346
 
343
347
  delete this._sourcesGroup[source.id];
344
348
 
345
- if (sourceGroup) {
346
- sourceGroup.hideButKeepFocus();
347
- }
348
-
349
349
  return sourceGroup;
350
350
  };
351
351
 
@@ -30,6 +30,7 @@ define([
30
30
 
31
31
  this._automaticallyCreatedLineId = null;
32
32
  this._automaticLineCreationPosition = null;
33
+ this._automaticLineCreationMouseY = null;
33
34
  this._automaticLineCreationTimeout = null;
34
35
 
35
36
  this._segmentsGroups = {};
@@ -141,7 +142,6 @@ define([
141
142
 
142
143
  LineGroups.prototype._onLineHeightChanged = function() {
143
144
  this.refreshLineYs();
144
- this._view.updateTimeline();
145
145
  };
146
146
 
147
147
  LineGroups.prototype._onSourcesWrappingChanged = function(sources) {
@@ -282,10 +282,11 @@ define([
282
282
  }
283
283
  this._automaticLineCreationTimeout = null;
284
284
  this._automaticLineCreationPosition = null;
285
+ this._automaticLineCreationMouseY = null;
285
286
  this._automaticallyCreatedLineId = null;
286
287
  };
287
288
 
288
- LineGroups.prototype.manageAutomaticLineCreation = function(newLinePosition, initialPosition, sources) {
289
+ LineGroups.prototype.manageAutomaticLineCreation = function(newLinePosition, initialPosition, sources, mouseY) {
289
290
  if (!Utils.isNullOrUndefined(this._automaticallyCreatedLineId)) {
290
291
  return;
291
292
  }
@@ -301,6 +302,7 @@ define([
301
302
  }
302
303
 
303
304
  this._automaticLineCreationPosition = newLinePosition;
305
+ this._automaticLineCreationMouseY = mouseY;
304
306
  this._automaticLineCreationTimeout = setTimeout(function() {
305
307
  this._automaticLineCreationTimeout = null;
306
308
 
@@ -344,6 +346,9 @@ define([
344
346
 
345
347
  this._automaticallyCreatedLineId = automaticallyCreatedLineGroup.getId();
346
348
  this._moveSourcesToPositionIfPossible(sources, newLinePosition);
349
+
350
+ // Notify that sources have been moved to a new line (for ghost preview updates)
351
+ this._peaks.emit('sources.delayedLineChange', sources);
347
352
  }.bind(this), this._peaks.options.automaticLineCreationDelay);
348
353
  };
349
354
 
@@ -361,7 +366,7 @@ define([
361
366
  }
362
367
 
363
368
  if (linePos[0] !== linePos[1]) {
364
- this.manageAutomaticLineCreation(linePos[0] + 1, position, sources);
369
+ this.manageAutomaticLineCreation(linePos[0] + 1, position, sources, mouseY);
365
370
  }
366
371
  else {
367
372
  this.stopAutomaticLineCreation();
@@ -9,10 +9,12 @@
9
9
  define([
10
10
  'konva',
11
11
  './svgs',
12
+ './invoker',
12
13
  '../utils'
13
14
  ], function(
14
15
  Konva,
15
16
  SVGs,
17
+ Invoker,
16
18
  Utils) {
17
19
  'use strict';
18
20
 
@@ -31,6 +33,8 @@ define([
31
33
  this._view = view;
32
34
  this._container = container;
33
35
 
36
+ this._invoker = new Invoker();
37
+
34
38
  this._width = this._peaks.options.lineIndicatorWidth;
35
39
  this._height = this._view.getHeight();
36
40
 
@@ -58,6 +62,11 @@ define([
58
62
  this._layer = new Konva.Layer();
59
63
  this._stage.add(this._layer);
60
64
 
65
+ // Throttled draws to coalesce multiple updates per frame.
66
+ this._throttledBatchDraw = this._invoker.throttleTrailing(
67
+ this._layer.batchDraw.bind(this._layer)
68
+ );
69
+
61
70
  this._indicators = {};
62
71
 
63
72
  this._separatingLine = new Konva.Line({
@@ -69,7 +78,7 @@ define([
69
78
 
70
79
  this._layer.add(this._separatingLine);
71
80
 
72
- this._layer.batchDraw();
81
+ this.batchDraw();
73
82
 
74
83
  this._isDragging = false;
75
84
  this._dragLineId = null;
@@ -423,7 +432,7 @@ define([
423
432
  };
424
433
 
425
434
  LineIndicator.prototype.batchDraw = function() {
426
- this._layer.batchDraw();
435
+ this._throttledBatchDraw();
427
436
  };
428
437
 
429
438
  LineIndicator.prototype._onWindowMove = function(event) {
@@ -8,10 +8,11 @@
8
8
 
9
9
  define([
10
10
  './source-group',
11
+ './invoker',
11
12
  '../models/source',
12
13
  '../utils',
13
14
  'konva'
14
- ], function(SourceGroup, Source, Utils, Konva) {
15
+ ], function(SourceGroup, Invoker, Source, Utils, Konva) {
15
16
  'use strict';
16
17
 
17
18
  var TIME_X_OFFSET = 20;
@@ -35,6 +36,8 @@ define([
35
36
  this._playheadLayer = playheadLayer;
36
37
  this._stage = stage;
37
38
 
39
+ this._invoker = new Invoker();
40
+
38
41
  this._selectedElements = {};
39
42
  this._currentLine = null;
40
43
 
@@ -42,6 +45,11 @@ define([
42
45
  listening: this._mode !== 'default'
43
46
  });
44
47
 
48
+ // Throttled draws to coalesce multiple updates per frame.
49
+ this._throttledBatchDraw = this._invoker.throttleTrailing(
50
+ this._layer.batchDraw.bind(this._layer)
51
+ );
52
+
45
53
  this._prepareDefaultMode();
46
54
 
47
55
  this._onMouseClickInDefaultMode = this._onMouseClickInDefaultMode.bind(this);
@@ -59,6 +67,10 @@ define([
59
67
  this._peaks.on('handler.sources.destroy', this._onSourcesDestroy.bind(this));
60
68
  }
61
69
 
70
+ ModeLayer.prototype.batchDraw = function() {
71
+ this._throttledBatchDraw();
72
+ };
73
+
62
74
  ModeLayer.prototype._onSourcesDestroy = function(sources) {
63
75
  const selectedElementsToDeselect = {};
64
76
 
@@ -262,7 +274,7 @@ define([
262
274
  ModeLayer.prototype._onMouseEnterInCutMode = function() {
263
275
  this._cutGroup.visible(true);
264
276
 
265
- this._layer.batchDraw();
277
+ this.batchDraw();
266
278
  };
267
279
 
268
280
  ModeLayer.prototype._updateCursorTime = function(position) {
@@ -363,13 +375,13 @@ define([
363
375
  this._updateCursorTime(cuttingPosition);
364
376
  this._updateCuttingLine(cuttingPosition);
365
377
 
366
- this._layer.batchDraw();
378
+ this.batchDraw();
367
379
  };
368
380
 
369
381
  ModeLayer.prototype._onMouseLeaveInCutMode = function() {
370
382
  this._cutGroup.visible(false);
371
383
 
372
- this._layer.batchDraw();
384
+ this.batchDraw();
373
385
  };
374
386
 
375
387
  ModeLayer.prototype._onMouseClickInCutMode = function() {
@@ -414,18 +426,49 @@ define([
414
426
 
415
427
  this._cuttingLine.visible(false);
416
428
 
429
+ // Avoid keeping a reference to a SourceGroup that will be destroyed
430
+ // as part of the cut update.
431
+ this._view.setHoveredElement(null);
432
+
417
433
  this._peaks.cutSource(hoveredElement.getSource(), this._view.pixelsToTime(cuttingPixel));
418
434
 
419
- this._view.setHoveredElement(null);
435
+ // Cutting replaces Konva nodes under the cursor; Konva won't always emit
436
+ // enter/leave for the new nodes until the mouse moves. Re-sync hover now
437
+ // so subsequent leave events behave correctly.
438
+ this._syncHoveredElementFromPointer();
420
439
 
421
440
  this._updateCursorTime(cuttingPosition);
422
441
  this._updateCuttingLine(cuttingPosition);
423
442
 
424
- this._layer.batchDraw();
443
+ this.batchDraw();
425
444
  }
426
445
  }
427
446
  };
428
447
 
448
+ ModeLayer.prototype._syncHoveredElementFromPointer = function() {
449
+ var pointerPos = this._stage.getPointerPosition();
450
+
451
+ if (!pointerPos) {
452
+ return;
453
+ }
454
+
455
+ var node = this._stage.getIntersection(pointerPos);
456
+
457
+ while (node) {
458
+ if (node.attrs && node.attrs.sourceId) {
459
+ var sourceGroup = this._view.getSourceGroupById(node.attrs.sourceId);
460
+
461
+ if (sourceGroup && !sourceGroup.isHovered()) {
462
+ sourceGroup.startHover();
463
+ }
464
+
465
+ return;
466
+ }
467
+
468
+ node = node.getParent ? node.getParent() : null;
469
+ }
470
+ };
471
+
429
472
  ModeLayer.prototype.setMode = function(mode) {
430
473
  if (this._mode === mode) {
431
474
  return;
@@ -498,7 +541,7 @@ define([
498
541
  }
499
542
 
500
543
  this._mode = mode;
501
- this._layer.batchDraw();
544
+ this.batchDraw();
502
545
  this._view.batchDrawSourcesLayer();
503
546
  };
504
547
 
@@ -8,8 +8,9 @@
8
8
 
9
9
  define([
10
10
  '../utils',
11
+ './invoker',
11
12
  'konva'
12
- ], function(Utils, Konva) {
13
+ ], function(Utils, Invoker, Konva) {
13
14
  'use strict';
14
15
 
15
16
  var HANDLE_RADIUS = 10;
@@ -38,6 +39,11 @@ define([
38
39
 
39
40
  this._playheadLayer = new Konva.Layer();
40
41
 
42
+ this._invoker = new Invoker();
43
+ this._throttledBatchDraw = this._invoker.throttleTrailing(
44
+ this._playheadLayer.batchDraw.bind(this._playheadLayer)
45
+ );
46
+
41
47
  this._activeSegments = {};
42
48
  this._activeSources = {};
43
49
 
@@ -236,10 +242,14 @@ define([
236
242
 
237
243
  this._time = time;
238
244
  if (pixelHasChanged || timeHasChanged) {
239
- this._playheadLayer.batchDraw();
245
+ this.batchDraw();
240
246
  }
241
247
  };
242
248
 
249
+ PlayheadLayer.prototype.batchDraw = function() {
250
+ this._throttledBatchDraw();
251
+ };
252
+
243
253
  /**
244
254
  * Update cached pixel values and position of the playhead group.
245
255
  * @private
@@ -375,7 +385,7 @@ define([
375
385
  }
376
386
 
377
387
  if (updated) {
378
- this._playheadLayer.batchDraw();
388
+ this.batchDraw();
379
389
  }
380
390
  };
381
391