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

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 (50) hide show
  1. package/README.md +90 -18
  2. package/dist/base-plugin.d.ts +6 -4
  3. package/dist/base-plugin.js +9 -3
  4. package/dist/decoder.d.ts +8 -7
  5. package/dist/decoder.js +43 -38
  6. package/dist/event-emitter.d.ts +16 -10
  7. package/dist/event-emitter.js +38 -16
  8. package/dist/fetcher.d.ts +4 -3
  9. package/dist/fetcher.js +5 -15
  10. package/dist/player.d.ts +31 -11
  11. package/dist/player.js +58 -31
  12. package/dist/plugins/envelope.d.ts +71 -0
  13. package/dist/plugins/envelope.js +347 -0
  14. package/dist/plugins/envelope.min.js +1 -0
  15. package/dist/plugins/minimap.d.ts +39 -0
  16. package/dist/plugins/minimap.js +117 -0
  17. package/dist/plugins/minimap.min.js +1 -0
  18. package/dist/plugins/multitrack.d.ts +85 -12
  19. package/dist/plugins/multitrack.js +331 -84
  20. package/dist/plugins/multitrack.min.js +1 -0
  21. package/dist/plugins/record.d.ts +26 -0
  22. package/dist/plugins/record.js +126 -0
  23. package/dist/plugins/record.min.js +1 -0
  24. package/dist/plugins/regions.d.ts +83 -43
  25. package/dist/plugins/regions.js +362 -192
  26. package/dist/plugins/regions.min.js +1 -0
  27. package/dist/plugins/spectrogram.d.ts +69 -0
  28. package/dist/plugins/spectrogram.js +340 -0
  29. package/dist/plugins/spectrogram.min.js +1 -0
  30. package/dist/plugins/timeline.d.ts +25 -9
  31. package/dist/plugins/timeline.js +65 -24
  32. package/dist/plugins/timeline.min.js +1 -0
  33. package/dist/renderer.d.ts +29 -13
  34. package/dist/renderer.js +280 -161
  35. package/dist/timer.d.ts +1 -1
  36. package/dist/wavesurfer.d.ts +151 -0
  37. package/dist/wavesurfer.js +241 -0
  38. package/dist/wavesurfer.min.js +1 -1
  39. package/package.json +54 -27
  40. package/dist/index.d.ts +0 -117
  41. package/dist/index.js +0 -227
  42. package/dist/player-webaudio.d.ts +0 -8
  43. package/dist/player-webaudio.js +0 -32
  44. package/dist/plugins/xmultitrack.d.ts +0 -44
  45. package/dist/plugins/xmultitrack.js +0 -260
  46. package/dist/react/useWavesurfer.d.ts +0 -5
  47. package/dist/react/useWavesurfer.js +0 -20
  48. package/dist/wavesurfer.Multitrack.min.js +0 -1
  49. package/dist/wavesurfer.Regions.min.js +0 -1
  50. package/dist/wavesurfer.Timeline.min.js +0 -1
@@ -1,38 +1,54 @@
1
1
  import EventEmitter from './event-emitter.js';
2
- type RendererOptions = {
3
- container: HTMLElement | string | null;
2
+ export type RendererStyleOptions = {
4
3
  height: number;
5
4
  waveColor: string;
6
5
  progressColor: string;
7
6
  cursorColor?: string;
7
+ cursorWidth: number;
8
8
  minPxPerSec: number;
9
9
  fillParent: boolean;
10
10
  barWidth?: number;
11
11
  barGap?: number;
12
12
  barRadius?: number;
13
+ barHeight?: number;
14
+ hideScrollbar?: boolean;
15
+ autoCenter?: boolean;
16
+ autoScroll?: boolean;
13
17
  };
14
18
  type RendererEvents = {
15
- click: {
16
- relativeX: number;
17
- };
19
+ click: [relativeX: number];
20
+ drag: [relativeX: number];
21
+ scroll: [relativeStart: number, relativeEnd: number];
22
+ render: [];
18
23
  };
19
24
  declare class Renderer extends EventEmitter<RendererEvents> {
25
+ private static MAX_CANVAS_WIDTH;
20
26
  private options;
21
27
  private container;
22
28
  private scrollContainer;
23
- private mainCanvas;
24
- private progressCanvas;
29
+ private wrapper;
30
+ private canvasWrapper;
31
+ private progressWrapper;
25
32
  private cursor;
26
- private ctx;
27
33
  private timeout;
28
- constructor(options: RendererOptions);
29
- getContainer(): HTMLElement;
34
+ private isScrolling;
35
+ private audioData;
36
+ private resizeObserver;
37
+ private isDragging;
38
+ constructor(container: HTMLElement | string | null, options: RendererStyleOptions);
39
+ private initEvents;
40
+ private initDrag;
41
+ private initHtml;
42
+ setOptions(options: RendererStyleOptions): void;
43
+ getWrapper(): HTMLElement;
44
+ getScroll(): number;
30
45
  destroy(): void;
31
46
  private delay;
32
47
  private renderPeaks;
33
- private createProgressMask;
34
48
  render(audioData: AudioBuffer): void;
35
- zoom(audioData: AudioBuffer, minPxPerSec: number): void;
36
- renderProgress(progress: number, autoCenter?: boolean): void;
49
+ reRender(): void;
50
+ zoom(minPxPerSec: number): void;
51
+ private scrollIntoView;
52
+ renderProgress(progress: number, isPlaying?: boolean): void;
37
53
  }
38
54
  export default Renderer;
package/dist/renderer.js CHANGED
@@ -1,28 +1,77 @@
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
- };
10
1
  import EventEmitter from './event-emitter.js';
11
2
  class Renderer extends EventEmitter {
12
- constructor(options) {
3
+ constructor(container, options) {
13
4
  super();
5
+ this.options = {
6
+ height: 0,
7
+ };
14
8
  this.timeout = null;
15
- this.options = options;
16
- let container = null;
17
- if (typeof this.options.container === 'string') {
18
- container = document.querySelector(this.options.container);
19
- }
20
- else if (this.options.container instanceof HTMLElement) {
21
- container = this.options.container;
9
+ this.isScrolling = false;
10
+ this.audioData = null;
11
+ this.resizeObserver = null;
12
+ this.isDragging = false;
13
+ this.options = { ...options };
14
+ if (typeof container === 'string') {
15
+ container = document.querySelector(container);
22
16
  }
23
17
  if (!container) {
24
18
  throw new Error('Container not found');
25
19
  }
20
+ const [div, shadow] = this.initHtml();
21
+ container.appendChild(div);
22
+ this.container = div;
23
+ this.scrollContainer = shadow.querySelector('.scroll');
24
+ this.wrapper = shadow.querySelector('.wrapper');
25
+ this.canvasWrapper = shadow.querySelector('.canvases');
26
+ this.progressWrapper = shadow.querySelector('.progress');
27
+ this.cursor = shadow.querySelector('.cursor');
28
+ this.initEvents();
29
+ }
30
+ initEvents() {
31
+ // Add a click listener
32
+ this.wrapper.addEventListener('click', (e) => {
33
+ const rect = this.wrapper.getBoundingClientRect();
34
+ const x = e.clientX - rect.left;
35
+ const relativeX = x / rect.width;
36
+ this.emit('click', relativeX);
37
+ });
38
+ // Drag
39
+ this.initDrag();
40
+ // Add a scroll listener
41
+ this.scrollContainer.addEventListener('scroll', () => {
42
+ const { scrollLeft, scrollWidth, clientWidth } = this.scrollContainer;
43
+ const startX = scrollLeft / scrollWidth;
44
+ const endX = (scrollLeft + clientWidth) / scrollWidth;
45
+ this.emit('scroll', startX, endX);
46
+ });
47
+ // Re-render the waveform on container resize
48
+ this.resizeObserver = new ResizeObserver(() => {
49
+ this.delay(() => this.reRender(), 100);
50
+ });
51
+ this.resizeObserver.observe(this.scrollContainer);
52
+ }
53
+ initDrag() {
54
+ this.wrapper.addEventListener('mousedown', (e) => {
55
+ const minDx = 5;
56
+ const x = e.clientX;
57
+ const move = (e) => {
58
+ const diff = Math.abs(e.clientX - x);
59
+ if (diff >= minDx) {
60
+ this.isDragging = true;
61
+ const rect = this.wrapper.getBoundingClientRect();
62
+ this.emit('drag', Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width)));
63
+ }
64
+ };
65
+ const up = () => {
66
+ document.removeEventListener('mousemove', move);
67
+ document.removeEventListener('mouseup', up);
68
+ this.isDragging = false;
69
+ };
70
+ document.addEventListener('mousemove', move);
71
+ document.addEventListener('mouseup', up);
72
+ });
73
+ }
74
+ initHtml() {
26
75
  const div = document.createElement('div');
27
76
  const shadow = div.attachShadow({ mode: 'open' });
28
77
  shadow.innerHTML = `
@@ -36,66 +85,79 @@ class Renderer extends EventEmitter {
36
85
  width: 100%;
37
86
  position: relative;
38
87
  }
88
+ :host .noScrollbar {
89
+ scrollbar-color: transparent;
90
+ scrollbar-width: none;
91
+ }
92
+ :host .noScrollbar::-webkit-scrollbar {
93
+ display: none;
94
+ -webkit-appearance: none;
95
+ }
39
96
  :host .wrapper {
40
97
  position: relative;
41
- width: fit-content;
42
- min-width: 100%;
98
+ overflow: visible;
43
99
  z-index: 2;
44
100
  }
101
+ :host .canvases {
102
+ position: relative;
103
+ height: ${this.options.height}px;
104
+ }
45
105
  :host canvas {
46
106
  display: block;
47
- height: ${this.options.height}px;
48
- min-width: 100%;
107
+ position: absolute;
108
+ top: 0;
49
109
  image-rendering: pixelated;
110
+ height: ${this.options.height}px;
50
111
  }
51
112
  :host .progress {
113
+ pointer-events: none;
52
114
  position: absolute;
53
115
  z-index: 2;
54
116
  top: 0;
55
117
  left: 0;
56
- pointer-events: none;
57
- clip-path: inset(100%);
118
+ width: 0;
119
+ height: 100%;
120
+ overflow: hidden;
58
121
  }
59
122
  :host .cursor {
123
+ pointer-events: none;
60
124
  position: absolute;
61
- z-index: 3;
125
+ z-index: 2;
62
126
  top: 0;
63
127
  left: 0;
64
128
  height: 100%;
65
- border-right: 1px solid ${this.options.cursorColor || this.options.progressColor};
66
- margin-left: -1px;
129
+ border-radius: 2px;
130
+ width: ${this.options.cursorWidth}px;
131
+ background-color: ${this.options.cursorColor || this.options.progressColor};
67
132
  }
68
133
  </style>
69
134
 
70
- <div class="scroll">
135
+ <div class="scroll" part="scroll">
71
136
  <div class="wrapper">
72
- <canvas></canvas>
73
- <canvas class="progress"></canvas>
74
- <div class="cursor"></div>
137
+ <div class="canvases"></div>
138
+ <div class="progress"></div>
139
+ <div class="cursor" part="cursor"></div>
75
140
  </div>
76
141
  </div>
77
142
  `;
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
- });
143
+ return [div, shadow];
144
+ }
145
+ setOptions(options) {
146
+ this.options = options;
147
+ // Re-render the waveform
148
+ this.reRender();
91
149
  }
92
- getContainer() {
93
- return this.scrollContainer.querySelector('.wrapper');
150
+ getWrapper() {
151
+ return this.wrapper;
152
+ }
153
+ getScroll() {
154
+ return this.scrollContainer.scrollLeft;
94
155
  }
95
156
  destroy() {
96
157
  this.container.remove();
158
+ this.resizeObserver?.disconnect();
97
159
  }
98
- delay(fn, delayMs = 100) {
160
+ delay(fn, delayMs = 10) {
99
161
  if (this.timeout) {
100
162
  clearTimeout(this.timeout);
101
163
  }
@@ -105,135 +167,192 @@ class Renderer extends EventEmitter {
105
167
  }, delayMs);
106
168
  });
107
169
  }
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
- }
170
+ async renderPeaks(audioData, width, height, pixelRatio) {
171
+ const barWidth = this.options.barWidth != null && !isNaN(this.options.barWidth) ? this.options.barWidth * pixelRatio : 1;
172
+ const barGap = this.options.barGap != null && !isNaN(this.options.barGap)
173
+ ? this.options.barGap * pixelRatio
174
+ : this.options.barWidth
175
+ ? barWidth / 2
176
+ : 0;
177
+ const barRadius = this.options.barRadius || 0;
178
+ const scaleY = this.options.barHeight || 1;
179
+ const leftChannel = audioData.getChannelData(0);
180
+ const len = leftChannel.length;
181
+ const barCount = Math.floor(width / (barWidth + barGap));
182
+ const barIndexScale = barCount / len;
183
+ const halfHeight = height / 2;
184
+ const isMono = audioData.numberOfChannels === 1;
185
+ const rightChannel = isMono ? leftChannel : audioData.getChannelData(1);
186
+ const useNegative = isMono && rightChannel.some((v) => v < 0);
187
+ const draw = (start, end) => {
188
+ let prevX = 0;
189
+ let prevLeft = 0;
190
+ let prevRight = 0;
191
+ const canvas = document.createElement('canvas');
192
+ canvas.width = Math.round((width * (end - start)) / len);
193
+ canvas.height = this.options.height;
194
+ canvas.style.width = `${Math.floor(canvas.width / pixelRatio)}px`;
195
+ canvas.style.height = `${this.options.height}px`;
196
+ canvas.style.left = `${Math.floor((start * width) / pixelRatio / len)}px`;
197
+ this.canvasWrapper.appendChild(canvas);
198
+ const ctx = canvas.getContext('2d', {
199
+ desynchronized: true,
200
+ });
201
+ ctx.beginPath();
202
+ ctx.fillStyle = this.options.waveColor ?? '';
203
+ // Firefox shim until 2023.04.11
204
+ if (!ctx.roundRect)
205
+ ctx.roundRect = ctx.fillRect;
206
+ for (let i = start; i < end; i++) {
207
+ const barIndex = Math.round((i - start) * barIndexScale);
208
+ if (barIndex > prevX) {
209
+ const leftBarHeight = Math.round(prevLeft * halfHeight * scaleY);
210
+ const rightBarHeight = Math.round(prevRight * halfHeight * scaleY);
211
+ ctx.roundRect(prevX * (barWidth + barGap), halfHeight - leftBarHeight, barWidth, leftBarHeight + (rightBarHeight || 1), barRadius);
212
+ prevX = barIndex;
213
+ prevLeft = 0;
214
+ prevRight = 0;
215
+ }
216
+ const leftValue = useNegative ? leftChannel[i] : Math.abs(leftChannel[i]);
217
+ const rightValue = useNegative ? rightChannel[i] : Math.abs(rightChannel[i]);
218
+ if (leftValue > prevLeft) {
219
+ prevLeft = leftValue;
220
+ }
221
+ // If stereo, both channels are drawn as max values
222
+ // If mono with negative values, the bottom channel will be the min negative values
223
+ if (useNegative ? rightValue < -prevRight : rightValue > prevRight) {
224
+ prevRight = rightValue < 0 ? -rightValue : rightValue;
153
225
  }
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
- });
172
226
  }
173
- if (end < len) {
174
- yield this.delay(() => {
175
- draw(end, len);
176
- });
227
+ ctx.fill();
228
+ ctx.closePath();
229
+ // Draw a progress canvas
230
+ const progressCanvas = canvas.cloneNode();
231
+ this.progressWrapper.appendChild(progressCanvas);
232
+ const progressCtx = progressCanvas.getContext('2d', {
233
+ desynchronized: true,
234
+ });
235
+ if (canvas.width > 0 && canvas.height > 0) {
236
+ progressCtx.drawImage(canvas, 0, 0);
177
237
  }
178
- // Redraw the progress mask
179
- this.delay(() => {
180
- this.createProgressMask();
238
+ // Set the composition method to draw only where the waveform is drawn
239
+ progressCtx.globalCompositeOperation = 'source-in';
240
+ progressCtx.fillStyle = this.options.progressColor ?? '';
241
+ // This rectangle acts as a mask thanks to the composition method
242
+ progressCtx.fillRect(0, 0, canvas.width, canvas.height);
243
+ };
244
+ // Clear the canvas
245
+ this.canvasWrapper.innerHTML = '';
246
+ this.progressWrapper.innerHTML = '';
247
+ // Determine the currently visible part of the waveform
248
+ const { scrollLeft, scrollWidth, clientWidth } = this.scrollContainer;
249
+ const scale = len / scrollWidth;
250
+ let viewportWidth = Math.min(Renderer.MAX_CANVAS_WIDTH, clientWidth);
251
+ viewportWidth -= viewportWidth % ((barWidth + barGap) / pixelRatio);
252
+ const start = Math.floor(Math.abs(scrollLeft) * scale);
253
+ const end = Math.ceil(start + viewportWidth * scale);
254
+ // Draw the visible portion of the waveform
255
+ draw(start, end);
256
+ // Draw the rest of the waveform with a timeout for better performance
257
+ const step = end - start;
258
+ for (let i = end; i < len; i += step) {
259
+ await this.delay(() => {
260
+ draw(i, Math.min(len, i + step));
181
261
  });
182
- });
183
- }
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);
194
- // Set the composition method to draw only where the waveform is drawn
195
- progressCtx.globalCompositeOperation = 'source-in';
196
- progressCtx.fillStyle = this.options.progressColor;
197
- // This rectangle acts as a mask thanks to the composition method
198
- progressCtx.fillRect(0, 0, this.progressCanvas.width, this.progressCanvas.height);
262
+ }
263
+ for (let i = start - 1; i >= 0; i -= step) {
264
+ await this.delay(() => {
265
+ draw(Math.max(0, i - step), i);
266
+ });
267
+ }
199
268
  }
200
269
  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);
270
+ // Determine the width of the waveform
271
+ const pixelRatio = window.devicePixelRatio || 1;
272
+ const parentWidth = this.scrollContainer.clientWidth;
273
+ const scrollWidth = Math.ceil(audioData.duration * (this.options.minPxPerSec || 0));
274
+ // Whether the container should scroll
275
+ this.isScrolling = scrollWidth > parentWidth;
276
+ const useParentWidth = this.options.fillParent && !this.isScrolling;
277
+ // Width and height of the waveform in pixels
278
+ const width = (useParentWidth ? parentWidth : scrollWidth) * pixelRatio;
207
279
  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);
280
+ // Set the width of the wrapper
281
+ this.wrapper.style.width = useParentWidth ? '100%' : `${scrollWidth}px`;
282
+ // Set additional styles
283
+ this.scrollContainer.style.overflowX = this.isScrolling ? 'auto' : 'hidden';
284
+ this.scrollContainer.classList.toggle('noScrollbar', !!this.options.hideScrollbar);
285
+ this.cursor.style.backgroundColor = `${this.options.cursorColor || this.options.progressColor}`;
286
+ this.cursor.style.width = `${this.options.cursorWidth}px`;
287
+ this.canvasWrapper.style.height = `${this.options.height}px`;
288
+ // Render the waveform
289
+ this.renderPeaks(audioData, width, height, pixelRatio);
290
+ this.audioData = audioData;
291
+ this.emit('render');
217
292
  }
218
- zoom(audioData, minPxPerSec) {
293
+ reRender() {
294
+ // Return if the waveform has not been rendered yet
295
+ if (!this.audioData)
296
+ return;
219
297
  // Remember the current cursor position
220
- const oldCursorPosition = this.cursor.getBoundingClientRect().left;
221
- this.options.minPxPerSec = minPxPerSec;
222
- this.render(audioData);
298
+ const oldCursorPosition = this.progressWrapper.clientWidth;
299
+ // Set the new zoom level and re-render the waveform
300
+ this.render(this.audioData);
223
301
  // Adjust the scroll position so that the cursor stays in the same place
224
- const newCursortPosition = this.cursor.getBoundingClientRect().left;
302
+ const newCursortPosition = this.progressWrapper.clientWidth;
225
303
  this.scrollContainer.scrollLeft += newCursortPosition - oldCursorPosition;
226
304
  }
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;
305
+ zoom(minPxPerSec) {
306
+ this.options.minPxPerSec = minPxPerSec;
307
+ this.reRender();
308
+ }
309
+ scrollIntoView(progress, isPlaying = false) {
310
+ const { clientWidth, scrollLeft, scrollWidth } = this.scrollContainer;
311
+ const progressWidth = scrollWidth * progress;
312
+ const center = clientWidth / 2;
313
+ const minScroll = isPlaying && this.options.autoCenter && !this.isDragging ? center : clientWidth;
314
+ if (progressWidth > scrollLeft + minScroll || progressWidth < scrollLeft) {
315
+ // Scroll to the center
316
+ if (this.options.autoCenter && !this.isDragging) {
317
+ // If the cursor is in viewport but not centered, scroll to the center slowly
318
+ const minDiff = center / 20;
319
+ if (progressWidth - (scrollLeft + center) >= minDiff && progressWidth < scrollLeft + clientWidth) {
320
+ this.scrollContainer.scrollLeft += minDiff;
321
+ }
322
+ else {
323
+ // Otherwise, scroll to the center immediately
324
+ this.scrollContainer.scrollLeft = progressWidth - center;
325
+ }
326
+ }
327
+ else if (this.isDragging) {
328
+ // Scroll just a little bit to allow for some space between the cursor and the edge
329
+ const gap = 10;
330
+ this.scrollContainer.scrollLeft =
331
+ progressWidth < scrollLeft ? progressWidth - gap : progressWidth - clientWidth + gap;
332
+ }
333
+ else {
334
+ // Scroll to the beginning
335
+ this.scrollContainer.scrollLeft = progressWidth;
235
336
  }
236
337
  }
338
+ // Emit the scroll event
339
+ {
340
+ const { scrollLeft } = this.scrollContainer;
341
+ const startX = scrollLeft / scrollWidth;
342
+ const endX = (scrollLeft + clientWidth) / scrollWidth;
343
+ this.emit('scroll', startX, endX);
344
+ }
345
+ }
346
+ renderProgress(progress, isPlaying) {
347
+ if (isNaN(progress))
348
+ return;
349
+ this.progressWrapper.style.width = `${progress * 100}%`;
350
+ this.cursor.style.left = `${progress * 100}%`;
351
+ this.cursor.style.marginLeft = Math.round(progress * 100) === 100 ? `-${this.options.cursorWidth}px` : '';
352
+ if (this.isScrolling && this.options.autoScroll) {
353
+ this.scrollIntoView(progress, isPlaying);
354
+ }
237
355
  }
238
356
  }
357
+ Renderer.MAX_CANVAS_WIDTH = 4000;
239
358
  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;