@checksub_team/peaks_timeline 2.2.1 → 2.3.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -10,12 +10,14 @@ define([
10
10
  './waveform-builder',
11
11
  './waveform-shape',
12
12
  './loader',
13
+ './invoker',
13
14
  '../utils',
14
15
  'konva'
15
16
  ], function(
16
17
  WaveformBuilder,
17
18
  WaveformShape,
18
19
  Loader,
20
+ Invoker,
19
21
  Utils,
20
22
  Konva) {
21
23
  'use strict';
@@ -24,7 +26,10 @@ define([
24
26
  var SPACING_BETWEEN_PREVIEWS = 1.5;
25
27
  var CORNER_RADIUS = 8;
26
28
  var INDICATOR_RADIUS = 4; // px
27
- var PREVIEW_CREATE_CHUNK = 8; // number of preview tiles to add per idle slice
29
+ var PREVIEW_CREATE_CHUNK = 12; // number of preview tiles to add per idle slice (increased for better throughput)
30
+
31
+ // Shared invoker for all source groups to coordinate updates
32
+ var sharedInvoker = new Invoker();
28
33
 
29
34
  /**
30
35
  * Creates a source group for the given source.
@@ -47,7 +52,6 @@ define([
47
52
 
48
53
  var self = this;
49
54
 
50
- this._x = this._view.timeToPixels(source.startTime);
51
55
  this._width = this._view.timeToPixels(source.endTime - source.startTime);
52
56
  var heights = SourceGroup.getHeights(source, peaks);
53
57
 
@@ -59,16 +63,22 @@ define([
59
63
  this._selected = this._source.selected;
60
64
  this._hovered = false;
61
65
  this._isDragged = false;
66
+ this._isHandleDragging = false;
62
67
  this._destroyed = false;
63
68
 
69
+ // Performance: track if draw is needed
70
+ this._drawScheduled = false;
71
+
64
72
  this._previewList = [];
65
- // internal queue state for async preview creation
66
- this._previewBuildQueue = new Set();
73
+ // internal queue state for async preview creation
74
+ this._previewBuildQueue = new Set();
75
+ // Track pending idle callbacks for cleanup
76
+ this._pendingIdleCallbacks = new Set();
67
77
 
68
78
  this._markersGroup = this._createMarkers();
69
79
 
70
80
  this._group = new Konva.Group({
71
- x: this._x,
81
+ x: this._view.timeToPixels(source.startTime),
72
82
  sourceId: this._source.id,
73
83
  draggable: this._source.draggable,
74
84
  dragBoundFunc: function() {
@@ -135,7 +145,7 @@ define([
135
145
  if (!this._source.loading) {
136
146
  this._showButtons();
137
147
  }
138
- this._batchDraw();
148
+ this._scheduleBatchDraw();
139
149
  };
140
150
 
141
151
  SourceGroup.prototype._onHoverEnd = function() {
@@ -143,7 +153,7 @@ define([
143
153
  this._manualHover = false;
144
154
  this._view.setHoveredElement(null);
145
155
  this._hideButtons();
146
- this._batchDraw();
156
+ this._scheduleBatchDraw();
147
157
  };
148
158
 
149
159
  SourceGroup.prototype._onDragStart = function(element) {
@@ -182,7 +192,7 @@ define([
182
192
  };
183
193
 
184
194
  SourceGroup.prototype.isActive = function() {
185
- return this._isDragged;
195
+ return this._isDragged || this._isHandleDragging;
186
196
  };
187
197
 
188
198
  SourceGroup.prototype.addToContent = function(newChild) {
@@ -202,14 +212,17 @@ define([
202
212
  this._rightHandle.x(this._width - handleWidth);
203
213
  };
204
214
 
205
- SourceGroup.prototype._onSourceGroupHandleDrag = function(draggedElement, dragPos, leftHandle) {
206
- const diff = this._view.pixelsToTime(dragPos.x - this._mouseDownX);
207
- const timeOffsetDiff = this._view.getTimeOffset() - this._initialTimeOffset;
208
-
215
+ SourceGroup.prototype._onSourceGroupHandleDrag = function(draggedElement, leftHandle) {
209
216
  const { start, end } = this._initialTimes;
210
217
 
211
218
  this._view.updateWithAutoScroll(
212
219
  function() {
220
+ var pointer = this._view.getPointerPosition();
221
+ var pointerX = pointer ? pointer.x : this._mouseDownX;
222
+
223
+ const diff = this._view.pixelsToTime(pointerX - this._mouseDownX);
224
+ const timeOffsetDiff = this._view.getTimeOffset() - this._initialTimeOffset;
225
+
213
226
  if (this._layer.manageSourceMovements(
214
227
  [this._source],
215
228
  leftHandle ? start + diff + timeOffsetDiff : null,
@@ -232,9 +245,13 @@ define([
232
245
  const frameOffset = this._view.timeToPixels(this._view.getTimeOffset());
233
246
  const newTimeToPixelsScale = this._view.getTimeToPixelsScale();
234
247
 
235
- this._group.x(startPixel - frameOffset);
236
-
237
- this._x = startPixel;
248
+ // When sources are being moved (dragged as blocks), their Konva positions are
249
+ // controlled by the drag handler so they follow the cursor.
250
+ // Auto-scroll triggers frequent `updateSources()` calls; avoid overwriting
251
+ // the drag position here to prevent jitter.
252
+ if (!this._isDragged) {
253
+ this._group.x(startPixel - frameOffset);
254
+ }
238
255
 
239
256
  const newWidth = endPixel - startPixel;
240
257
 
@@ -307,14 +324,20 @@ define([
307
324
  start: this._source.startTime,
308
325
  end: this._source.endTime
309
326
  };
310
- this._isDragged = true;
327
+ this._isHandleDragging = true;
311
328
 
312
329
  this._hideButtons();
313
330
  };
314
331
 
315
332
  SourceGroup.prototype._onHandleDragEnd = function() {
316
- this._isDragged = false;
333
+ this._isHandleDragging = false;
317
334
  this._showButtons();
335
+
336
+ // When resizing via handles, drag events no longer bubble to the parent
337
+ // group, so we won't hit the move-drag end path that calls prepareDragEnd.
338
+ // Normalize handle geometry here so handles snap to the updated width.
339
+ this.update();
340
+ this.prepareDragEnd();
318
341
  };
319
342
 
320
343
  SourceGroup.prototype._addHandles = function(forceCreate) {
@@ -328,14 +351,23 @@ define([
328
351
  height: this._unwrappedHeight,
329
352
  visible: true,
330
353
  draggable: this._source.resizable,
331
- dragBoundFunc: function(pos) {
332
- return self._onSourceGroupHandleDrag(this, pos, true);
354
+ dragBoundFunc: function() {
355
+ return self._onSourceGroupHandleDrag(this, true);
333
356
  }
334
357
  });
335
358
 
336
- this._leftHandle.on('dragstart', this._onHandleDragStart.bind(this));
359
+ // Prevent handle drag events from bubbling to the parent group.
360
+ // Otherwise the parent SourceGroup dragstart/dragend handlers run and
361
+ // the sources-layer creates move-drag ghosts during resize.
362
+ this._leftHandle.on('dragstart', function(event) {
363
+ event.cancelBubble = true;
364
+ self._onHandleDragStart(event);
365
+ });
337
366
 
338
- this._leftHandle.on('dragend', this._onHandleDragEnd.bind(this));
367
+ this._leftHandle.on('dragend', function(event) {
368
+ event.cancelBubble = true;
369
+ self._onHandleDragEnd(event);
370
+ });
339
371
 
340
372
  if (this._source.resizable) {
341
373
  this._leftHandle.on('mouseover', function() {
@@ -355,14 +387,23 @@ define([
355
387
  height: this._unwrappedHeight,
356
388
  visible: true,
357
389
  draggable: this._source.resizable,
358
- dragBoundFunc: function(pos) {
359
- return self._onSourceGroupHandleDrag(this, pos, false);
390
+ dragBoundFunc: function() {
391
+ return self._onSourceGroupHandleDrag(this, false);
360
392
  }
361
393
  });
362
394
 
363
- this._rightHandle.on('dragstart', this._onHandleDragStart.bind(this));
395
+ // Prevent handle drag events from bubbling to the parent group.
396
+ // Otherwise the parent SourceGroup dragstart/dragend handlers run and
397
+ // the sources-layer creates move-drag ghosts during resize.
398
+ this._rightHandle.on('dragstart', function(event) {
399
+ event.cancelBubble = true;
400
+ self._onHandleDragStart(event);
401
+ });
364
402
 
365
- this._rightHandle.on('dragend', this._onHandleDragEnd.bind(this));
403
+ this._rightHandle.on('dragend', function(event) {
404
+ event.cancelBubble = true;
405
+ self._onHandleDragEnd(event);
406
+ });
366
407
 
367
408
  if (this._source.resizable) {
368
409
  this._rightHandle.on('mouseover', function() {
@@ -425,17 +466,20 @@ define([
425
466
  )
426
467
  )
427
468
  );
469
+
470
+ var actualX = this._group.x() + this._view.getFrameOffset();
428
471
  var x = Math.max(
429
472
  0,
430
- this._view.getFrameOffset() - this._x - 2 * radius
473
+ this._view.getFrameOffset() - actualX - 2 * radius
431
474
  );
432
475
  var width = Math.min(
433
476
  this._width - x,
434
477
  this._view.getWidth() + 4 * radius - Math.max(
435
478
  0,
436
- this._x - this._view.getFrameOffset()
479
+ actualX - this._view.getFrameOffset()
437
480
  )
438
481
  );
482
+
439
483
  var xWidth = x + width;
440
484
 
441
485
  if (width > 0) {
@@ -678,10 +722,6 @@ define([
678
722
  return this._width;
679
723
  };
680
724
 
681
- SourceGroup.prototype.getX = function() {
682
- return this._x;
683
- };
684
-
685
725
  SourceGroup.prototype.getAbsoluteY = function() {
686
726
  return this._group.absolutePosition().y;
687
727
  };
@@ -700,6 +740,13 @@ define([
700
740
  return this._group.y(value);
701
741
  };
702
742
 
743
+ SourceGroup.prototype.absolutePosition = function(value) {
744
+ if (value) {
745
+ return this._group.absolutePosition(value);
746
+ }
747
+ return this._group.absolutePosition();
748
+ };
749
+
703
750
  SourceGroup.prototype.getSource = function() {
704
751
  return this._source;
705
752
  };
@@ -713,6 +760,10 @@ define([
713
760
  this._group.fire('mouseleave', { evt: new MouseEvent('mouseleave') }, true);
714
761
  };
715
762
 
763
+ SourceGroup.prototype.setDragging = function(isDragging) {
764
+ this._isDragged = isDragging;
765
+ };
766
+
716
767
  SourceGroup.prototype.startDrag = function() {
717
768
  return this._group.startDrag();
718
769
  };
@@ -725,12 +776,12 @@ define([
725
776
  this._group.moveTo(group);
726
777
  };
727
778
 
728
- SourceGroup.prototype.isDescendantOf = function(group) {
729
- return group.isAncestorOf(this._group);
779
+ SourceGroup.prototype.moveToTop = function() {
780
+ this._group.moveToTop();
730
781
  };
731
782
 
732
- SourceGroup.prototype.hideButKeepFocus = function() {
733
- this._group.moveTo(this._view.getTempGroup());
783
+ SourceGroup.prototype.isDescendantOf = function(group) {
784
+ return group.isAncestorOf(this._group);
734
785
  };
735
786
 
736
787
  SourceGroup.prototype.getParent = function() {
@@ -878,19 +929,38 @@ define([
878
929
  };
879
930
 
880
931
  SourceGroup.prototype._createAudioPreview = function(preview, redraw) {
932
+ var url = preview.url;
933
+
934
+ // Resample the waveform data for the current zoom level if needed
935
+ var scaledData = this._layer.getLoadedData(url + '-scaled');
936
+ var currentScale = this._view.getTimeToPixelsScale();
937
+
938
+ if (scaledData && scaledData.scale !== currentScale) {
939
+ var originalData = this._layer.getLoadedData(url);
940
+
941
+ if (originalData) {
942
+ this._layer.setLoadedData(url + '-scaled', {
943
+ data: originalData.resample({
944
+ scale: originalData.sample_rate / currentScale
945
+ }),
946
+ scale: currentScale
947
+ });
948
+ }
949
+ }
950
+
881
951
  var waveform = new WaveformShape({
882
952
  layer: this._layer,
883
953
  view: this._view,
884
954
  source: this._source,
885
955
  height: preview.group.height(),
886
- url: preview.url
956
+ url: url
887
957
  });
888
958
 
889
959
  preview.group.add(waveform);
890
960
  this._addToUnwrap(preview.group);
891
961
 
892
962
  if (redraw) {
893
- this._layer.rescale(true);
963
+ this._scheduleBatchDraw();
894
964
  }
895
965
 
896
966
  this._previewList.push(preview);
@@ -991,6 +1061,26 @@ define([
991
1061
  });
992
1062
  };
993
1063
 
1064
+ /**
1065
+ * Schedules a batch draw using RAF to coalesce multiple draw requests.
1066
+ * This is more efficient than calling _batchDraw directly.
1067
+ */
1068
+ SourceGroup.prototype._scheduleBatchDraw = function() {
1069
+ if (this._destroyed || this._drawScheduled) {
1070
+ return;
1071
+ }
1072
+
1073
+ this._drawScheduled = true;
1074
+ var self = this;
1075
+
1076
+ sharedInvoker.scheduleFrame(function() {
1077
+ self._drawScheduled = false;
1078
+ if (!self._destroyed) {
1079
+ self._batchDraw();
1080
+ }
1081
+ });
1082
+ };
1083
+
994
1084
  SourceGroup.prototype._batchDraw = function() {
995
1085
  var layer = this._group && this._group.getLayer && this._group.getLayer();
996
1086
 
@@ -999,31 +1089,16 @@ define([
999
1089
  }
1000
1090
  };
1001
1091
 
1002
- // Utility to schedule work during idle time or next frame
1092
+ // Utility to schedule work during idle time, with tracking for cleanup
1003
1093
  SourceGroup.prototype._scheduleIdle = function(fn) {
1004
- if (typeof window !== 'undefined' && window.requestIdleCallback) {
1005
- return window.requestIdleCallback(fn, { timeout: 50 });
1006
- }
1007
-
1008
- if (typeof window !== 'undefined' && window.requestAnimationFrame) {
1009
- return window.requestAnimationFrame(function() {
1010
- fn({
1011
- timeRemaining: function() {
1012
- return 0;
1013
- },
1014
- didTimeout: true
1015
- });
1016
- });
1017
- }
1094
+ var self = this;
1095
+ var id = Utils.scheduleIdle(function(deadline) {
1096
+ self._pendingIdleCallbacks.delete(id);
1097
+ fn(deadline);
1098
+ }, { timeout: 50 });
1018
1099
 
1019
- return setTimeout(function() {
1020
- fn({
1021
- timeRemaining: function() {
1022
- return 0;
1023
- },
1024
- didTimeout: true
1025
- });
1026
- }, 0);
1100
+ this._pendingIdleCallbacks.add(id);
1101
+ return id;
1027
1102
  };
1028
1103
 
1029
1104
  SourceGroup.prototype._ensureImagePreviewCount = function(preview, targetCount, interImageSpacing) {
@@ -1038,7 +1113,7 @@ define([
1038
1113
  }
1039
1114
 
1040
1115
  if (currentCount >= targetCount || this._previewBuildQueue.has(preview)) {
1041
- this._batchDraw();
1116
+ this._scheduleBatchDraw();
1042
1117
  return;
1043
1118
  }
1044
1119
 
@@ -1066,7 +1141,7 @@ define([
1066
1141
  added += 1;
1067
1142
  }
1068
1143
 
1069
- self._batchDraw();
1144
+ self._scheduleBatchDraw();
1070
1145
 
1071
1146
  if (nextIndex < targetCount) {
1072
1147
  self._scheduleIdle(buildChunk);
@@ -1111,7 +1186,7 @@ define([
1111
1186
  this._ensureImagePreviewCount(preview, targetCount, interImageSpacing);
1112
1187
 
1113
1188
  if (redraw) {
1114
- this._batchDraw();
1189
+ this._scheduleBatchDraw();
1115
1190
  }
1116
1191
 
1117
1192
  this._previewList.push(preview);
@@ -1684,7 +1759,7 @@ define([
1684
1759
  self._source.volume = Math.max(self._source.volumeRange[0], Math.min(volume, self._source.volumeRange[1]));
1685
1760
  self._peaks.emit('source.volumeChanged', self._source);
1686
1761
 
1687
- self._batchDraw();
1762
+ self._scheduleBatchDraw();
1688
1763
  });
1689
1764
 
1690
1765
  volumeSliderGroup.on('dragend', function() {
@@ -1707,6 +1782,19 @@ define([
1707
1782
  };
1708
1783
 
1709
1784
  SourceGroup.prototype.destroy = function() {
1785
+ // Cancel any pending idle callbacks to prevent memory leaks
1786
+ if (this._pendingIdleCallbacks) {
1787
+ this._pendingIdleCallbacks.forEach(function(id) {
1788
+ Utils.cancelIdle(id);
1789
+ });
1790
+ this._pendingIdleCallbacks.clear();
1791
+ }
1792
+
1793
+ // Clear preview build queue
1794
+ if (this._previewBuildQueue) {
1795
+ this._previewBuildQueue.clear();
1796
+ }
1797
+
1710
1798
  if (this._buttonsAnimation) {
1711
1799
  this._buttonsAnimation.destroy();
1712
1800
  this._buttonsAnimation = null;