@checksub_team/peaks_timeline 2.2.1-alpha.0 → 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/src/utils.js CHANGED
@@ -338,6 +338,96 @@ define([
338
338
  var BB = ((B.toString(16).length === 1) ? '0' + B.toString(16) : B.toString(16));
339
339
 
340
340
  return '#' + RR + GG + BB;
341
+ },
342
+
343
+ // ==================== Performance Utilities ====================
344
+
345
+ /**
346
+ * Schedules work during browser idle time.
347
+ * Falls back to setTimeout if requestIdleCallback is not available.
348
+ *
349
+ * @param {Function} callback Function to execute
350
+ * @param {Object} options Options object with timeout property
351
+ * @returns {Number} ID that can be used to cancel
352
+ */
353
+ scheduleIdle: function(callback, options) {
354
+ options = options || { timeout: 50 };
355
+
356
+ if (typeof window !== 'undefined' && window.requestIdleCallback) {
357
+ return window.requestIdleCallback(callback, options);
358
+ }
359
+ return setTimeout(function() {
360
+ callback({
361
+ didTimeout: true,
362
+ timeRemaining: function() {
363
+ return 0;
364
+ }
365
+ });
366
+ }, 0);
367
+ },
368
+
369
+ /**
370
+ * Cancels a scheduled idle callback.
371
+ *
372
+ * @param {Number} id The ID returned by scheduleIdle
373
+ */
374
+ cancelIdle: function(id) {
375
+ if (typeof window !== 'undefined' && window.cancelIdleCallback) {
376
+ window.cancelIdleCallback(id);
377
+ }
378
+ else {
379
+ clearTimeout(id);
380
+ }
381
+ },
382
+
383
+ /**
384
+ * Creates a simple LRU (Least Recently Used) cache.
385
+ *
386
+ * @param {Number} maxSize Maximum number of items to cache
387
+ * @returns {Object} Cache with get(), set(), has(), delete(), clear() methods
388
+ */
389
+ createLRUCache: function(maxSize) {
390
+ var cache = new Map();
391
+
392
+ maxSize = maxSize || 100;
393
+
394
+ return {
395
+ get: function(key) {
396
+ if (!cache.has(key)) {
397
+ return undefined;
398
+ }
399
+ // Move to end (most recently used)
400
+ var value = cache.get(key);
401
+
402
+ cache.delete(key);
403
+ cache.set(key, value);
404
+ return value;
405
+ },
406
+ set: function(key, value) {
407
+ if (cache.has(key)) {
408
+ cache.delete(key);
409
+ }
410
+ else if (cache.size >= maxSize) {
411
+ // Delete oldest (first) entry
412
+ var firstKey = cache.keys().next().value;
413
+
414
+ cache.delete(firstKey);
415
+ }
416
+ cache.set(key, value);
417
+ },
418
+ has: function(key) {
419
+ return cache.has(key);
420
+ },
421
+ delete: function(key) {
422
+ return cache.delete(key);
423
+ },
424
+ clear: function() {
425
+ cache.clear();
426
+ },
427
+ size: function() {
428
+ return cache.size;
429
+ }
430
+ };
341
431
  }
342
432
  };
343
433
  });
package/src/view.js CHANGED
@@ -120,8 +120,6 @@ define([
120
120
  height: self._height
121
121
  });
122
122
 
123
- self._tempGroup = new Konva.Group({ listening: false });
124
-
125
123
  self._width -= self._peaks.options.lineIndicatorWidth;
126
124
 
127
125
  self._axis = new Axis(self._peaks, self, {
@@ -134,8 +132,6 @@ define([
134
132
  self._sourcesLayer = new SourcesLayer(peaks, self, true);
135
133
  self._sourcesLayer.addToStage(self._stage);
136
134
 
137
- self._sourcesLayer.add(self._tempGroup);
138
-
139
135
  self._axis.addFrontToStage(self._stage);
140
136
 
141
137
  self._playheadLayer = new PlayheadLayer(
@@ -315,7 +311,7 @@ define([
315
311
  }
316
312
 
317
313
  View.prototype._mouseUp = function() {
318
- this.clearScrollingInterval();
314
+ this.stopAutoScroll();
319
315
  this._peaks.emit('handler.view.mouseup');
320
316
  };
321
317
 
@@ -329,68 +325,113 @@ define([
329
325
  this._isClickable = clickable;
330
326
  };
331
327
 
332
- View.prototype.getTempGroup = function() {
333
- return this._tempGroup;
334
- };
335
-
336
328
  View.prototype.getSelectedElements = function() {
337
329
  return Object.values(this._modeLayer.getSelectedElements());
338
330
  };
339
331
 
340
- View.prototype.updateWithAutoScroll = function(updateInInterval,
341
- updateOutInterval) {
332
+ /**
333
+ * Updates the view with auto-scroll behavior during drag operations.
334
+ *
335
+ * Auto-scroll activates when the pointer is within `autoScrollThreshold` of
336
+ * the left/right edges. While scrolling, we call `updateWhileScrolling` on
337
+ * every animation frame after updating the timeline offset.
338
+ *
339
+ * If auto-scroll is not active, we stop any pending auto-scroll loop and call
340
+ * `updateWhileNotScrolling` (or fall back to `updateWhileScrolling`).
341
+ *
342
+ * Uses requestAnimationFrame for smooth, consistent scrolling.
343
+ *
344
+ * @param {Function} updateWhileScrolling Called after each scroll step.
345
+ * @param {Function} [updateWhileNotScrolling] Called when not scrolling.
346
+ */
347
+ View.prototype.updateWithAutoScroll = function(updateWhileScrolling,
348
+ updateWhileNotScrolling) {
342
349
  var self = this;
343
- var posX = this.getPointerPosition().x;
344
- var threshold = Math.round(this._peaks.options.autoScrollThreshold * this.getWidth());
345
350
 
346
- this._limited = 0;
351
+ var pointer = this.getPointerPosition();
352
+ var pointerX = pointer ? pointer.x : null;
353
+ var viewWidth = this.getWidth();
354
+ var thresholdPx = Math.round(this._peaks.options.autoScrollThreshold * viewWidth);
347
355
 
348
- if (posX < threshold) {
349
- this._limited = Math.round(-30 * Math.min(1, (threshold - posX) / threshold));
350
- }
351
- else if (posX > this.getWidth() - threshold) {
352
- this._limited = Math.round(
353
- 30 * Math.min(1, (posX - (this.getWidth() - threshold)) / threshold)
354
- );
355
- }
356
+ var MAX_AUTO_SCROLL_PX_PER_FRAME = 30;
357
+ var NOMINAL_FRAME_MS = 16.67; // ~60fps
356
358
 
357
- if (this._limited && self.getFrameOffset() > 0 || this._limited > 0) {
358
- if (!this._scrollingInterval) {
359
- this._scrollingInterval = setInterval(
360
- function() {
361
- var newOffset = self.getFrameOffset() + self._limited;
362
-
363
- if (newOffset < 0) {
364
- self.updateTimeline(0);
365
- clearInterval(self._scrollingInterval);
366
- self._scrollingInterval = null;
367
- }
368
- else {
369
- self.updateTimeline(self.getFrameOffset() + self._limited);
370
- }
371
-
372
- updateInInterval();
373
- },
374
- 10
359
+ function getAutoScrollVelocity(pointerXValue) {
360
+ if (typeof pointerXValue !== 'number' || thresholdPx <= 0) {
361
+ return 0;
362
+ }
363
+
364
+ if (pointerXValue < thresholdPx) {
365
+ return Math.round(
366
+ -MAX_AUTO_SCROLL_PX_PER_FRAME * Math.min(1, (thresholdPx - pointerXValue) / thresholdPx)
375
367
  );
376
368
  }
369
+ if (pointerXValue > viewWidth - thresholdPx) {
370
+ return Math.round(
371
+ MAX_AUTO_SCROLL_PX_PER_FRAME
372
+ * Math.min(1, (pointerXValue - (viewWidth - thresholdPx)) / thresholdPx)
373
+ );
374
+ }
375
+ return 0;
376
+ }
377
+
378
+ var velocityPxPerFrame = getAutoScrollVelocity(pointerX);
379
+
380
+ // For left scroll (negative), only scroll if we can actually move left.
381
+ // For right scroll (positive), allow scrolling (timeline can extend).
382
+ var canScroll = velocityPxPerFrame > 0 || (velocityPxPerFrame < 0 && self.getFrameOffset() > 0);
383
+
384
+ // Keep the current velocity on the instance for debugging/inspection.
385
+ this._limited = velocityPxPerFrame;
386
+
387
+ if (velocityPxPerFrame !== 0 && canScroll) {
388
+ if (!this._scrollingRafId) {
389
+ var lastTime = performance.now();
390
+
391
+ function scrollFrame(currentTime) {
392
+ if (!self._scrollingRafId) {
393
+ return;
394
+ }
395
+
396
+ // Calculate time delta for consistent scroll speed regardless of frame rate
397
+ var deltaTime = currentTime - lastTime;
398
+ var scrollAmount = Math.round(self._limited * deltaTime / NOMINAL_FRAME_MS);
399
+
400
+ lastTime = currentTime;
401
+
402
+ var newOffset = self.getFrameOffset() + scrollAmount;
403
+
404
+ if (newOffset < 0) {
405
+ self.updateTimeline(0);
406
+ self._scrollingRafId = null;
407
+ }
408
+ else {
409
+ self.updateTimeline(newOffset);
410
+ updateWhileScrolling();
411
+ self._scrollingRafId = requestAnimationFrame(scrollFrame);
412
+ }
413
+ }
414
+
415
+ self._scrollingRafId = requestAnimationFrame(scrollFrame);
416
+ }
377
417
  }
378
418
  else {
379
- this.clearScrollingInterval();
419
+ this.stopAutoScroll();
380
420
 
381
- if (updateOutInterval) {
382
- updateOutInterval();
421
+ if (updateWhileNotScrolling) {
422
+ updateWhileNotScrolling();
383
423
  }
384
424
  else {
385
- updateInInterval();
425
+ updateWhileScrolling();
386
426
  }
387
427
  }
388
428
  };
389
429
 
390
- View.prototype.clearScrollingInterval = function() {
391
- if (this._scrollingInterval) {
392
- clearInterval(this._scrollingInterval);
393
- this._scrollingInterval = null;
430
+ // Clear/stop any active auto-scroll loop.
431
+ View.prototype.stopAutoScroll = function() {
432
+ if (this._scrollingRafId) {
433
+ cancelAnimationFrame(this._scrollingRafId);
434
+ this._scrollingRafId = null;
394
435
  }
395
436
  };
396
437
 
@@ -663,7 +704,7 @@ define([
663
704
 
664
705
  this.updateTimeline(this._frameOffset);
665
706
 
666
- this._sourcesLayer.rescale(true);
707
+ this._sourcesLayer.rescale();
667
708
 
668
709
  // Update the playhead position after zooming.
669
710
  this._playheadLayer.updatePlayheadTime(currentTime);