wavesurfer.js 7.11.0 → 7.12.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.
Files changed (91) hide show
  1. package/README.md +1 -1
  2. package/dist/__tests__/memory-leaks.test.d.ts +7 -0
  3. package/dist/__tests__/memory-leaks.test.js +184 -0
  4. package/dist/__tests__/player.test.js +107 -0
  5. package/dist/__tests__/renderer-utils.test.d.ts +1 -0
  6. package/dist/__tests__/renderer-utils.test.js +373 -0
  7. package/dist/decoder.js +12 -4
  8. package/dist/draggable.d.ts +4 -0
  9. package/dist/draggable.js +20 -5
  10. package/dist/fetcher.js +14 -23
  11. package/dist/player.d.ts +21 -0
  12. package/dist/player.js +86 -0
  13. package/dist/plugins/envelope.cjs +1 -1
  14. package/dist/plugins/envelope.esm.js +1 -1
  15. package/dist/plugins/envelope.js +1 -1
  16. package/dist/plugins/envelope.min.js +1 -1
  17. package/dist/plugins/hover.cjs +1 -1
  18. package/dist/plugins/hover.d.ts +0 -3
  19. package/dist/plugins/hover.esm.js +1 -1
  20. package/dist/plugins/hover.js +1 -1
  21. package/dist/plugins/hover.min.js +1 -1
  22. package/dist/plugins/minimap.cjs +1 -1
  23. package/dist/plugins/minimap.esm.js +1 -1
  24. package/dist/plugins/minimap.js +1 -1
  25. package/dist/plugins/minimap.min.js +1 -1
  26. package/dist/plugins/record.cjs +1 -1
  27. package/dist/plugins/record.esm.js +1 -1
  28. package/dist/plugins/record.js +1 -1
  29. package/dist/plugins/record.min.js +1 -1
  30. package/dist/plugins/regions.cjs +1 -1
  31. package/dist/plugins/regions.esm.js +1 -1
  32. package/dist/plugins/regions.js +1 -1
  33. package/dist/plugins/regions.min.js +1 -1
  34. package/dist/plugins/timeline.cjs +1 -1
  35. package/dist/plugins/timeline.d.ts +1 -1
  36. package/dist/plugins/timeline.esm.js +1 -1
  37. package/dist/plugins/timeline.js +1 -1
  38. package/dist/plugins/timeline.min.js +1 -1
  39. package/dist/plugins/zoom.cjs +1 -1
  40. package/dist/plugins/zoom.d.ts +10 -1
  41. package/dist/plugins/zoom.esm.js +1 -1
  42. package/dist/plugins/zoom.js +1 -1
  43. package/dist/plugins/zoom.min.js +1 -1
  44. package/dist/reactive/__tests__/drag-stream.test.d.ts +1 -0
  45. package/dist/reactive/__tests__/drag-stream.test.js +199 -0
  46. package/dist/reactive/__tests__/event-stream-emitter.test.d.ts +1 -0
  47. package/dist/reactive/__tests__/event-stream-emitter.test.js +230 -0
  48. package/dist/reactive/__tests__/event-streams.test.d.ts +1 -0
  49. package/dist/reactive/__tests__/event-streams.test.js +278 -0
  50. package/dist/reactive/__tests__/media-event-bridge.test.d.ts +1 -0
  51. package/dist/reactive/__tests__/media-event-bridge.test.js +202 -0
  52. package/dist/reactive/__tests__/render-scheduler.test.d.ts +1 -0
  53. package/dist/reactive/__tests__/render-scheduler.test.js +216 -0
  54. package/dist/reactive/__tests__/scroll-stream.test.d.ts +1 -0
  55. package/dist/reactive/__tests__/scroll-stream.test.js +191 -0
  56. package/dist/reactive/__tests__/state-event-emitter.test.d.ts +1 -0
  57. package/dist/reactive/__tests__/state-event-emitter.test.js +234 -0
  58. package/dist/reactive/__tests__/store.test.d.ts +1 -0
  59. package/dist/reactive/__tests__/store.test.js +271 -0
  60. package/dist/reactive/drag-stream.d.ts +48 -0
  61. package/dist/reactive/drag-stream.js +146 -0
  62. package/dist/reactive/event-stream-emitter.d.ts +104 -0
  63. package/dist/reactive/event-stream-emitter.js +151 -0
  64. package/dist/reactive/event-streams.d.ts +65 -0
  65. package/dist/reactive/event-streams.js +146 -0
  66. package/dist/reactive/media-event-bridge.d.ts +51 -0
  67. package/dist/reactive/media-event-bridge.js +152 -0
  68. package/dist/reactive/render-scheduler.d.ts +43 -0
  69. package/dist/reactive/render-scheduler.js +75 -0
  70. package/dist/reactive/scroll-stream.d.ts +93 -0
  71. package/dist/reactive/scroll-stream.js +131 -0
  72. package/dist/reactive/state-event-emitter.d.ts +105 -0
  73. package/dist/reactive/state-event-emitter.js +213 -0
  74. package/dist/reactive/store.d.ts +66 -0
  75. package/dist/reactive/store.js +115 -0
  76. package/dist/renderer-utils.d.ts +126 -0
  77. package/dist/renderer-utils.js +252 -0
  78. package/dist/renderer.d.ts +4 -4
  79. package/dist/renderer.js +133 -197
  80. package/dist/state/__tests__/wavesurfer-state.test.d.ts +1 -0
  81. package/dist/state/__tests__/wavesurfer-state.test.js +268 -0
  82. package/dist/state/wavesurfer-state.d.ts +90 -0
  83. package/dist/state/wavesurfer-state.js +124 -0
  84. package/dist/types.d.ts +167 -1
  85. package/dist/wavesurfer.cjs +1 -1
  86. package/dist/wavesurfer.d.ts +18 -0
  87. package/dist/wavesurfer.esm.js +1 -1
  88. package/dist/wavesurfer.js +46 -8
  89. package/dist/wavesurfer.min.js +1 -1
  90. package/dist/webaudio.js +4 -0
  91. package/package.json +6 -6
package/dist/renderer.js CHANGED
@@ -18,8 +18,11 @@ var __rest = (this && this.__rest) || function (s, e) {
18
18
  }
19
19
  return t;
20
20
  };
21
- import { makeDraggable } from './draggable.js';
22
21
  import EventEmitter from './event-emitter.js';
22
+ import * as utils from './renderer-utils.js';
23
+ import { createDragStream } from './reactive/drag-stream.js';
24
+ import { createScrollStream } from './reactive/scroll-stream.js';
25
+ import { effect } from './reactive/store.js';
23
26
  class Renderer extends EventEmitter {
24
27
  constructor(options, audioElement) {
25
28
  super();
@@ -31,7 +34,8 @@ class Renderer extends EventEmitter {
31
34
  this.isDragging = false;
32
35
  this.subscriptions = [];
33
36
  this.unsubscribeOnScroll = [];
34
- this.dragUnsubscribe = null;
37
+ this.dragStream = null;
38
+ this.scrollStream = null;
35
39
  this.subscriptions = [];
36
40
  this.options = options;
37
41
  const parent = this.parentFromOptionsContainer(options.container);
@@ -63,35 +67,30 @@ class Renderer extends EventEmitter {
63
67
  return parent;
64
68
  }
65
69
  initEvents() {
66
- const getClickPosition = (e) => {
67
- const rect = this.wrapper.getBoundingClientRect();
68
- const x = e.clientX - rect.left;
69
- const y = e.clientY - rect.top;
70
- const relativeX = x / rect.width;
71
- const relativeY = y / rect.height;
72
- return [relativeX, relativeY];
73
- };
74
70
  // Add a click listener
75
71
  this.wrapper.addEventListener('click', (e) => {
76
- const [x, y] = getClickPosition(e);
72
+ const rect = this.wrapper.getBoundingClientRect();
73
+ const [x, y] = utils.getRelativePointerPosition(rect, e.clientX, e.clientY);
77
74
  this.emit('click', x, y);
78
75
  });
79
76
  // Add a double click listener
80
77
  this.wrapper.addEventListener('dblclick', (e) => {
81
- const [x, y] = getClickPosition(e);
78
+ const rect = this.wrapper.getBoundingClientRect();
79
+ const [x, y] = utils.getRelativePointerPosition(rect, e.clientX, e.clientY);
82
80
  this.emit('dblclick', x, y);
83
81
  });
84
82
  // Drag
85
83
  if (this.options.dragToSeek === true || typeof this.options.dragToSeek === 'object') {
86
84
  this.initDrag();
87
85
  }
88
- // Add a scroll listener
89
- this.scrollContainer.addEventListener('scroll', () => {
90
- const { scrollLeft, scrollWidth, clientWidth } = this.scrollContainer;
91
- const startX = scrollLeft / scrollWidth;
92
- const endX = (scrollLeft + clientWidth) / scrollWidth;
93
- this.emit('scroll', startX, endX, scrollLeft, scrollLeft + clientWidth);
94
- });
86
+ // Add a scroll listener using reactive stream
87
+ this.scrollStream = createScrollStream(this.scrollContainer);
88
+ const unsubscribeScroll = effect(() => {
89
+ const { startX, endX } = this.scrollStream.percentages.value;
90
+ const { left, right } = this.scrollStream.bounds.value;
91
+ this.emit('scroll', startX, endX, left, right);
92
+ }, [this.scrollStream.percentages, this.scrollStream.bounds]);
93
+ this.subscriptions.push(unsubscribeScroll);
95
94
  // Re-render the waveform on container resize
96
95
  if (typeof ResizeObserver === 'function') {
97
96
  const delay = this.createDelay(100);
@@ -109,43 +108,32 @@ class Renderer extends EventEmitter {
109
108
  return;
110
109
  this.lastContainerWidth = width;
111
110
  this.reRender();
111
+ this.emit('resize');
112
112
  }
113
113
  initDrag() {
114
114
  // Don't initialize drag if it's already set up
115
- if (this.dragUnsubscribe)
115
+ if (this.dragStream)
116
116
  return;
117
- this.dragUnsubscribe = makeDraggable(this.wrapper,
118
- // On drag
119
- (_, __, x) => {
120
- this.emit('drag', Math.max(0, Math.min(1, x / this.wrapper.getBoundingClientRect().width)));
121
- },
122
- // On start drag
123
- (x) => {
124
- this.isDragging = true;
125
- this.emit('dragstart', Math.max(0, Math.min(1, x / this.wrapper.getBoundingClientRect().width)));
126
- },
127
- // On end drag
128
- (x) => {
129
- this.isDragging = false;
130
- this.emit('dragend', Math.max(0, Math.min(1, x / this.wrapper.getBoundingClientRect().width)));
131
- });
132
- this.subscriptions.push(this.dragUnsubscribe);
133
- }
134
- getHeight(optionsHeight, optionsSplitChannel) {
135
- var _a;
136
- const defaultHeight = 128;
137
- const numberOfChannels = ((_a = this.audioData) === null || _a === void 0 ? void 0 : _a.numberOfChannels) || 1;
138
- if (optionsHeight == null)
139
- return defaultHeight;
140
- if (!isNaN(Number(optionsHeight)))
141
- return Number(optionsHeight);
142
- if (optionsHeight === 'auto') {
143
- const height = this.parent.clientHeight || defaultHeight;
144
- if (optionsSplitChannel === null || optionsSplitChannel === void 0 ? void 0 : optionsSplitChannel.every((channel) => !channel.overlay))
145
- return height / numberOfChannels;
146
- return height;
147
- }
148
- return defaultHeight;
117
+ this.dragStream = createDragStream(this.wrapper);
118
+ const unsubscribeDrag = effect(() => {
119
+ const drag = this.dragStream.signal.value;
120
+ if (!drag)
121
+ return;
122
+ const width = this.wrapper.getBoundingClientRect().width;
123
+ const relX = utils.clampToUnit(drag.x / width);
124
+ if (drag.type === 'start') {
125
+ this.isDragging = true;
126
+ this.emit('dragstart', relX);
127
+ }
128
+ else if (drag.type === 'move') {
129
+ this.emit('drag', relX);
130
+ }
131
+ else if (drag.type === 'end') {
132
+ this.isDragging = false;
133
+ this.emit('dragend', relX);
134
+ }
135
+ }, [this.dragStream.signal]);
136
+ this.subscriptions.push(unsubscribeDrag);
149
137
  }
150
138
  initHtml() {
151
139
  const div = document.createElement('div');
@@ -182,6 +170,7 @@ class Renderer extends EventEmitter {
182
170
  }
183
171
  :host .canvases {
184
172
  min-height: ${this.getHeight(this.options.height, this.options.splitChannels)}px;
173
+ pointer-events: none;
185
174
  }
186
175
  :host .canvases > div {
187
176
  position: relative;
@@ -267,6 +256,14 @@ class Renderer extends EventEmitter {
267
256
  }
268
257
  (_a = this.unsubscribeOnScroll) === null || _a === void 0 ? void 0 : _a.forEach((unsubscribe) => unsubscribe());
269
258
  this.unsubscribeOnScroll = [];
259
+ if (this.dragStream) {
260
+ this.dragStream.cleanup();
261
+ this.dragStream = null;
262
+ }
263
+ if (this.scrollStream) {
264
+ this.scrollStream.cleanup();
265
+ this.scrollStream = null;
266
+ }
270
267
  }
271
268
  createDelay(delayMs = 10) {
272
269
  let timeout;
@@ -297,123 +294,88 @@ class Renderer extends EventEmitter {
297
294
  });
298
295
  };
299
296
  }
300
- // Convert array of color values to linear gradient
301
- convertColorValues(color) {
302
- if (!Array.isArray(color))
303
- return color || '';
304
- if (color.length === 0)
305
- return '#999'; // Return default color for empty array
306
- if (color.length < 2)
307
- return color[0] || '';
308
- const canvasElement = document.createElement('canvas');
309
- const ctx = canvasElement.getContext('2d');
310
- const gradientHeight = canvasElement.height * (window.devicePixelRatio || 1);
311
- const gradient = ctx.createLinearGradient(0, 0, 0, gradientHeight);
312
- const colorStopPercentage = 1 / (color.length - 1);
313
- color.forEach((color, index) => {
314
- const offset = index * colorStopPercentage;
315
- gradient.addColorStop(offset, color);
297
+ getHeight(optionsHeight, optionsSplitChannel) {
298
+ var _a;
299
+ const numberOfChannels = ((_a = this.audioData) === null || _a === void 0 ? void 0 : _a.numberOfChannels) || 1;
300
+ return utils.resolveChannelHeight({
301
+ optionsHeight,
302
+ optionsSplitChannels: optionsSplitChannel,
303
+ parentHeight: this.parent.clientHeight,
304
+ numberOfChannels,
305
+ defaultHeight: utils.DEFAULT_HEIGHT,
316
306
  });
317
- return gradient;
307
+ }
308
+ convertColorValues(color) {
309
+ return utils.resolveColorValue(color, this.getPixelRatio());
318
310
  }
319
311
  getPixelRatio() {
320
- return Math.max(1, window.devicePixelRatio || 1);
312
+ return utils.getPixelRatio(window.devicePixelRatio);
321
313
  }
322
314
  renderBarWaveform(channelData, options, ctx, vScale) {
323
- const topChannel = channelData[0];
324
- const bottomChannel = channelData[1] || channelData[0];
325
- const length = topChannel.length;
326
315
  const { width, height } = ctx.canvas;
327
- const halfHeight = height / 2;
328
- const pixelRatio = this.getPixelRatio();
329
- const barWidth = options.barWidth ? options.barWidth * pixelRatio : 1;
330
- const barGap = options.barGap ? options.barGap * pixelRatio : options.barWidth ? barWidth / 2 : 0;
331
- const barRadius = options.barRadius || 0;
332
- const barIndexScale = width / (barWidth + barGap) / length;
333
- const rectFn = barRadius && 'roundRect' in ctx ? 'roundRect' : 'rect';
316
+ const { halfHeight, barWidth, barRadius, barIndexScale, barSpacing, barMinHeight } = utils.calculateBarRenderConfig({
317
+ width,
318
+ height,
319
+ length: (channelData[0] || []).length,
320
+ options,
321
+ pixelRatio: this.getPixelRatio(),
322
+ });
323
+ const segments = utils.calculateBarSegments({
324
+ channelData,
325
+ barIndexScale,
326
+ barSpacing,
327
+ barWidth,
328
+ halfHeight,
329
+ vScale,
330
+ canvasHeight: height,
331
+ barAlign: options.barAlign,
332
+ barMinHeight,
333
+ });
334
334
  ctx.beginPath();
335
- let prevX = 0;
336
- let maxTop = 0;
337
- let maxBottom = 0;
338
- for (let i = 0; i <= length; i++) {
339
- const x = Math.round(i * barIndexScale);
340
- if (x > prevX) {
341
- const topBarHeight = Math.round(maxTop * halfHeight * vScale);
342
- const bottomBarHeight = Math.round(maxBottom * halfHeight * vScale);
343
- const barHeight = topBarHeight + bottomBarHeight || 1;
344
- // Vertical alignment
345
- let y = halfHeight - topBarHeight;
346
- if (options.barAlign === 'top') {
347
- y = 0;
348
- }
349
- else if (options.barAlign === 'bottom') {
350
- y = height - barHeight;
351
- }
352
- ctx[rectFn](prevX * (barWidth + barGap), y, barWidth, barHeight, barRadius);
353
- prevX = x;
354
- maxTop = 0;
355
- maxBottom = 0;
335
+ for (const segment of segments) {
336
+ if (barRadius && 'roundRect' in ctx) {
337
+ ;
338
+ ctx.roundRect(segment.x, segment.y, segment.width, segment.height, barRadius);
339
+ }
340
+ else {
341
+ ctx.rect(segment.x, segment.y, segment.width, segment.height);
356
342
  }
357
- const magnitudeTop = Math.abs(topChannel[i] || 0);
358
- const magnitudeBottom = Math.abs(bottomChannel[i] || 0);
359
- if (magnitudeTop > maxTop)
360
- maxTop = magnitudeTop;
361
- if (magnitudeBottom > maxBottom)
362
- maxBottom = magnitudeBottom;
363
343
  }
364
344
  ctx.fill();
365
345
  ctx.closePath();
366
346
  }
367
347
  renderLineWaveform(channelData, _options, ctx, vScale) {
368
- const drawChannel = (index) => {
369
- const channel = channelData[index] || channelData[0];
370
- const length = channel.length;
371
- const { height } = ctx.canvas;
372
- const halfHeight = height / 2;
373
- const hScale = ctx.canvas.width / length;
374
- ctx.moveTo(0, halfHeight);
375
- let prevX = 0;
376
- let max = 0;
377
- for (let i = 0; i <= length; i++) {
378
- const x = Math.round(i * hScale);
379
- if (x > prevX) {
380
- const h = Math.round(max * halfHeight * vScale) || 1;
381
- const y = halfHeight + h * (index === 0 ? -1 : 1);
382
- ctx.lineTo(prevX, y);
383
- prevX = x;
384
- max = 0;
385
- }
386
- const value = Math.abs(channel[i] || 0);
387
- if (value > max)
388
- max = value;
389
- }
390
- ctx.lineTo(prevX, halfHeight);
391
- };
348
+ const { width, height } = ctx.canvas;
349
+ const paths = utils.calculateLinePaths({ channelData, width, height, vScale });
392
350
  ctx.beginPath();
393
- drawChannel(0);
394
- drawChannel(1);
351
+ for (const path of paths) {
352
+ if (!path.length)
353
+ continue;
354
+ ctx.moveTo(path[0].x, path[0].y);
355
+ for (let i = 1; i < path.length; i++) {
356
+ const point = path[i];
357
+ ctx.lineTo(point.x, point.y);
358
+ }
359
+ }
395
360
  ctx.fill();
396
361
  ctx.closePath();
397
362
  }
398
363
  renderWaveform(channelData, options, ctx) {
399
364
  ctx.fillStyle = this.convertColorValues(options.waveColor);
400
- // Custom rendering function
401
365
  if (options.renderFunction) {
402
366
  options.renderFunction(channelData, ctx);
403
367
  return;
404
368
  }
405
- // Vertical scaling
406
- let vScale = options.barHeight || 1;
407
- if (options.normalize) {
408
- const max = Array.from(channelData[0]).reduce((max, value) => Math.max(max, Math.abs(value)), 0);
409
- vScale = max ? vScale / max : vScale;
410
- }
411
- // Render waveform as bars
412
- if (options.barWidth || options.barGap || options.barAlign) {
369
+ const vScale = utils.calculateVerticalScale({
370
+ channelData,
371
+ barHeight: options.barHeight,
372
+ normalize: options.normalize,
373
+ maxPeak: options.maxPeak,
374
+ });
375
+ if (utils.shouldRenderBars(options)) {
413
376
  this.renderBarWaveform(channelData, options, ctx, vScale);
414
377
  return;
415
378
  }
416
- // Render waveform as a polyline
417
379
  this.renderLineWaveform(channelData, options, ctx, vScale);
418
380
  }
419
381
  renderSingleCanvas(data, options, width, height, offset, canvasContainer, progressContainer) {
@@ -426,7 +388,13 @@ class Renderer extends EventEmitter {
426
388
  canvas.style.left = `${Math.round(offset)}px`;
427
389
  canvasContainer.appendChild(canvas);
428
390
  const ctx = canvas.getContext('2d');
429
- this.renderWaveform(data, options, ctx);
391
+ if (options.renderFunction) {
392
+ ctx.fillStyle = this.convertColorValues(options.waveColor);
393
+ options.renderFunction(data, ctx);
394
+ }
395
+ else {
396
+ this.renderWaveform(data, options, ctx);
397
+ }
430
398
  // Draw a progress canvas
431
399
  if (canvas.width > 0 && canvas.height > 0) {
432
400
  const progressCanvas = canvas.cloneNode();
@@ -444,17 +412,8 @@ class Renderer extends EventEmitter {
444
412
  const pixelRatio = this.getPixelRatio();
445
413
  const { clientWidth } = this.scrollContainer;
446
414
  const totalWidth = width / pixelRatio;
447
- let singleCanvasWidth = Math.min(Renderer.MAX_CANVAS_WIDTH, clientWidth, totalWidth);
415
+ const singleCanvasWidth = utils.calculateSingleCanvasWidth({ clientWidth, totalWidth, options });
448
416
  let drawnIndexes = {};
449
- // Adjust width to avoid gaps between canvases when using bars
450
- if (options.barWidth || options.barGap) {
451
- const barWidth = options.barWidth || 0.5;
452
- const barGap = options.barGap || barWidth / 2;
453
- const totalBarWidth = barWidth + barGap;
454
- if (singleCanvasWidth % totalBarWidth !== 0) {
455
- singleCanvasWidth = Math.floor(singleCanvasWidth / totalBarWidth) * totalBarWidth;
456
- }
457
- }
458
417
  // Nothing to render
459
418
  if (singleCanvasWidth === 0)
460
419
  return;
@@ -468,24 +427,15 @@ class Renderer extends EventEmitter {
468
427
  const offset = index * singleCanvasWidth;
469
428
  let clampedWidth = Math.min(totalWidth - offset, singleCanvasWidth);
470
429
  // Clamp the width to the bar grid to avoid empty canvases at the end
471
- if (options.barWidth || options.barGap) {
472
- const barWidth = options.barWidth || 0.5;
473
- const barGap = options.barGap || barWidth / 2;
474
- const totalBarWidth = barWidth + barGap;
475
- clampedWidth = Math.floor(clampedWidth / totalBarWidth) * totalBarWidth;
476
- }
430
+ clampedWidth = utils.clampWidthToBarGrid(clampedWidth, options);
477
431
  if (clampedWidth <= 0)
478
432
  return;
479
- const data = channelData.map((channel) => {
480
- const start = Math.floor((offset / totalWidth) * channel.length);
481
- const end = Math.floor(((offset + clampedWidth) / totalWidth) * channel.length);
482
- return channel.slice(start, end);
483
- });
433
+ const data = utils.sliceChannelData({ channelData, offset, clampedWidth, totalWidth });
484
434
  this.renderSingleCanvas(data, options, clampedWidth, height, offset, canvasContainer, progressContainer);
485
435
  };
486
436
  // Clear canvases to avoid too many DOM nodes
487
437
  const clearCanvases = () => {
488
- if (Object.keys(drawnIndexes).length > Renderer.MAX_NODES) {
438
+ if (utils.shouldClearCanvases(Object.keys(drawnIndexes).length)) {
489
439
  canvasContainer.innerHTML = '';
490
440
  progressContainer.innerHTML = '';
491
441
  drawnIndexes = {};
@@ -501,21 +451,18 @@ class Renderer extends EventEmitter {
501
451
  return;
502
452
  }
503
453
  // Lazy rendering
504
- const viewPosition = this.scrollContainer.scrollLeft / totalWidth;
505
- const startCanvas = Math.floor(viewPosition * numCanvases);
506
- // Draw the canvases in the viewport first
507
- draw(startCanvas - 1);
508
- draw(startCanvas);
509
- draw(startCanvas + 1);
454
+ const initialRange = utils.getLazyRenderRange({
455
+ scrollLeft: this.scrollContainer.scrollLeft,
456
+ totalWidth,
457
+ numCanvases,
458
+ });
459
+ initialRange.forEach((index) => draw(index));
510
460
  // Subscribe to the scroll event to draw additional canvases
511
461
  if (numCanvases > 1) {
512
462
  const unsubscribe = this.on('scroll', () => {
513
463
  const { scrollLeft } = this.scrollContainer;
514
- const canvasIndex = Math.floor((scrollLeft / totalWidth) * numCanvases);
515
464
  clearCanvases();
516
- draw(canvasIndex - 1);
517
- draw(canvasIndex);
518
- draw(canvasIndex + 1);
465
+ utils.getLazyRenderRange({ scrollLeft, totalWidth, numCanvases }).forEach((index) => draw(index));
519
466
  });
520
467
  this.unsubscribeOnScroll.push(unsubscribe);
521
468
  }
@@ -554,12 +501,15 @@ class Renderer extends EventEmitter {
554
501
  // Determine the width of the waveform
555
502
  const pixelRatio = this.getPixelRatio();
556
503
  const parentWidth = this.scrollContainer.clientWidth;
557
- const scrollWidth = Math.ceil(audioData.duration * (this.options.minPxPerSec || 0));
504
+ const { scrollWidth, isScrollable, useParentWidth, width } = utils.calculateWaveformLayout({
505
+ duration: audioData.duration,
506
+ minPxPerSec: this.options.minPxPerSec || 0,
507
+ parentWidth,
508
+ fillParent: this.options.fillParent,
509
+ pixelRatio,
510
+ });
558
511
  // Whether the container should scroll
559
- this.isScrollable = scrollWidth > parentWidth;
560
- const useParentWidth = this.options.fillParent && !this.isScrollable;
561
- // Width of the waveform in pixels
562
- const width = (useParentWidth ? parentWidth : scrollWidth) * pixelRatio;
512
+ this.isScrollable = isScrollable;
563
513
  // Set the width of the wrapper
564
514
  this.wrapper.style.width = useParentWidth ? '100%' : `${scrollWidth}px`;
565
515
  // Set additional styles
@@ -602,12 +552,7 @@ class Renderer extends EventEmitter {
602
552
  // Adjust the scroll position so that the cursor stays in the same place
603
553
  if (this.isScrollable && scrollWidth !== this.scrollContainer.scrollWidth) {
604
554
  const { right: after } = this.progressWrapper.getBoundingClientRect();
605
- let delta = after - before;
606
- // to limit compounding floating-point drift
607
- // we need to round to the half px furthest from 0
608
- delta *= 2;
609
- delta = delta < 0 ? Math.floor(delta) : Math.ceil(delta);
610
- delta /= 2;
555
+ const delta = utils.roundToHalfAwayFromZero(after - before);
611
556
  this.scrollContainer.scrollLeft += delta;
612
557
  }
613
558
  }
@@ -638,16 +583,9 @@ class Renderer extends EventEmitter {
638
583
  // Keep the cursor centered when playing
639
584
  const center = progressWidth - scrollLeft - middle;
640
585
  if (isPlaying && this.options.autoCenter && center > 0) {
641
- this.scrollContainer.scrollLeft += Math.min(center, 10);
586
+ this.scrollContainer.scrollLeft += center;
642
587
  }
643
588
  }
644
- // Emit the scroll event
645
- {
646
- const newScroll = this.scrollContainer.scrollLeft;
647
- const startX = newScroll / scrollWidth;
648
- const endX = (newScroll + clientWidth) / scrollWidth;
649
- this.emit('scroll', startX, endX, newScroll, newScroll + clientWidth);
650
- }
651
589
  }
652
590
  renderProgress(progress, isPlaying) {
653
591
  if (isNaN(progress))
@@ -690,6 +628,4 @@ class Renderer extends EventEmitter {
690
628
  });
691
629
  }
692
630
  }
693
- Renderer.MAX_CANVAS_WIDTH = 8000;
694
- Renderer.MAX_NODES = 10;
695
631
  export default Renderer;
@@ -0,0 +1 @@
1
+ export {};