@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.
- package/package.json +1 -1
- package/peaks.js +650 -137
- package/src/components/invoker.js +218 -2
- package/src/components/line-group.js +6 -6
- package/src/components/line-groups.js +40 -3
- package/src/components/source-group.js +152 -64
- package/src/components/sources-layer.js +397 -41
- package/src/components/waveform-builder.js +27 -9
- package/src/utils.js +90 -0
- package/src/view.js +90 -49
|
@@ -39,16 +39,28 @@ define([
|
|
|
39
39
|
this._allowEditing = allowEditing;
|
|
40
40
|
this._sourcesGroup = {};
|
|
41
41
|
this._layer = new Konva.Layer();
|
|
42
|
+
// Separate layer for dragged elements - always renders on top of sources layer
|
|
43
|
+
this._dragLayer = new Konva.Layer();
|
|
42
44
|
this._dataRetriever = new DataRetriever(peaks);
|
|
43
45
|
this._lineGroups = new LineGroups(peaks, view, this);
|
|
44
46
|
this._lineGroups.addToLayer(this);
|
|
45
47
|
|
|
46
48
|
this._loadedData = {};
|
|
47
49
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
+
// Create invoker for performance optimizations
|
|
51
|
+
this._invoker = new Invoker();
|
|
52
|
+
|
|
53
|
+
// Version counter for cancellable rescale operations
|
|
54
|
+
this._rescaleVersion = 0;
|
|
55
|
+
|
|
56
|
+
// Throttled batch draw to prevent excessive redraws
|
|
57
|
+
this._throttledBatchDraw = this._invoker.throttleTrailing(
|
|
58
|
+
this._layer.batchDraw.bind(this._layer)
|
|
50
59
|
);
|
|
51
60
|
|
|
61
|
+
// Pending draw flag to coalesce multiple draw requests
|
|
62
|
+
this._drawPending = false;
|
|
63
|
+
|
|
52
64
|
this._peaks.on('handler.sources.add', this._onSourcesAdd.bind(this));
|
|
53
65
|
this._peaks.on('handler.sources.destroy', this._onSourcesDestroy.bind(this));
|
|
54
66
|
this._peaks.on('handler.sources.show', this._onSourcesShow.bind(this));
|
|
@@ -59,8 +71,16 @@ define([
|
|
|
59
71
|
this._peaks.on('handler.segments.show', this._onSegmentsShow.bind(this));
|
|
60
72
|
this._peaks.on('model.source.setIndicators', this.setIndicators.bind(this));
|
|
61
73
|
this._peaks.on('handler.view.mouseup', this._stopDrag.bind(this));
|
|
74
|
+
this._peaks.on('sources.delayedLineChange', this._onSourcesDelayedLineChanged.bind(this));
|
|
62
75
|
}
|
|
63
76
|
|
|
77
|
+
SourcesLayer.prototype._onSourcesDelayedLineChanged = function() {
|
|
78
|
+
// Update dragged source groups when sources change line after a delay (e.g., after automatic line creation)
|
|
79
|
+
if (this._dragGhosts && this._draggedElements && this._draggedElements.length > 0) {
|
|
80
|
+
this._dragSourcesGroup();
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
|
|
64
84
|
SourcesLayer.prototype._stopDrag = function() {
|
|
65
85
|
const draggedSourceGroup = this._sourcesGroup[this._draggedElementId];
|
|
66
86
|
|
|
@@ -97,6 +117,7 @@ define([
|
|
|
97
117
|
|
|
98
118
|
SourcesLayer.prototype.addToStage = function(stage) {
|
|
99
119
|
stage.add(this._layer);
|
|
120
|
+
stage.add(this._dragLayer);
|
|
100
121
|
};
|
|
101
122
|
|
|
102
123
|
SourcesLayer.prototype.enableEditing = function(enable) {
|
|
@@ -190,7 +211,7 @@ define([
|
|
|
190
211
|
|
|
191
212
|
this._view.updateTimelineLength();
|
|
192
213
|
|
|
193
|
-
this.
|
|
214
|
+
this.batchDraw();
|
|
194
215
|
};
|
|
195
216
|
|
|
196
217
|
SourcesLayer.prototype._onSourcesShow = function(sources) {
|
|
@@ -200,7 +221,7 @@ define([
|
|
|
200
221
|
self._sourcesGroup[source.id].setWrapping(false, true);
|
|
201
222
|
});
|
|
202
223
|
|
|
203
|
-
this.
|
|
224
|
+
this.batchDraw();
|
|
204
225
|
};
|
|
205
226
|
|
|
206
227
|
SourcesLayer.prototype._onSourcesHide = function(sources) {
|
|
@@ -210,7 +231,7 @@ define([
|
|
|
210
231
|
self._sourcesGroup[source.id].setWrapping(true, true);
|
|
211
232
|
});
|
|
212
233
|
|
|
213
|
-
this.
|
|
234
|
+
this.batchDraw();
|
|
214
235
|
};
|
|
215
236
|
|
|
216
237
|
SourcesLayer.prototype._onDataRetrieved = function(data, source, url) {
|
|
@@ -243,7 +264,7 @@ define([
|
|
|
243
264
|
SourcesLayer.prototype._onSegmentsShow = function(segmentsGroupId, lineId) {
|
|
244
265
|
this._lineGroups.addSegments(segmentsGroupId, lineId);
|
|
245
266
|
this._view.updateTimelineLength();
|
|
246
|
-
this.
|
|
267
|
+
this.batchDraw();
|
|
247
268
|
};
|
|
248
269
|
|
|
249
270
|
/**
|
|
@@ -285,12 +306,13 @@ define([
|
|
|
285
306
|
|
|
286
307
|
if (sourceGroup) {
|
|
287
308
|
sourceGroup.createIndicators();
|
|
288
|
-
this.
|
|
309
|
+
this.batchDraw();
|
|
289
310
|
}
|
|
290
311
|
};
|
|
291
312
|
|
|
292
313
|
/**
|
|
293
314
|
* Updates the positions of all displayed sources in the view.
|
|
315
|
+
* Uses optimized batching to reduce draw calls.
|
|
294
316
|
*
|
|
295
317
|
* @param {Number} startTime The start of the visible range in the view,
|
|
296
318
|
* in seconds.
|
|
@@ -307,12 +329,16 @@ define([
|
|
|
307
329
|
|
|
308
330
|
var count = sources.length;
|
|
309
331
|
|
|
310
|
-
sources
|
|
332
|
+
// Batch update all sources
|
|
333
|
+
for (var i = 0; i < sources.length; i++) {
|
|
334
|
+
this._updateSource(sources[i]);
|
|
335
|
+
}
|
|
311
336
|
|
|
312
337
|
count += this._removeInvisibleSources(startTime, endTime);
|
|
313
338
|
|
|
314
339
|
if (count > 0) {
|
|
315
|
-
|
|
340
|
+
// Use throttled batch draw for consistent performance
|
|
341
|
+
this.batchDraw();
|
|
316
342
|
}
|
|
317
343
|
};
|
|
318
344
|
|
|
@@ -348,23 +374,85 @@ define([
|
|
|
348
374
|
orderable: true,
|
|
349
375
|
draggable: true
|
|
350
376
|
});
|
|
377
|
+
|
|
378
|
+
// Store initial source positions and create ghost previews
|
|
379
|
+
var self = this;
|
|
380
|
+
|
|
381
|
+
this._dragGhosts = [];
|
|
382
|
+
this._initialSourcePositions = {};
|
|
383
|
+
|
|
384
|
+
this._draggedElements.forEach(function(source) {
|
|
385
|
+
// Store initial position for ALL dragged sources (even those outside view)
|
|
386
|
+
// This is needed for time calculations during drag
|
|
387
|
+
self._initialSourcePositions[source.id] = {
|
|
388
|
+
startTime: source.startTime,
|
|
389
|
+
endTime: source.endTime,
|
|
390
|
+
lineId: source.lineId
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
var sourceGroup = self._sourcesGroup[source.id];
|
|
394
|
+
|
|
395
|
+
if (sourceGroup) {
|
|
396
|
+
// Mark as dragging (for all sources, not just the clicked one)
|
|
397
|
+
sourceGroup.setDragging(true);
|
|
398
|
+
|
|
399
|
+
// Get absolute Y position before moving (relative to line group)
|
|
400
|
+
var absoluteY = sourceGroup.getAbsoluteY();
|
|
401
|
+
|
|
402
|
+
// Move source to the drag layer so it draws above ALL other sources/segments
|
|
403
|
+
sourceGroup.moveTo(self._dragLayer);
|
|
404
|
+
// Restore the Y position (now relative to drag layer, which is at y=0)
|
|
405
|
+
sourceGroup.y(absoluteY);
|
|
406
|
+
|
|
407
|
+
// Create ghost preview
|
|
408
|
+
var ghost = self._createDragGhost(sourceGroup);
|
|
409
|
+
|
|
410
|
+
self._dragGhosts.push({
|
|
411
|
+
ghost: ghost,
|
|
412
|
+
sourceId: source.id
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
});
|
|
351
416
|
};
|
|
352
417
|
|
|
353
418
|
SourcesLayer.prototype.onSourcesGroupDragEnd = function() {
|
|
419
|
+
// Clean up ghost previews
|
|
420
|
+
if (this._dragGhosts) {
|
|
421
|
+
this._dragGhosts.forEach(function(item) {
|
|
422
|
+
if (item.ghost) {
|
|
423
|
+
item.ghost.destroy();
|
|
424
|
+
}
|
|
425
|
+
});
|
|
426
|
+
this._dragGhosts = null;
|
|
427
|
+
}
|
|
428
|
+
this._initialSourcePositions = null;
|
|
429
|
+
this._dragOffsetX = undefined;
|
|
430
|
+
this._dragOffsetY = undefined;
|
|
431
|
+
|
|
432
|
+
var self = this;
|
|
433
|
+
|
|
354
434
|
const updatedSources = this._draggedElements.map(
|
|
355
435
|
function(source) {
|
|
356
|
-
const sourceGroup =
|
|
436
|
+
const sourceGroup = self._sourcesGroup[source.id];
|
|
357
437
|
|
|
358
438
|
if (sourceGroup) {
|
|
439
|
+
// Clear dragging state before moving back to line group
|
|
440
|
+
sourceGroup.setDragging(false);
|
|
359
441
|
sourceGroup.prepareDragEnd();
|
|
442
|
+
// Move source back to its line group (it was moved to layer during drag)
|
|
443
|
+
self._lineGroups.addSource(source, sourceGroup);
|
|
444
|
+
// Reset Y position to 0 relative to parent line group
|
|
445
|
+
// (source followed cursor during drag, now snap to line position)
|
|
446
|
+
sourceGroup.y(0);
|
|
360
447
|
}
|
|
361
448
|
|
|
362
449
|
return source;
|
|
363
|
-
}
|
|
450
|
+
}
|
|
364
451
|
);
|
|
365
452
|
|
|
366
453
|
this._draggedElementId = null;
|
|
367
454
|
|
|
455
|
+
this.refresh();
|
|
368
456
|
this._view.batchDrawSourcesLayer();
|
|
369
457
|
this._view.updateTimelineLength();
|
|
370
458
|
|
|
@@ -372,8 +460,72 @@ define([
|
|
|
372
460
|
};
|
|
373
461
|
|
|
374
462
|
SourcesLayer.prototype.onSourcesGroupDrag = function(draggedElement) {
|
|
463
|
+
var pointerPos = this._view.getPointerPosition();
|
|
464
|
+
|
|
375
465
|
this._view.updateWithAutoScroll(this._dragSourcesGroup.bind(this));
|
|
376
466
|
|
|
467
|
+
// Return position that follows the mouse cursor exactly
|
|
468
|
+
// The ghost preview shows where it will actually be placed
|
|
469
|
+
var clickedSourceGroup = this._sourcesGroup[this._draggedElementId];
|
|
470
|
+
|
|
471
|
+
if (clickedSourceGroup) {
|
|
472
|
+
var mouseX = pointerPos.x;
|
|
473
|
+
var mouseY = pointerPos.y;
|
|
474
|
+
var offsetX = this._dragOffsetX || 0;
|
|
475
|
+
var offsetY = this._dragOffsetY || 0;
|
|
476
|
+
|
|
477
|
+
// Calculate offset on first drag if not set
|
|
478
|
+
if (this._dragOffsetX === undefined) {
|
|
479
|
+
var currentPos = draggedElement.absolutePosition();
|
|
480
|
+
|
|
481
|
+
this._dragOffsetX = currentPos.x - mouseX;
|
|
482
|
+
this._dragOffsetY = currentPos.y - mouseY;
|
|
483
|
+
offsetX = this._dragOffsetX;
|
|
484
|
+
offsetY = this._dragOffsetY;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
var clickedSourceX = mouseX + offsetX;
|
|
488
|
+
var clickedSourceY = mouseY + offsetY;
|
|
489
|
+
|
|
490
|
+
// Position all other dragged sources relative to the clicked source
|
|
491
|
+
// They should maintain their initial relative positions
|
|
492
|
+
if (this._draggedElements && this._draggedElements.length > 1 && this._initialSourcePositions) {
|
|
493
|
+
var self = this;
|
|
494
|
+
var clickedInitialPos = this._initialSourcePositions[this._draggedElementId];
|
|
495
|
+
|
|
496
|
+
if (clickedInitialPos) {
|
|
497
|
+
var clickedInitialPixelX = this._view.timeToPixels(clickedInitialPos.startTime);
|
|
498
|
+
|
|
499
|
+
this._draggedElements.forEach(function(source) {
|
|
500
|
+
if (source.id !== self._draggedElementId) {
|
|
501
|
+
var sourceGroup = self._sourcesGroup[source.id];
|
|
502
|
+
|
|
503
|
+
if (sourceGroup) {
|
|
504
|
+
var initialPos = self._initialSourcePositions[source.id];
|
|
505
|
+
|
|
506
|
+
if (initialPos) {
|
|
507
|
+
// Calculate pixel offset from clicked source's initial position
|
|
508
|
+
var initialPixelX = self._view.timeToPixels(initialPos.startTime);
|
|
509
|
+
var pixelOffset = initialPixelX - clickedInitialPixelX;
|
|
510
|
+
|
|
511
|
+
// Position this source relative to clicked source's current position
|
|
512
|
+
sourceGroup.absolutePosition({
|
|
513
|
+
x: clickedSourceX + pixelOffset,
|
|
514
|
+
y: clickedSourceY
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
return {
|
|
524
|
+
x: clickedSourceX,
|
|
525
|
+
y: clickedSourceY
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
|
|
377
529
|
return {
|
|
378
530
|
x: draggedElement.absolutePosition().x,
|
|
379
531
|
y: draggedElement.absolutePosition().y
|
|
@@ -399,17 +551,23 @@ define([
|
|
|
399
551
|
return;
|
|
400
552
|
}
|
|
401
553
|
|
|
554
|
+
var newStartTime = Utils.roundTime(initialStartTime + timeOffsetDiff + timeDiff);
|
|
555
|
+
var newEndTime = Utils.roundTime(initialEndTime + timeOffsetDiff + timeDiff);
|
|
556
|
+
|
|
402
557
|
const shouldRedraw = this.manageSourceMovements(
|
|
403
558
|
this._draggedElements,
|
|
404
|
-
|
|
405
|
-
|
|
559
|
+
newStartTime,
|
|
560
|
+
newEndTime,
|
|
406
561
|
orderable,
|
|
407
562
|
mousePosX,
|
|
408
563
|
mousePosY
|
|
409
564
|
);
|
|
410
565
|
|
|
566
|
+
this._updateDragGhosts();
|
|
567
|
+
|
|
411
568
|
if (shouldRedraw) {
|
|
412
569
|
this.batchDraw();
|
|
570
|
+
this._dragLayer.batchDraw();
|
|
413
571
|
}
|
|
414
572
|
};
|
|
415
573
|
|
|
@@ -424,8 +582,112 @@ define([
|
|
|
424
582
|
);
|
|
425
583
|
};
|
|
426
584
|
|
|
427
|
-
|
|
428
|
-
|
|
585
|
+
/**
|
|
586
|
+
* Creates a ghost preview element for a source being dragged.
|
|
587
|
+
* The ghost shows where the source will be placed when released.
|
|
588
|
+
*
|
|
589
|
+
* @private
|
|
590
|
+
* @param {SourceGroup} sourceGroup The source group to create a ghost for
|
|
591
|
+
* @returns {Konva.Rect} The ghost preview element
|
|
592
|
+
*/
|
|
593
|
+
SourcesLayer.prototype._createDragGhost = function(sourceGroup) {
|
|
594
|
+
var source = sourceGroup.getSource();
|
|
595
|
+
var frameOffset = this._view.getFrameOffset();
|
|
596
|
+
var x = this._view.timeToPixels(source.startTime) - frameOffset;
|
|
597
|
+
var width = this._view.timeToPixels(source.endTime - source.startTime);
|
|
598
|
+
var height = sourceGroup.getCurrentHeight();
|
|
599
|
+
var y = sourceGroup.getAbsoluteY();
|
|
600
|
+
|
|
601
|
+
var ghost = new Konva.Rect({
|
|
602
|
+
x: x,
|
|
603
|
+
y: y,
|
|
604
|
+
width: width,
|
|
605
|
+
height: height,
|
|
606
|
+
fill: source.backgroundColor,
|
|
607
|
+
opacity: 0.4,
|
|
608
|
+
stroke: source.selectedBorderColor,
|
|
609
|
+
strokeWidth: 2,
|
|
610
|
+
dash: [8, 4],
|
|
611
|
+
cornerRadius: 8,
|
|
612
|
+
listening: false
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
this._layer.add(ghost);
|
|
616
|
+
ghost.moveToBottom();
|
|
617
|
+
|
|
618
|
+
return ghost;
|
|
619
|
+
};
|
|
620
|
+
|
|
621
|
+
/**
|
|
622
|
+
* Updates the positions of all drag ghost previews.
|
|
623
|
+
*
|
|
624
|
+
* @private
|
|
625
|
+
*/
|
|
626
|
+
SourcesLayer.prototype._updateDragGhosts = function() {
|
|
627
|
+
if (!this._dragGhosts) {
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
var self = this;
|
|
632
|
+
var frameOffset = this._view.getFrameOffset();
|
|
633
|
+
var lineGroupsById = this._lineGroups.getLineGroupsById();
|
|
634
|
+
|
|
635
|
+
this._dragGhosts.forEach(function(item) {
|
|
636
|
+
var sourceGroup = self._sourcesGroup[item.sourceId];
|
|
637
|
+
|
|
638
|
+
if (!sourceGroup || !item.ghost) {
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// Use current source times (updated during drag)
|
|
643
|
+
var sourceData = sourceGroup.getSource();
|
|
644
|
+
var x = self._view.timeToPixels(sourceData.startTime) - frameOffset;
|
|
645
|
+
|
|
646
|
+
item.ghost.x(x);
|
|
647
|
+
|
|
648
|
+
// Update Y position based on line
|
|
649
|
+
var lineGroup = lineGroupsById[sourceData.lineId];
|
|
650
|
+
|
|
651
|
+
if (lineGroup) {
|
|
652
|
+
item.ghost.y(lineGroup.y());
|
|
653
|
+
}
|
|
654
|
+
});
|
|
655
|
+
};
|
|
656
|
+
|
|
657
|
+
/**
|
|
658
|
+
* Updates source times during drag using initial positions.
|
|
659
|
+
*
|
|
660
|
+
* @private
|
|
661
|
+
* @param {Number} newStartTime The new start time for the first source
|
|
662
|
+
*/
|
|
663
|
+
SourcesLayer.prototype._updateSourceTimesDuringDrag = function(newStartTime) {
|
|
664
|
+
if (!this._initialSourcePositions || !this._draggedElements) {
|
|
665
|
+
return;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
var self = this;
|
|
669
|
+
var firstSourceInitial = this._initialSourcePositions[this._draggedElements[0].id];
|
|
670
|
+
|
|
671
|
+
if (!firstSourceInitial) {
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
// Calculate time diff from INITIAL position, not current position
|
|
676
|
+
var timeDiff = Utils.roundTime(newStartTime - firstSourceInitial.startTime);
|
|
677
|
+
|
|
678
|
+
this._draggedElements.forEach(function(source) {
|
|
679
|
+
var initialPos = self._initialSourcePositions[source.id];
|
|
680
|
+
|
|
681
|
+
if (initialPos) {
|
|
682
|
+
source.updateTimes(
|
|
683
|
+
Utils.roundTime(initialPos.startTime + timeDiff),
|
|
684
|
+
Utils.roundTime(initialPos.endTime + timeDiff)
|
|
685
|
+
);
|
|
686
|
+
}
|
|
687
|
+
});
|
|
688
|
+
};
|
|
689
|
+
|
|
690
|
+
SourcesLayer.prototype._applyTimeChangesToSources = function(sources, initialStartTime, newStartTime, newEndTime
|
|
429
691
|
) {
|
|
430
692
|
if (sources.length === 1) {
|
|
431
693
|
sources[0].updateTimes(
|
|
@@ -434,18 +696,49 @@ define([
|
|
|
434
696
|
);
|
|
435
697
|
}
|
|
436
698
|
else {
|
|
437
|
-
//
|
|
438
|
-
//
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
if (
|
|
443
|
-
sources.
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
}
|
|
699
|
+
// When moving multiple sources via block-drag, compute time diff from the
|
|
700
|
+
// drag-start positions to avoid cumulative drift as times are updated.
|
|
701
|
+
var canUseInitialPositions = Boolean(this._initialSourcePositions
|
|
702
|
+
&& this._initialSourcePositions[sources[0].id]);
|
|
703
|
+
|
|
704
|
+
if (canUseInitialPositions) {
|
|
705
|
+
for (var i = 0; i < sources.length; i++) {
|
|
706
|
+
if (!this._initialSourcePositions[sources[i].id]) {
|
|
707
|
+
canUseInitialPositions = false;
|
|
708
|
+
break;
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
if (canUseInitialPositions) {
|
|
714
|
+
var firstInitial = this._initialSourcePositions[sources[0].id];
|
|
715
|
+
var timeDiffFromInitial = Utils.roundTime(newStartTime - firstInitial.startTime);
|
|
716
|
+
|
|
717
|
+
if (timeDiffFromInitial !== 0) {
|
|
718
|
+
var self = this;
|
|
719
|
+
|
|
720
|
+
sources.forEach(function(source) {
|
|
721
|
+
var initialPos = self._initialSourcePositions[source.id];
|
|
722
|
+
|
|
723
|
+
source.updateTimes(
|
|
724
|
+
Utils.roundTime(initialPos.startTime + timeDiffFromInitial),
|
|
725
|
+
Utils.roundTime(initialPos.endTime + timeDiffFromInitial)
|
|
726
|
+
);
|
|
727
|
+
});
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
else {
|
|
731
|
+
// Fallback for non-drag multi-updates.
|
|
732
|
+
const timeDiff = Utils.roundTime(newStartTime - initialStartTime);
|
|
733
|
+
|
|
734
|
+
if (timeDiff !== 0) {
|
|
735
|
+
sources.forEach(function(source) {
|
|
736
|
+
source.updateTimes(
|
|
737
|
+
Utils.roundTime(source.startTime + timeDiff),
|
|
738
|
+
Utils.roundTime(source.endTime + timeDiff)
|
|
739
|
+
);
|
|
740
|
+
});
|
|
741
|
+
}
|
|
449
742
|
}
|
|
450
743
|
}
|
|
451
744
|
|
|
@@ -501,9 +794,39 @@ define([
|
|
|
501
794
|
|
|
502
795
|
SourcesLayer.prototype._findOrAddSourceGroup = function(source) {
|
|
503
796
|
var sourceGroup = this._sourcesGroup[source.id];
|
|
797
|
+
var isNewlyCreated = false;
|
|
504
798
|
|
|
505
799
|
if (!sourceGroup) {
|
|
506
800
|
sourceGroup = this._addSourceGroup(source);
|
|
801
|
+
isNewlyCreated = true;
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
// If this source is being dragged and was just recreated (came back into view),
|
|
805
|
+
// set it up properly for dragging
|
|
806
|
+
if (isNewlyCreated && this._draggedElements && this._initialSourcePositions) {
|
|
807
|
+
var isDraggedSource = this._draggedElements.some(function(s) {
|
|
808
|
+
return s.id === source.id;
|
|
809
|
+
});
|
|
810
|
+
|
|
811
|
+
if (isDraggedSource) {
|
|
812
|
+
// Mark as dragging
|
|
813
|
+
sourceGroup.setDragging(true);
|
|
814
|
+
|
|
815
|
+
// Get Y position from line group before moving
|
|
816
|
+
var absoluteY = sourceGroup.getAbsoluteY();
|
|
817
|
+
|
|
818
|
+
// Move to drag layer
|
|
819
|
+
sourceGroup.moveTo(this._dragLayer);
|
|
820
|
+
sourceGroup.y(absoluteY);
|
|
821
|
+
|
|
822
|
+
// Create ghost preview for this source
|
|
823
|
+
var ghost = this._createDragGhost(sourceGroup);
|
|
824
|
+
|
|
825
|
+
this._dragGhosts.push({
|
|
826
|
+
ghost: ghost,
|
|
827
|
+
sourceId: source.id
|
|
828
|
+
});
|
|
829
|
+
}
|
|
507
830
|
}
|
|
508
831
|
|
|
509
832
|
return sourceGroup;
|
|
@@ -625,8 +948,12 @@ define([
|
|
|
625
948
|
this._layer.setVisible(visible);
|
|
626
949
|
};
|
|
627
950
|
|
|
951
|
+
/**
|
|
952
|
+
* Schedules a batch draw using RAF throttling for better performance.
|
|
953
|
+
* Multiple calls within the same frame are coalesced into one draw.
|
|
954
|
+
*/
|
|
628
955
|
SourcesLayer.prototype.batchDraw = function() {
|
|
629
|
-
this.
|
|
956
|
+
this._throttledBatchDraw();
|
|
630
957
|
};
|
|
631
958
|
|
|
632
959
|
SourcesLayer.prototype.listening = function(bool) {
|
|
@@ -654,21 +981,36 @@ define([
|
|
|
654
981
|
return endsLater || startsEarlier;
|
|
655
982
|
};
|
|
656
983
|
|
|
657
|
-
SourcesLayer.prototype.rescale = function(
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
else {
|
|
662
|
-
this._rescale();
|
|
663
|
-
}
|
|
984
|
+
SourcesLayer.prototype.rescale = function() {
|
|
985
|
+
// Increment the rescale version to cancel any in-progress rescale
|
|
986
|
+
this._rescaleVersion = (this._rescaleVersion || 0) + 1;
|
|
987
|
+
this._rescale(this._rescaleVersion);
|
|
664
988
|
};
|
|
665
989
|
|
|
666
|
-
SourcesLayer.prototype._rescale = function() {
|
|
667
|
-
var
|
|
990
|
+
SourcesLayer.prototype._rescale = function(version) {
|
|
991
|
+
var self = this;
|
|
992
|
+
var ids = Object.keys(this._sourcesGroup);
|
|
993
|
+
var urls = [];
|
|
994
|
+
var index = 0;
|
|
995
|
+
|
|
996
|
+
function processNext() {
|
|
997
|
+
// Check if this rescale was cancelled (a newer one started)
|
|
998
|
+
if (self._rescaleVersion !== version) {
|
|
999
|
+
return;
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
if (index >= ids.length) {
|
|
1003
|
+
// Done processing all sources
|
|
1004
|
+
self.batchDraw();
|
|
1005
|
+
return;
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
var id = ids[index];
|
|
1009
|
+
var sourceGroup = self._sourcesGroup[id];
|
|
668
1010
|
|
|
669
|
-
|
|
670
|
-
if (
|
|
671
|
-
audioPreviews =
|
|
1011
|
+
// Skip if source group was removed during async processing
|
|
1012
|
+
if (sourceGroup) {
|
|
1013
|
+
var audioPreviews = sourceGroup.getAudioPreview();
|
|
672
1014
|
|
|
673
1015
|
audioPreviews.forEach(function(audioPreview) {
|
|
674
1016
|
if (self._shouldResampleAudio(audioPreview.url, urls)) {
|
|
@@ -681,8 +1023,14 @@ define([
|
|
|
681
1023
|
}
|
|
682
1024
|
});
|
|
683
1025
|
}
|
|
1026
|
+
|
|
1027
|
+
index++;
|
|
1028
|
+
|
|
1029
|
+
// Yield to allow cancellation and UI updates
|
|
1030
|
+
Utils.scheduleIdle(processNext, { timeout: 16 });
|
|
684
1031
|
}
|
|
685
|
-
|
|
1032
|
+
|
|
1033
|
+
processNext();
|
|
686
1034
|
};
|
|
687
1035
|
|
|
688
1036
|
SourcesLayer.prototype._shouldResampleAudio = function(audioUrl, urls) {
|
|
@@ -708,6 +1056,14 @@ define([
|
|
|
708
1056
|
this._peaks.off('handler.segments.show', this._onSegmentsShow);
|
|
709
1057
|
this._peaks.off('model.source.setIndicators', this.setIndicators);
|
|
710
1058
|
this._peaks.off('handler.view.mouseup', this._stopDrag);
|
|
1059
|
+
|
|
1060
|
+
// Cancel any in-progress rescale
|
|
1061
|
+
this._rescaleVersion++;
|
|
1062
|
+
|
|
1063
|
+
// Clean up invoker resources
|
|
1064
|
+
if (this._invoker) {
|
|
1065
|
+
this._invoker.destroy();
|
|
1066
|
+
}
|
|
711
1067
|
};
|
|
712
1068
|
|
|
713
1069
|
SourcesLayer.prototype.getHeight = function() {
|
|
@@ -16,13 +16,8 @@ define([
|
|
|
16
16
|
|
|
17
17
|
var isXhr2 = ('withCredentials' in new XMLHttpRequest());
|
|
18
18
|
|
|
19
|
-
//
|
|
20
|
-
|
|
21
|
-
if (typeof window !== 'undefined' && window.requestIdleCallback) {
|
|
22
|
-
return window.requestIdleCallback(fn, { timeout: 80 });
|
|
23
|
-
}
|
|
24
|
-
return setTimeout(fn, 0);
|
|
25
|
-
}
|
|
19
|
+
// Waveform data cache for better performance on repeated requests
|
|
20
|
+
var waveformCache = Utils.createLRUCache(50);
|
|
26
21
|
|
|
27
22
|
/**
|
|
28
23
|
* Creates and returns a WaveformData object, either by requesting the
|
|
@@ -39,6 +34,14 @@ define([
|
|
|
39
34
|
this._peaks = peaks;
|
|
40
35
|
}
|
|
41
36
|
|
|
37
|
+
/**
|
|
38
|
+
* Clears the waveform data cache.
|
|
39
|
+
* Useful when memory needs to be freed.
|
|
40
|
+
*/
|
|
41
|
+
WaveformBuilder.clearCache = function() {
|
|
42
|
+
waveformCache.clear();
|
|
43
|
+
};
|
|
44
|
+
|
|
42
45
|
/**
|
|
43
46
|
* Options for requesting remote waveform data.
|
|
44
47
|
*
|
|
@@ -192,6 +195,18 @@ define([
|
|
|
192
195
|
return;
|
|
193
196
|
}
|
|
194
197
|
|
|
198
|
+
// Check cache first for better performance
|
|
199
|
+
var cacheKey = url + ':' + requestType;
|
|
200
|
+
var cachedData = waveformCache.get(cacheKey);
|
|
201
|
+
|
|
202
|
+
if (cachedData) {
|
|
203
|
+
// Return cached waveform data asynchronously to maintain consistent API
|
|
204
|
+
Utils.scheduleIdle(function() {
|
|
205
|
+
callback(null, cachedData);
|
|
206
|
+
});
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
|
|
195
210
|
var xhr = self._createXHR(url, requestType, options.withCredentials, function(event) {
|
|
196
211
|
if (this.readyState !== 4) {
|
|
197
212
|
return;
|
|
@@ -205,7 +220,7 @@ define([
|
|
|
205
220
|
return;
|
|
206
221
|
}
|
|
207
222
|
|
|
208
|
-
scheduleIdle(function() {
|
|
223
|
+
Utils.scheduleIdle(function() {
|
|
209
224
|
try {
|
|
210
225
|
var waveformData = WaveformData.create(event.target.response);
|
|
211
226
|
|
|
@@ -214,6 +229,9 @@ define([
|
|
|
214
229
|
return;
|
|
215
230
|
}
|
|
216
231
|
|
|
232
|
+
// Cache the waveform data for future use
|
|
233
|
+
waveformCache.set(cacheKey, waveformData);
|
|
234
|
+
|
|
217
235
|
callback(null, waveformData);
|
|
218
236
|
}
|
|
219
237
|
catch (err) {
|
|
@@ -270,7 +288,7 @@ define([
|
|
|
270
288
|
return;
|
|
271
289
|
}
|
|
272
290
|
|
|
273
|
-
scheduleIdle(function() {
|
|
291
|
+
Utils.scheduleIdle(function() {
|
|
274
292
|
try {
|
|
275
293
|
var createdWaveformData = WaveformData.create(data);
|
|
276
294
|
|