wavesurfer.js 7.0.0-alpha.9 → 7.0.0-beta.10

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 (55) hide show
  1. package/LICENSE +29 -0
  2. package/README.md +137 -20
  3. package/dist/base-plugin.d.ts +6 -4
  4. package/dist/base-plugin.js +9 -3
  5. package/dist/decoder.d.ts +8 -7
  6. package/dist/decoder.js +43 -38
  7. package/dist/draggable.d.ts +1 -0
  8. package/dist/draggable.js +59 -0
  9. package/dist/event-emitter.d.ts +16 -10
  10. package/dist/event-emitter.js +38 -16
  11. package/dist/fetcher.d.ts +6 -3
  12. package/dist/fetcher.js +9 -15
  13. package/dist/player.d.ts +31 -11
  14. package/dist/player.js +58 -31
  15. package/dist/plugins/envelope.d.ts +71 -0
  16. package/dist/plugins/envelope.js +326 -0
  17. package/dist/plugins/envelope.min.cjs +1 -0
  18. package/dist/plugins/minimap.d.ts +39 -0
  19. package/dist/plugins/minimap.js +114 -0
  20. package/dist/plugins/minimap.min.cjs +1 -0
  21. package/dist/plugins/record.d.ts +28 -0
  22. package/dist/plugins/record.js +132 -0
  23. package/dist/plugins/record.min.cjs +1 -0
  24. package/dist/plugins/regions.d.ts +84 -43
  25. package/dist/plugins/regions.js +330 -195
  26. package/dist/plugins/regions.min.cjs +1 -0
  27. package/dist/plugins/spectrogram-fft.d.ts +9 -0
  28. package/dist/plugins/spectrogram-fft.js +150 -0
  29. package/dist/plugins/spectrogram.d.ts +72 -0
  30. package/dist/plugins/spectrogram.js +341 -0
  31. package/dist/plugins/spectrogram.min.cjs +1 -0
  32. package/dist/plugins/timeline.d.ts +25 -9
  33. package/dist/plugins/timeline.js +65 -24
  34. package/dist/plugins/timeline.min.cjs +1 -0
  35. package/dist/renderer.d.ts +32 -26
  36. package/dist/renderer.js +363 -167
  37. package/dist/timer.d.ts +1 -1
  38. package/dist/wavesurfer.d.ts +154 -0
  39. package/dist/wavesurfer.js +230 -0
  40. package/dist/wavesurfer.min.cjs +1 -0
  41. package/package.json +61 -28
  42. package/dist/index.d.ts +0 -117
  43. package/dist/index.js +0 -227
  44. package/dist/player-webaudio.d.ts +0 -8
  45. package/dist/player-webaudio.js +0 -32
  46. package/dist/plugins/multitrack.d.ts +0 -44
  47. package/dist/plugins/multitrack.js +0 -260
  48. package/dist/plugins/xmultitrack.d.ts +0 -44
  49. package/dist/plugins/xmultitrack.js +0 -260
  50. package/dist/react/useWavesurfer.d.ts +0 -5
  51. package/dist/react/useWavesurfer.js +0 -20
  52. package/dist/wavesurfer.Multitrack.min.js +0 -1
  53. package/dist/wavesurfer.Regions.min.js +0 -1
  54. package/dist/wavesurfer.Timeline.min.js +0 -1
  55. package/dist/wavesurfer.min.js +0 -1
package/dist/renderer.js CHANGED
@@ -1,28 +1,81 @@
1
- var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2
- function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3
- return new (P || (P = Promise))(function (resolve, reject) {
4
- function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5
- function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6
- function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7
- step((generator = generator.apply(thisArg, _arguments || [])).next());
8
- });
9
- };
1
+ import { makeDraggable } from './draggable.js';
10
2
  import EventEmitter from './event-emitter.js';
11
3
  class Renderer extends EventEmitter {
12
4
  constructor(options) {
13
5
  super();
14
- this.timeout = null;
6
+ this.timeouts = [];
7
+ this.isScrolling = false;
8
+ this.audioData = null;
9
+ this.resizeObserver = null;
10
+ this.isDragging = false;
15
11
  this.options = options;
16
- let container = null;
17
- if (typeof this.options.container === 'string') {
18
- container = document.querySelector(this.options.container);
12
+ let parent;
13
+ if (typeof options.container === 'string') {
14
+ parent = document.querySelector(options.container);
19
15
  }
20
- else if (this.options.container instanceof HTMLElement) {
21
- container = this.options.container;
16
+ else if (options.container instanceof HTMLElement) {
17
+ parent = options.container;
22
18
  }
23
- if (!container) {
19
+ if (!parent) {
24
20
  throw new Error('Container not found');
25
21
  }
22
+ this.parent = parent;
23
+ const [div, shadow] = this.initHtml();
24
+ parent.appendChild(div);
25
+ this.container = div;
26
+ this.scrollContainer = shadow.querySelector('.scroll');
27
+ this.wrapper = shadow.querySelector('.wrapper');
28
+ this.canvasWrapper = shadow.querySelector('.canvases');
29
+ this.progressWrapper = shadow.querySelector('.progress');
30
+ this.cursor = shadow.querySelector('.cursor');
31
+ this.initEvents();
32
+ }
33
+ initEvents() {
34
+ // Add a click listener
35
+ this.wrapper.addEventListener('click', (e) => {
36
+ const rect = this.wrapper.getBoundingClientRect();
37
+ const x = e.clientX - rect.left;
38
+ const relativeX = x / rect.width;
39
+ this.emit('click', relativeX);
40
+ });
41
+ // Drag
42
+ this.initDrag();
43
+ // Add a scroll listener
44
+ this.scrollContainer.addEventListener('scroll', () => {
45
+ const { scrollLeft, scrollWidth, clientWidth } = this.scrollContainer;
46
+ const startX = scrollLeft / scrollWidth;
47
+ const endX = (scrollLeft + clientWidth) / scrollWidth;
48
+ this.emit('scroll', startX, endX);
49
+ });
50
+ // Re-render the waveform on container resize
51
+ const delay = this.createDelay(100);
52
+ this.resizeObserver = new ResizeObserver(() => {
53
+ delay(() => this.reRender());
54
+ });
55
+ this.resizeObserver.observe(this.scrollContainer);
56
+ }
57
+ initDrag() {
58
+ makeDraggable(this.wrapper,
59
+ // On drag
60
+ (_, __, x) => {
61
+ this.emit('drag', Math.max(0, Math.min(1, x / this.wrapper.clientWidth)));
62
+ },
63
+ // On start drag
64
+ () => (this.isDragging = true),
65
+ // On end drag
66
+ () => (this.isDragging = false));
67
+ }
68
+ getHeight() {
69
+ const defaultHeight = 128;
70
+ if (this.options.height == null)
71
+ return defaultHeight;
72
+ if (!isNaN(Number(this.options.height)))
73
+ return Number(this.options.height);
74
+ if (this.options.height === 'auto')
75
+ return this.parent.clientHeight || defaultHeight;
76
+ return defaultHeight;
77
+ }
78
+ initHtml() {
26
79
  const div = document.createElement('div');
27
80
  const shadow = div.attachShadow({ mode: 'open' });
28
81
  shadow.innerHTML = `
@@ -36,204 +89,347 @@ class Renderer extends EventEmitter {
36
89
  width: 100%;
37
90
  position: relative;
38
91
  }
92
+ :host .noScrollbar {
93
+ scrollbar-color: transparent;
94
+ scrollbar-width: none;
95
+ }
96
+ :host .noScrollbar::-webkit-scrollbar {
97
+ display: none;
98
+ -webkit-appearance: none;
99
+ }
39
100
  :host .wrapper {
40
101
  position: relative;
41
- width: fit-content;
42
- min-width: 100%;
102
+ overflow: visible;
43
103
  z-index: 2;
44
104
  }
105
+ :host .canvases {
106
+ min-height: ${this.getHeight()}px;
107
+ }
108
+ :host .canvases > div {
109
+ position: relative;
110
+ }
45
111
  :host canvas {
46
112
  display: block;
47
- height: ${this.options.height}px;
48
- min-width: 100%;
113
+ position: absolute;
114
+ top: 0;
49
115
  image-rendering: pixelated;
50
116
  }
51
117
  :host .progress {
118
+ pointer-events: none;
52
119
  position: absolute;
53
120
  z-index: 2;
54
121
  top: 0;
55
122
  left: 0;
56
- pointer-events: none;
57
- clip-path: inset(100%);
123
+ width: 0;
124
+ height: 100%;
125
+ overflow: hidden;
126
+ }
127
+ :host .progress > div {
128
+ position: relative;
58
129
  }
59
130
  :host .cursor {
131
+ pointer-events: none;
60
132
  position: absolute;
61
- z-index: 3;
133
+ z-index: 5;
62
134
  top: 0;
63
135
  left: 0;
64
136
  height: 100%;
65
- border-right: 1px solid ${this.options.cursorColor || this.options.progressColor};
66
- margin-left: -1px;
137
+ border-radius: 2px;
67
138
  }
68
139
  </style>
69
140
 
70
- <div class="scroll">
141
+ <div class="scroll" part="scroll">
71
142
  <div class="wrapper">
72
- <canvas></canvas>
73
- <canvas class="progress"></canvas>
74
- <div class="cursor"></div>
143
+ <div class="canvases"></div>
144
+ <div class="progress" part="progress"></div>
145
+ <div class="cursor" part="cursor"></div>
75
146
  </div>
76
147
  </div>
77
148
  `;
78
- this.container = div;
79
- this.scrollContainer = shadow.querySelector('.scroll');
80
- this.mainCanvas = shadow.querySelector('canvas');
81
- this.ctx = this.mainCanvas.getContext('2d', { desynchronized: true });
82
- this.progressCanvas = shadow.querySelector('.progress');
83
- this.cursor = shadow.querySelector('.cursor');
84
- container.appendChild(div);
85
- this.mainCanvas.addEventListener('click', (e) => {
86
- const rect = this.mainCanvas.getBoundingClientRect();
87
- const x = e.clientX - rect.left;
88
- const relativeX = x / rect.width;
89
- this.emit('click', { relativeX });
90
- });
149
+ return [div, shadow];
150
+ }
151
+ setOptions(options) {
152
+ this.options = options;
153
+ // Re-render the waveform
154
+ this.reRender();
155
+ }
156
+ getWrapper() {
157
+ return this.wrapper;
91
158
  }
92
- getContainer() {
93
- return this.scrollContainer.querySelector('.wrapper');
159
+ getScroll() {
160
+ return this.scrollContainer.scrollLeft;
94
161
  }
95
162
  destroy() {
96
163
  this.container.remove();
164
+ this.resizeObserver?.disconnect();
97
165
  }
98
- delay(fn, delayMs = 100) {
99
- if (this.timeout) {
100
- clearTimeout(this.timeout);
101
- }
102
- return new Promise((resolve) => {
103
- this.timeout = setTimeout(() => {
104
- resolve(fn());
105
- }, delayMs);
166
+ createDelay(delayMs = 10) {
167
+ const context = {};
168
+ this.timeouts.push(context);
169
+ return (callback) => {
170
+ context.timeout && clearTimeout(context.timeout);
171
+ context.timeout = setTimeout(callback, delayMs);
172
+ };
173
+ }
174
+ // Convert array of color values to linear gradient
175
+ convertColorValues(color) {
176
+ if (!Array.isArray(color))
177
+ return color || '';
178
+ if (color.length < 2)
179
+ return color[0] || '';
180
+ const canvasElement = document.createElement('canvas');
181
+ const ctx = canvasElement.getContext('2d');
182
+ const gradient = ctx.createLinearGradient(0, 0, 0, canvasElement.height);
183
+ const colorStopPercentage = 1 / (color.length - 1);
184
+ color.forEach((color, index) => {
185
+ const offset = index * colorStopPercentage;
186
+ gradient.addColorStop(offset, color);
106
187
  });
188
+ return gradient;
107
189
  }
108
- renderPeaks(channelData, width, height) {
109
- var _a;
110
- return __awaiter(this, void 0, void 0, function* () {
111
- const { devicePixelRatio } = window;
112
- const { ctx } = this;
113
- const barWidth = this.options.barWidth != null ? this.options.barWidth * devicePixelRatio : 1;
114
- const barGap = this.options.barGap != null ? this.options.barGap * devicePixelRatio : this.options.barWidth ? barWidth / 2 : 0;
115
- const barRadius = (_a = this.options.barRadius) !== null && _a !== void 0 ? _a : 0;
116
- const leftChannel = channelData[0];
117
- const len = leftChannel.length;
118
- const barCount = Math.floor(width / (barWidth + barGap));
119
- const barIndexScale = barCount / len;
120
- const halfHeight = height / 2;
121
- const isMono = channelData.length === 1;
122
- const rightChannel = isMono ? leftChannel : channelData[1];
123
- const useNegative = isMono && rightChannel.some((v) => v < 0);
124
- const draw = (start, end) => {
125
- let prevX = 0;
126
- let prevLeft = 0;
127
- let prevRight = 0;
128
- ctx.beginPath();
129
- ctx.fillStyle = this.options.waveColor;
130
- // Firefox shim until 2023.04.11
131
- if (!ctx.roundRect)
132
- ctx.roundRect = ctx.fillRect;
133
- for (let i = start; i < end; i++) {
134
- const barIndex = Math.round(i * barIndexScale);
135
- if (barIndex > prevX) {
136
- const leftBarHeight = Math.round(prevLeft * halfHeight);
137
- const rightBarHeight = Math.round(prevRight * halfHeight);
138
- ctx.roundRect(prevX * (barWidth + barGap), halfHeight - leftBarHeight, barWidth, leftBarHeight + rightBarHeight || 1, barRadius);
139
- prevX = barIndex;
140
- prevLeft = 0;
141
- prevRight = 0;
142
- }
143
- const leftValue = useNegative ? leftChannel[i] : Math.abs(leftChannel[i]);
144
- const rightValue = useNegative ? rightChannel[i] : Math.abs(rightChannel[i]);
145
- if (leftValue > prevLeft) {
146
- prevLeft = leftValue;
147
- }
148
- // If stereo, both channels are drawn as max values
149
- // If mono with negative values, the bottom channel will be the min negative values
150
- if (useNegative ? rightValue < -prevRight : rightValue > prevRight) {
151
- prevRight = rightValue < 0 ? -rightValue : rightValue;
152
- }
153
- }
154
- ctx.fill();
155
- ctx.closePath();
156
- };
157
- // Clear the canvas
158
- ctx.clearRect(0, 0, width, height);
159
- // Draw the currently visible part of the waveform
160
- const { scrollLeft, scrollWidth, clientWidth } = this.scrollContainer;
161
- const scale = len / scrollWidth;
162
- const start = Math.floor(scrollLeft * scale);
163
- const end = Math.ceil((scrollLeft + clientWidth) * scale);
164
- draw(start, end);
165
- // Draw the progress mask
166
- this.createProgressMask();
167
- // Draw the rest of the waveform with a timeout for better performance
168
- if (start > 0) {
169
- yield this.delay(() => {
170
- draw(0, start);
171
- });
190
+ renderBars(channelData, options, ctx) {
191
+ ctx.fillStyle = this.convertColorValues(options.waveColor);
192
+ // Custom rendering function
193
+ if (options.renderFunction) {
194
+ options.renderFunction(channelData, ctx);
195
+ return;
196
+ }
197
+ const topChannel = channelData[0];
198
+ const bottomChannel = channelData[1] || channelData[0];
199
+ const length = topChannel.length;
200
+ const pixelRatio = window.devicePixelRatio || 1;
201
+ const { width, height } = ctx.canvas;
202
+ const halfHeight = height / 2;
203
+ const barHeight = options.barHeight || 1;
204
+ const barWidth = options.barWidth ? options.barWidth * pixelRatio : 1;
205
+ const barGap = options.barGap ? options.barGap * pixelRatio : options.barWidth ? barWidth / 2 : 0;
206
+ const barRadius = options.barRadius || 0;
207
+ const barIndexScale = width / (barWidth + barGap) / length;
208
+ let max = 1;
209
+ if (options.normalize) {
210
+ max = 0;
211
+ for (let i = 0; i < length; i++) {
212
+ const value = Math.abs(topChannel[i]);
213
+ if (value > max)
214
+ max = value;
172
215
  }
173
- if (end < len) {
174
- yield this.delay(() => {
175
- draw(end, len);
176
- });
216
+ }
217
+ const vScale = (halfHeight / max) * barHeight;
218
+ ctx.beginPath();
219
+ let prevX = 0;
220
+ let maxTop = 0;
221
+ let maxBottom = 0;
222
+ for (let i = 0; i <= length; i++) {
223
+ const x = Math.round(i * barIndexScale);
224
+ if (x > prevX) {
225
+ const leftBarHeight = Math.round(maxTop * vScale);
226
+ const rightBarHeight = Math.round(maxBottom * vScale);
227
+ const barHeight = leftBarHeight + rightBarHeight || 1;
228
+ // Vertical alignment
229
+ let y = halfHeight - leftBarHeight;
230
+ if (options.barAlign === 'top')
231
+ y = 0;
232
+ else if (options.barAlign === 'bottom')
233
+ y = height - barHeight;
234
+ ctx.roundRect(prevX * (barWidth + barGap), y, barWidth, barHeight, barRadius);
235
+ prevX = x;
236
+ maxTop = 0;
237
+ maxBottom = 0;
177
238
  }
178
- // Redraw the progress mask
179
- this.delay(() => {
180
- this.createProgressMask();
181
- });
182
- });
239
+ const magnitudeTop = Math.abs(topChannel[i] || 0);
240
+ const magnitudeBottom = Math.abs(bottomChannel[i] || 0);
241
+ if (magnitudeTop > maxTop)
242
+ maxTop = magnitudeTop;
243
+ if (magnitudeBottom > maxBottom)
244
+ maxBottom = magnitudeBottom;
245
+ }
246
+ ctx.fill();
247
+ ctx.closePath();
183
248
  }
184
- createProgressMask() {
185
- const progressCtx = this.progressCanvas.getContext('2d', { desynchronized: true });
186
- // Set the canvas to the same size as the main canvas
187
- this.progressCanvas.width = this.mainCanvas.width;
188
- this.progressCanvas.height = this.mainCanvas.height;
189
- this.progressCanvas.style.width = this.mainCanvas.style.width;
190
- this.progressCanvas.style.height = this.mainCanvas.style.height;
191
- // Copy the waveform image to the progress canvas
192
- // The main canvas itself is used as the source image
193
- progressCtx.drawImage(this.mainCanvas, 0, 0);
249
+ renderSingleCanvas(channelData, options, width, height, start, end, canvasContainer, progressContainer) {
250
+ const pixelRatio = window.devicePixelRatio || 1;
251
+ const canvas = document.createElement('canvas');
252
+ const length = channelData[0].length;
253
+ canvas.width = Math.round((width * (end - start)) / length);
254
+ canvas.height = height * pixelRatio;
255
+ canvas.style.width = `${Math.floor(canvas.width / pixelRatio)}px`;
256
+ canvas.style.height = `${height}px`;
257
+ canvas.style.left = `${Math.floor((start * width) / pixelRatio / length)}px`;
258
+ canvasContainer.appendChild(canvas);
259
+ const ctx = canvas.getContext('2d');
260
+ this.renderBars(channelData.map((channel) => channel.slice(start, end)), options, ctx);
261
+ // Draw a progress canvas
262
+ const progressCanvas = canvas.cloneNode();
263
+ progressContainer.appendChild(progressCanvas);
264
+ const progressCtx = progressCanvas.getContext('2d');
265
+ if (canvas.width > 0 && canvas.height > 0) {
266
+ progressCtx.drawImage(canvas, 0, 0);
267
+ }
194
268
  // Set the composition method to draw only where the waveform is drawn
195
269
  progressCtx.globalCompositeOperation = 'source-in';
196
- progressCtx.fillStyle = this.options.progressColor;
270
+ progressCtx.fillStyle = this.convertColorValues(options.progressColor);
197
271
  // This rectangle acts as a mask thanks to the composition method
198
- progressCtx.fillRect(0, 0, this.progressCanvas.width, this.progressCanvas.height);
272
+ progressCtx.fillRect(0, 0, canvas.width, canvas.height);
273
+ }
274
+ renderWaveform(channelData, options, width) {
275
+ // A container for canvases
276
+ const canvasContainer = document.createElement('div');
277
+ const height = this.getHeight();
278
+ canvasContainer.style.height = `${height}px`;
279
+ this.canvasWrapper.style.minHeight = `${height}px`;
280
+ this.canvasWrapper.appendChild(canvasContainer);
281
+ // A container for progress canvases
282
+ const progressContainer = canvasContainer.cloneNode();
283
+ this.progressWrapper.appendChild(progressContainer);
284
+ // Determine the currently visible part of the waveform
285
+ const { scrollLeft, scrollWidth, clientWidth } = this.scrollContainer;
286
+ const len = channelData[0].length;
287
+ const scale = len / scrollWidth;
288
+ let viewportWidth = Math.min(Renderer.MAX_CANVAS_WIDTH, clientWidth);
289
+ // Adjust width to avoid gaps between canvases when using bars
290
+ if (options.barWidth || options.barGap) {
291
+ const barWidth = options.barWidth || 0.5;
292
+ const barGap = options.barGap || barWidth / 2;
293
+ const totalBarWidth = barWidth + barGap;
294
+ if (viewportWidth % totalBarWidth !== 0) {
295
+ viewportWidth = Math.floor(viewportWidth / totalBarWidth) * totalBarWidth;
296
+ }
297
+ }
298
+ const start = Math.floor(Math.abs(scrollLeft) * scale);
299
+ const end = Math.floor(start + viewportWidth * scale);
300
+ const viewportLen = end - start;
301
+ // Draw a portion of the waveform from start peak to end peak
302
+ const draw = (start, end) => {
303
+ this.renderSingleCanvas(channelData, options, width, height, Math.max(0, start), Math.min(end, len), canvasContainer, progressContainer);
304
+ };
305
+ // Draw the waveform in viewport chunks, each with a delay
306
+ const headDelay = this.createDelay();
307
+ const tailDelay = this.createDelay();
308
+ const renderHead = (fromIndex, toIndex) => {
309
+ draw(fromIndex, toIndex);
310
+ if (fromIndex > 0) {
311
+ headDelay(() => {
312
+ renderHead(fromIndex - viewportLen, toIndex - viewportLen);
313
+ });
314
+ }
315
+ };
316
+ const renderTail = (fromIndex, toIndex) => {
317
+ draw(fromIndex, toIndex);
318
+ if (toIndex < len) {
319
+ tailDelay(() => {
320
+ renderTail(fromIndex + viewportLen, toIndex + viewportLen);
321
+ });
322
+ }
323
+ };
324
+ renderHead(start, end);
325
+ if (end < len) {
326
+ renderTail(end, end + viewportLen);
327
+ }
199
328
  }
200
329
  render(audioData) {
201
- // Determine the width of the canvas
202
- const { devicePixelRatio } = window;
203
- const parentWidth = this.options.fillParent ? this.scrollContainer.clientWidth * devicePixelRatio : 0;
204
- const scrollWidth = audioData.duration * this.options.minPxPerSec;
205
- const isScrolling = scrollWidth > parentWidth;
206
- const width = Math.max(1, isScrolling ? scrollWidth : parentWidth);
207
- const { height } = this.options;
208
- this.mainCanvas.width = width;
209
- this.mainCanvas.height = height;
210
- this.mainCanvas.style.width = Math.floor(scrollWidth / devicePixelRatio) + 'px';
211
- // First two channels are used
212
- const channelData = [audioData.getChannelData(0)];
213
- if (audioData.numberOfChannels > 1) {
214
- channelData.push(audioData.getChannelData(1));
215
- }
216
- this.renderPeaks(channelData, width, height);
217
- }
218
- zoom(audioData, minPxPerSec) {
330
+ // Clear previous timeouts
331
+ this.timeouts.forEach((context) => context.timeout && clearTimeout(context.timeout));
332
+ this.timeouts = [];
333
+ // Determine the width of the waveform
334
+ const pixelRatio = window.devicePixelRatio || 1;
335
+ const parentWidth = this.scrollContainer.clientWidth;
336
+ const scrollWidth = Math.ceil(audioData.duration * (this.options.minPxPerSec || 0));
337
+ // Whether the container should scroll
338
+ this.isScrolling = scrollWidth > parentWidth;
339
+ const useParentWidth = this.options.fillParent && !this.isScrolling;
340
+ // Width and height of the waveform in pixels
341
+ const width = (useParentWidth ? parentWidth : scrollWidth) * pixelRatio;
342
+ // Set the width of the wrapper
343
+ this.wrapper.style.width = useParentWidth ? '100%' : `${scrollWidth}px`;
344
+ // Set additional styles
345
+ this.scrollContainer.style.overflowX = this.isScrolling ? 'auto' : 'hidden';
346
+ this.scrollContainer.classList.toggle('noScrollbar', !!this.options.hideScrollbar);
347
+ this.cursor.style.backgroundColor = `${this.options.cursorColor || this.options.progressColor}`;
348
+ this.cursor.style.width = `${this.options.cursorWidth}px`;
349
+ // Clear the canvases
350
+ this.canvasWrapper.innerHTML = '';
351
+ this.progressWrapper.innerHTML = '';
352
+ // Render the waveform
353
+ if (this.options.splitChannels) {
354
+ // Render a waveform for each channel
355
+ for (let i = 0; i < audioData.numberOfChannels; i++) {
356
+ const options = { ...this.options, ...this.options.splitChannels[i] };
357
+ this.renderWaveform([audioData.getChannelData(i)], options, width);
358
+ }
359
+ }
360
+ else {
361
+ // Render a single waveform for the first two channels (left and right)
362
+ const channels = [audioData.getChannelData(0)];
363
+ if (audioData.numberOfChannels > 1)
364
+ channels.push(audioData.getChannelData(1));
365
+ this.renderWaveform(channels, this.options, width);
366
+ }
367
+ this.audioData = audioData;
368
+ this.emit('render');
369
+ }
370
+ reRender() {
371
+ // Return if the waveform has not been rendered yet
372
+ if (!this.audioData)
373
+ return;
219
374
  // Remember the current cursor position
220
- const oldCursorPosition = this.cursor.getBoundingClientRect().left;
221
- this.options.minPxPerSec = minPxPerSec;
222
- this.render(audioData);
375
+ const oldCursorPosition = this.progressWrapper.clientWidth;
376
+ // Set the new zoom level and re-render the waveform
377
+ this.render(this.audioData);
223
378
  // Adjust the scroll position so that the cursor stays in the same place
224
- const newCursortPosition = this.cursor.getBoundingClientRect().left;
379
+ const newCursortPosition = this.progressWrapper.clientWidth;
225
380
  this.scrollContainer.scrollLeft += newCursortPosition - oldCursorPosition;
226
381
  }
227
- renderProgress(progress, autoCenter = false) {
228
- this.progressCanvas.style.clipPath = `inset(0 ${100 - progress * 100}% 0 0)`;
229
- this.cursor.style.left = `${progress * 100}%`;
230
- if (autoCenter) {
231
- const center = this.container.clientWidth / 2;
232
- const fullWidth = this.mainCanvas.clientWidth;
233
- if (fullWidth * progress >= center) {
234
- this.scrollContainer.scrollLeft = fullWidth * progress - center;
382
+ zoom(minPxPerSec) {
383
+ this.options.minPxPerSec = minPxPerSec;
384
+ this.reRender();
385
+ }
386
+ scrollIntoView(progress, isPlaying = false) {
387
+ const { clientWidth, scrollLeft, scrollWidth } = this.scrollContainer;
388
+ const progressWidth = scrollWidth * progress;
389
+ const center = clientWidth / 2;
390
+ const minScroll = isPlaying && this.options.autoCenter && !this.isDragging ? center : clientWidth;
391
+ if (progressWidth > scrollLeft + minScroll || progressWidth < scrollLeft) {
392
+ // Scroll to the center
393
+ if (this.options.autoCenter && !this.isDragging) {
394
+ // If the cursor is in viewport but not centered, scroll to the center slowly
395
+ const minDiff = center / 20;
396
+ if (progressWidth - (scrollLeft + center) >= minDiff && progressWidth < scrollLeft + clientWidth) {
397
+ this.scrollContainer.scrollLeft += minDiff;
398
+ }
399
+ else {
400
+ // Otherwise, scroll to the center immediately
401
+ this.scrollContainer.scrollLeft = progressWidth - center;
402
+ }
403
+ }
404
+ else if (this.isDragging) {
405
+ // Scroll just a little bit to allow for some space between the cursor and the edge
406
+ const gap = 10;
407
+ this.scrollContainer.scrollLeft =
408
+ progressWidth < scrollLeft ? progressWidth - gap : progressWidth - clientWidth + gap;
409
+ }
410
+ else {
411
+ // Scroll to the beginning
412
+ this.scrollContainer.scrollLeft = progressWidth;
235
413
  }
236
414
  }
415
+ // Emit the scroll event
416
+ {
417
+ const { scrollLeft } = this.scrollContainer;
418
+ const startX = scrollLeft / scrollWidth;
419
+ const endX = (scrollLeft + clientWidth) / scrollWidth;
420
+ this.emit('scroll', startX, endX);
421
+ }
422
+ }
423
+ renderProgress(progress, isPlaying) {
424
+ if (isNaN(progress))
425
+ return;
426
+ this.progressWrapper.style.width = `${progress * 100}%`;
427
+ this.cursor.style.left = `${progress * 100}%`;
428
+ this.cursor.style.marginLeft = Math.round(progress * 100) === 100 ? `-${this.options.cursorWidth}px` : '';
429
+ if (this.isScrolling && this.options.autoScroll) {
430
+ this.scrollIntoView(progress, isPlaying);
431
+ }
237
432
  }
238
433
  }
434
+ Renderer.MAX_CANVAS_WIDTH = 4000;
239
435
  export default Renderer;
package/dist/timer.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import EventEmitter from './event-emitter.js';
2
2
  type TimerEvents = {
3
- tick: void;
3
+ tick: [];
4
4
  };
5
5
  declare class Timer extends EventEmitter<TimerEvents> {
6
6
  private unsubscribe;