wavesurfer.js 7.0.0-alpha.2 → 7.0.0-alpha.21

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.
@@ -0,0 +1,231 @@
1
+ import BasePlugin from '../base-plugin.js';
2
+ const MIN_WIDTH = 10;
3
+ const defaultOptions = {
4
+ dragSelection: true,
5
+ draggable: true,
6
+ resizable: true,
7
+ };
8
+ const style = (element, styles) => {
9
+ for (const key in styles) {
10
+ element.style[key] = styles[key] || '';
11
+ }
12
+ };
13
+ const el = (tagName, css) => {
14
+ const element = document.createElement(tagName);
15
+ style(element, css);
16
+ return element;
17
+ };
18
+ class RegionsPlugin extends BasePlugin {
19
+ dragStart = NaN;
20
+ regionsContainer;
21
+ regions = [];
22
+ createdRegion = null;
23
+ modifiedRegion = null;
24
+ isResizingLeft = false;
25
+ isMoving = false;
26
+ /** Create an instance of RegionsPlugin */
27
+ constructor(params, options) {
28
+ super(params, options);
29
+ this.options = Object.assign({}, defaultOptions, options);
30
+ this.regionsContainer = this.initRegionsContainer();
31
+ this.subscriptions.push(this.wavesurfer.once('decode', () => {
32
+ this.wrapper.appendChild(this.regionsContainer);
33
+ }));
34
+ this.wrapper.addEventListener('mousedown', this.handleMouseDown);
35
+ document.addEventListener('mousemove', this.handleMouseMove);
36
+ document.addEventListener('mouseup', this.handleMouseUp);
37
+ }
38
+ /** Unmount */
39
+ destroy() {
40
+ this.wrapper.removeEventListener('mousedown', this.handleMouseDown);
41
+ document.removeEventListener('mousemove', this.handleMouseMove);
42
+ document.removeEventListener('mouseup', this.handleMouseUp, true);
43
+ this.regionsContainer.remove();
44
+ super.destroy();
45
+ }
46
+ initRegionsContainer() {
47
+ return el('div', {
48
+ position: 'absolute',
49
+ top: '0',
50
+ left: '0',
51
+ width: '100%',
52
+ height: '100%',
53
+ zIndex: '3',
54
+ pointerEvents: 'none',
55
+ });
56
+ }
57
+ handleMouseDown = (e) => {
58
+ if (this.options.draggable || this.options.resizable || this.options.dragSelection) {
59
+ this.dragStart = e.clientX - this.wrapper.getBoundingClientRect().left;
60
+ }
61
+ };
62
+ handleMouseMove = (e) => {
63
+ const box = this.wrapper.getBoundingClientRect();
64
+ const { width } = box;
65
+ const dragEnd = e.clientX - box.left;
66
+ if (this.options.draggable && this.modifiedRegion && this.isMoving) {
67
+ this.moveRegion(this.modifiedRegion, (dragEnd - this.dragStart) / width);
68
+ this.dragStart = dragEnd;
69
+ return;
70
+ }
71
+ if (this.options.resizable && this.modifiedRegion) {
72
+ this.updateRegion(this.modifiedRegion, this.isResizingLeft ? dragEnd / width : undefined, this.isResizingLeft ? undefined : dragEnd / width);
73
+ return;
74
+ }
75
+ if (this.options.dragSelection && !isNaN(this.dragStart)) {
76
+ const dragEnd = e.clientX - this.regionsContainer.getBoundingClientRect().left;
77
+ if (dragEnd - this.dragStart >= MIN_WIDTH) {
78
+ if (!this.createdRegion) {
79
+ this.wrapper.style.pointerEvents = 'none';
80
+ this.createdRegion = this.createRegion(this.dragStart / width, dragEnd / width);
81
+ }
82
+ else {
83
+ this.updateRegion(this.createdRegion, this.dragStart / width, dragEnd / width);
84
+ }
85
+ }
86
+ }
87
+ };
88
+ handleMouseUp = () => {
89
+ if (this.createdRegion) {
90
+ this.addRegion(this.createdRegion);
91
+ this.createdRegion = null;
92
+ }
93
+ this.modifiedRegion = null;
94
+ this.isMoving = false;
95
+ this.dragStart = NaN;
96
+ this.wrapper.style.pointerEvents = '';
97
+ };
98
+ createRegionElement(start, end, title = '') {
99
+ const noWidth = start === end;
100
+ const div = el('div', {
101
+ position: 'absolute',
102
+ left: `${start * 100}%`,
103
+ width: `${(end - start) * 100}%`,
104
+ height: '100%',
105
+ backgroundColor: noWidth ? '' : 'rgba(0, 0, 0, 0.1)',
106
+ borderRadius: '2px',
107
+ boxSizing: 'border-box',
108
+ borderLeft: noWidth ? '2px solid rgba(0, 0, 0, 0.5)' : '',
109
+ transition: 'background-color 0.2s ease',
110
+ cursor: this.options.draggable ? 'move' : '',
111
+ pointerEvents: 'all',
112
+ whiteSpace: noWidth ? 'nowrap' : '',
113
+ padding: '0.2em',
114
+ });
115
+ div.textContent = title;
116
+ const leftHandle = el('div', {
117
+ position: 'absolute',
118
+ left: '0',
119
+ top: '0',
120
+ width: '6px',
121
+ height: '100%',
122
+ cursor: this.options.resizable ? 'ew-resize' : '',
123
+ pointerEvents: 'all',
124
+ borderLeft: noWidth ? '' : '2px solid rgba(0, 0, 0, 0.5)',
125
+ borderRadius: '2px 0 0 2px',
126
+ });
127
+ div.appendChild(leftHandle);
128
+ const rightHandle = leftHandle.cloneNode();
129
+ style(rightHandle, {
130
+ left: '',
131
+ right: '0',
132
+ borderLeft: '',
133
+ borderRight: noWidth ? '' : '2px solid rgba(0, 0, 0, 0.5)',
134
+ borderRadius: '0 2px 2px 0',
135
+ });
136
+ div.appendChild(rightHandle);
137
+ leftHandle.addEventListener('mousedown', (e) => {
138
+ if (!this.options.resizable)
139
+ return;
140
+ e.stopPropagation();
141
+ this.modifiedRegion = this.regions.find((r) => r.element === div) || null;
142
+ this.isResizingLeft = true;
143
+ this.isMoving = false;
144
+ });
145
+ rightHandle.addEventListener('mousedown', (e) => {
146
+ if (!this.options.resizable)
147
+ return;
148
+ e.stopPropagation();
149
+ this.modifiedRegion = this.regions.find((r) => r.element === div) || null;
150
+ this.isResizingLeft = false;
151
+ this.isMoving = false;
152
+ });
153
+ div.addEventListener('mousedown', () => {
154
+ if (!this.options.draggable)
155
+ return;
156
+ this.modifiedRegion = this.regions.find((r) => r.element === div) || null;
157
+ this.isMoving = true;
158
+ });
159
+ div.addEventListener('click', () => {
160
+ const region = this.regions.find((r) => r.element === div);
161
+ if (region) {
162
+ this.emit('region-clicked', { region });
163
+ }
164
+ });
165
+ this.regionsContainer.appendChild(div);
166
+ // Check that the label doesn't overlap with other labels
167
+ // If it does, push it down
168
+ const labelLeft = div.getBoundingClientRect().left;
169
+ const labelWidth = div.scrollWidth;
170
+ const overlap = this.regions
171
+ .filter((reg) => {
172
+ const { left } = reg.element.getBoundingClientRect();
173
+ const width = reg.element.scrollWidth;
174
+ return labelLeft < left + width && left < labelLeft + labelWidth;
175
+ })
176
+ .map((reg) => parseFloat(reg.element.style.paddingTop))
177
+ .reduce((sum, val) => sum + val, 0);
178
+ if (overlap > 0) {
179
+ div.style.paddingTop = `${overlap + 1}em`;
180
+ }
181
+ return div;
182
+ }
183
+ createRegion(start, end, title = '') {
184
+ const duration = this.wavesurfer.getDuration();
185
+ return {
186
+ element: this.createRegionElement(start, end, title),
187
+ start,
188
+ end,
189
+ startTime: start * duration,
190
+ endTime: end * duration,
191
+ title,
192
+ };
193
+ }
194
+ addRegion(region) {
195
+ this.regions.push(region);
196
+ this.emit('region-created', { region });
197
+ }
198
+ updateRegion(region, start, end) {
199
+ if (start != null) {
200
+ region.start = start;
201
+ region.element.style.left = `${region.start * 100}%`;
202
+ region.element.style.width = `${(region.end - region.start) * 100}%`;
203
+ region.startTime = start * this.wavesurfer.getDuration();
204
+ }
205
+ if (end != null) {
206
+ region.end = end;
207
+ region.element.style.width = `${(region.end - region.start) * 100}%`;
208
+ region.endTime = end * this.wavesurfer.getDuration();
209
+ }
210
+ this.emit('region-updated', { region });
211
+ }
212
+ moveRegion(region, delta) {
213
+ this.updateRegion(region, region.start + delta, region.end + delta);
214
+ }
215
+ /** Create a region at a given start and end time, with an optional title */
216
+ add(startTime, endTime, title = '', color = '') {
217
+ const duration = this.wavesurfer.getDuration();
218
+ const start = startTime / duration;
219
+ const end = endTime / duration;
220
+ const region = this.createRegion(start, end, title);
221
+ this.addRegion(region);
222
+ if (color)
223
+ this.setRegionColor(region, color);
224
+ return region;
225
+ }
226
+ /** Set the background color of a region */
227
+ setRegionColor(region, color) {
228
+ region.element.style[region.startTime === region.endTime ? 'borderColor' : 'backgroundColor'] = color;
229
+ }
230
+ }
231
+ export default RegionsPlugin;
@@ -0,0 +1,36 @@
1
+ import BasePlugin from '../base-plugin.js';
2
+ import type { WaveSurferPluginParams } from '../index.js';
3
+ export type TimelinePluginOptions = {
4
+ /** The height of the timeline in pixels, defaults to 20 */
5
+ height?: number;
6
+ /** HTML container for the timeline, defaults to wavesufer's container */
7
+ container?: HTMLElement;
8
+ /** The duration of the timeline in seconds, defaults to wavesurfer's duration */
9
+ duration?: number;
10
+ /** Interval between ticks in seconds */
11
+ timeInterval?: number;
12
+ /** Interval between numeric labels */
13
+ primaryLabelInterval?: number;
14
+ /** Interval between secondary numeric labels */
15
+ secondaryLabelInterval?: number;
16
+ };
17
+ declare const defaultOptions: {
18
+ height: number;
19
+ };
20
+ export type TimelinePluginEvents = {
21
+ ready: void;
22
+ };
23
+ declare class TimelinePlugin extends BasePlugin<TimelinePluginEvents, TimelinePluginOptions> {
24
+ private timelineWrapper;
25
+ protected options: TimelinePluginOptions & typeof defaultOptions;
26
+ constructor(params: WaveSurferPluginParams, options: TimelinePluginOptions);
27
+ /** Unmount */
28
+ destroy(): void;
29
+ private initTimelineWrapper;
30
+ private formatTime;
31
+ private defaultTimeInterval;
32
+ defaultPrimaryLabelInterval(pxPerSec: number): number;
33
+ defaultSecondaryLabelInterval(pxPerSec: number): number;
34
+ private initTimeline;
35
+ }
36
+ export default TimelinePlugin;
@@ -0,0 +1,125 @@
1
+ import BasePlugin from '../base-plugin.js';
2
+ const defaultOptions = {
3
+ height: 20,
4
+ };
5
+ class TimelinePlugin extends BasePlugin {
6
+ timelineWrapper;
7
+ options;
8
+ constructor(params, options) {
9
+ super(params, options);
10
+ this.options = Object.assign({}, defaultOptions, options);
11
+ this.container = this.options.container ?? this.wrapper;
12
+ this.timelineWrapper = this.initTimelineWrapper();
13
+ this.container.appendChild(this.timelineWrapper);
14
+ if (this.options.duration) {
15
+ this.initTimeline(this.options.duration);
16
+ }
17
+ else {
18
+ this.subscriptions.push(this.wavesurfer.on('decode', ({ duration }) => {
19
+ this.initTimeline(duration);
20
+ }));
21
+ }
22
+ }
23
+ /** Unmount */
24
+ destroy() {
25
+ this.timelineWrapper.remove();
26
+ super.destroy();
27
+ }
28
+ initTimelineWrapper() {
29
+ return document.createElement('div');
30
+ }
31
+ formatTime(seconds) {
32
+ if (seconds / 60 > 1) {
33
+ // calculate minutes and seconds from seconds count
34
+ const minutes = Math.round(seconds / 60);
35
+ seconds = Math.round(seconds % 60);
36
+ const paddedSeconds = `${seconds < 10 ? '0' : ''}${seconds}`;
37
+ return `${minutes}:${paddedSeconds}`;
38
+ }
39
+ const rounded = Math.round(seconds * 1000) / 1000;
40
+ return `${rounded}`;
41
+ }
42
+ // Return how many seconds should be between each notch
43
+ defaultTimeInterval(pxPerSec) {
44
+ if (pxPerSec >= 25) {
45
+ return 1;
46
+ }
47
+ else if (pxPerSec * 5 >= 25) {
48
+ return 5;
49
+ }
50
+ else if (pxPerSec * 15 >= 25) {
51
+ return 15;
52
+ }
53
+ return Math.ceil(0.5 / pxPerSec) * 60;
54
+ }
55
+ // Return the cadence of notches that get labels in the primary color.
56
+ defaultPrimaryLabelInterval(pxPerSec) {
57
+ if (pxPerSec >= 25) {
58
+ return 10;
59
+ }
60
+ else if (pxPerSec * 5 >= 25) {
61
+ return 6;
62
+ }
63
+ else if (pxPerSec * 15 >= 25) {
64
+ return 4;
65
+ }
66
+ return 4;
67
+ }
68
+ // Return the cadence of notches that get labels in the secondary color.
69
+ defaultSecondaryLabelInterval(pxPerSec) {
70
+ if (pxPerSec >= 25) {
71
+ return 5;
72
+ }
73
+ else if (pxPerSec * 5 >= 25) {
74
+ return 2;
75
+ }
76
+ else if (pxPerSec * 15 >= 25) {
77
+ return 2;
78
+ }
79
+ return 2;
80
+ }
81
+ initTimeline(duration) {
82
+ const width = Math.round(this.timelineWrapper.scrollWidth * devicePixelRatio);
83
+ const pxPerSec = width / duration;
84
+ const timeInterval = this.options.timeInterval ?? this.defaultTimeInterval(pxPerSec);
85
+ const primaryLabelInterval = this.options.primaryLabelInterval ?? this.defaultPrimaryLabelInterval(pxPerSec);
86
+ const secondaryLabelInterval = this.options.secondaryLabelInterval ?? this.defaultSecondaryLabelInterval(pxPerSec);
87
+ const timeline = document.createElement('div');
88
+ timeline.setAttribute('style', `
89
+ height: ${this.options.height}px;
90
+ overflow: hidden;
91
+ display: flex;
92
+ justify-content: space-between;
93
+ align-items: flex-end;
94
+ font-size: ${this.options.height / 2}px;
95
+ white-space: nowrap;
96
+ `);
97
+ const notchEl = document.createElement('div');
98
+ notchEl.setAttribute('style', `
99
+ width: 1px;
100
+ height: 50%;
101
+ display: flex;
102
+ flex-direction: column;
103
+ justify-content: flex-end;
104
+ overflow: visible;
105
+ border-left: 1px solid currentColor;
106
+ opacity: 0.25;
107
+ `);
108
+ for (let i = 0; i < duration; i += timeInterval) {
109
+ const notch = notchEl.cloneNode();
110
+ const isPrimary = i % primaryLabelInterval === 0;
111
+ const isSecondary = i % secondaryLabelInterval === 0;
112
+ if (isPrimary || isSecondary) {
113
+ notch.style.height = '100%';
114
+ notch.style.textIndent = '3px';
115
+ notch.textContent = this.formatTime(i);
116
+ if (isPrimary)
117
+ notch.style.opacity = '1';
118
+ }
119
+ timeline.appendChild(notch);
120
+ }
121
+ this.timelineWrapper.appendChild(timeline);
122
+ this.emit('ready');
123
+ }
124
+ }
125
+ export default TimelinePlugin;
@@ -4,10 +4,14 @@ type RendererOptions = {
4
4
  height: number;
5
5
  waveColor: string;
6
6
  progressColor: string;
7
+ cursorColor?: string;
8
+ cursorWidth: number;
7
9
  minPxPerSec: number;
10
+ fillParent: boolean;
8
11
  barWidth?: number;
9
12
  barGap?: number;
10
13
  barRadius?: number;
14
+ noScrollbar?: boolean;
11
15
  };
12
16
  type RendererEvents = {
13
17
  click: {
@@ -17,18 +21,20 @@ type RendererEvents = {
17
21
  declare class Renderer extends EventEmitter<RendererEvents> {
18
22
  private options;
19
23
  private container;
20
- private shadowRoot;
21
- private mainCanvas;
22
- private progressCanvas;
23
- private cursor;
24
- private ctx;
24
+ private scrollContainer;
25
+ private wrapper;
26
+ private canvasWrapper;
27
+ private progressWrapper;
28
+ private timeout;
29
+ private isScrolling;
25
30
  constructor(options: RendererOptions);
31
+ getContainer(): HTMLElement;
32
+ getWrapper(): HTMLElement;
26
33
  destroy(): void;
27
- private renderLinePeaks;
28
- private renderBarPeaks;
29
- render(channelData: Float32Array[], duration: number, minPxPerSec?: number): void;
30
- private createProgressMask;
34
+ private delay;
35
+ private renderPeaks;
36
+ render(audioData: AudioBuffer): void;
37
+ zoom(audioData: AudioBuffer, minPxPerSec: number): void;
31
38
  renderProgress(progress: number, autoCenter?: boolean): void;
32
- getContainer(): HTMLElement;
33
39
  }
34
40
  export default Renderer;
@@ -0,0 +1,239 @@
1
+ import EventEmitter from './event-emitter.js';
2
+ class Renderer extends EventEmitter {
3
+ options;
4
+ container;
5
+ scrollContainer;
6
+ wrapper;
7
+ canvasWrapper;
8
+ progressWrapper;
9
+ timeout = null;
10
+ isScrolling = false;
11
+ constructor(options) {
12
+ super();
13
+ this.options = options;
14
+ let container = null;
15
+ if (typeof this.options.container === 'string') {
16
+ container = document.querySelector(this.options.container);
17
+ }
18
+ else if (this.options.container instanceof HTMLElement) {
19
+ container = this.options.container;
20
+ }
21
+ if (!container) {
22
+ throw new Error('Container not found');
23
+ }
24
+ const div = document.createElement('div');
25
+ const shadow = div.attachShadow({ mode: 'open' });
26
+ shadow.innerHTML = `
27
+ <style>
28
+ :host {
29
+ user-select: none;
30
+ }
31
+ :host .scroll {
32
+ overflow-x: auto;
33
+ overflow-y: hidden;
34
+ width: 100%;
35
+ position: relative;
36
+ ${this.options.noScrollbar ? 'scrollbar-color: transparent;' : ''}
37
+ }
38
+ :host ::-webkit-scrollbar {
39
+ display: ${this.options.noScrollbar ? 'none' : 'auto'};
40
+ }
41
+ :host .wrapper {
42
+ position: relative;
43
+ min-width: 100%;
44
+ z-index: 2;
45
+ }
46
+ :host .canvases {
47
+ position: relative;
48
+ height: ${this.options.height}px;
49
+ }
50
+ :host canvas {
51
+ display: block;
52
+ position: absolute;
53
+ top: 0;
54
+ height: ${this.options.height}px;
55
+ image-rendering: pixelated;
56
+ }
57
+ :host .progress {
58
+ pointer-events: none;
59
+ position: absolute;
60
+ z-index: 2;
61
+ top: 0;
62
+ left: 0;
63
+ width: 0;
64
+ height: ${this.options.height}px;
65
+ overflow: hidden;
66
+ box-sizing: border-box;
67
+ border-right-style: solid;
68
+ border-right-width: ${this.options.cursorWidth}px;
69
+ border-right-color: ${this.options.cursorColor || this.options.progressColor};
70
+ }
71
+ </style>
72
+
73
+ <div class="scroll">
74
+ <div class="wrapper">
75
+ <div class="canvases"></div>
76
+ <div class="progress"></div>
77
+ </div>
78
+ </div>
79
+ `;
80
+ this.container = div;
81
+ this.scrollContainer = shadow.querySelector('.scroll');
82
+ this.wrapper = shadow.querySelector('.wrapper');
83
+ this.canvasWrapper = shadow.querySelector('.canvases');
84
+ this.progressWrapper = shadow.querySelector('.progress');
85
+ container.appendChild(div);
86
+ this.wrapper.addEventListener('click', (e) => {
87
+ const rect = this.wrapper.getBoundingClientRect();
88
+ const x = e.clientX - rect.left;
89
+ const relativeX = x / rect.width;
90
+ this.emit('click', { relativeX });
91
+ });
92
+ }
93
+ getContainer() {
94
+ return this.scrollContainer;
95
+ }
96
+ getWrapper() {
97
+ return this.wrapper;
98
+ }
99
+ destroy() {
100
+ this.container.remove();
101
+ }
102
+ delay(fn, delayMs = 10) {
103
+ if (this.timeout) {
104
+ clearTimeout(this.timeout);
105
+ }
106
+ return new Promise((resolve) => {
107
+ this.timeout = setTimeout(() => {
108
+ resolve(fn());
109
+ }, delayMs);
110
+ });
111
+ }
112
+ async renderPeaks(channelData, width, height, pixelRatio) {
113
+ const barWidth = this.options.barWidth != null ? this.options.barWidth * pixelRatio : 1;
114
+ const barGap = this.options.barGap != null ? this.options.barGap * pixelRatio : this.options.barWidth ? barWidth / 2 : 0;
115
+ const barRadius = this.options.barRadius ?? 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
+ const canvas = document.createElement('canvas');
129
+ canvas.width = Math.round((width * (end - start)) / len);
130
+ canvas.height = this.options.height;
131
+ canvas.style.width = `${Math.floor(canvas.width / pixelRatio)}px`;
132
+ canvas.style.left = `${Math.floor((start * width) / pixelRatio / len)}px`;
133
+ this.canvasWrapper.appendChild(canvas);
134
+ const ctx = canvas.getContext('2d', {
135
+ desynchronized: true,
136
+ });
137
+ ctx.beginPath();
138
+ ctx.fillStyle = this.options.waveColor;
139
+ // Firefox shim until 2023.04.11
140
+ if (!ctx.roundRect)
141
+ ctx.roundRect = ctx.fillRect;
142
+ for (let i = start; i < end; i++) {
143
+ const barIndex = Math.round((i - start) * barIndexScale);
144
+ if (barIndex > prevX) {
145
+ const leftBarHeight = Math.round(prevLeft * halfHeight);
146
+ const rightBarHeight = Math.round(prevRight * halfHeight);
147
+ ctx.roundRect(prevX * (barWidth + barGap), halfHeight - leftBarHeight, barWidth, leftBarHeight + rightBarHeight || 1, barRadius);
148
+ prevX = barIndex;
149
+ prevLeft = 0;
150
+ prevRight = 0;
151
+ }
152
+ const leftValue = useNegative ? leftChannel[i] : Math.abs(leftChannel[i]);
153
+ const rightValue = useNegative ? rightChannel[i] : Math.abs(rightChannel[i]);
154
+ if (leftValue > prevLeft) {
155
+ prevLeft = leftValue;
156
+ }
157
+ // If stereo, both channels are drawn as max values
158
+ // If mono with negative values, the bottom channel will be the min negative values
159
+ if (useNegative ? rightValue < -prevRight : rightValue > prevRight) {
160
+ prevRight = rightValue < 0 ? -rightValue : rightValue;
161
+ }
162
+ }
163
+ ctx.fill();
164
+ ctx.closePath();
165
+ // Draw a progress canvas
166
+ const progressCanvas = canvas.cloneNode();
167
+ this.progressWrapper.appendChild(progressCanvas);
168
+ const progressCtx = progressCanvas.getContext('2d', {
169
+ desynchronized: true,
170
+ });
171
+ progressCtx.drawImage(canvas, 0, 0);
172
+ // Set the composition method to draw only where the waveform is drawn
173
+ progressCtx.globalCompositeOperation = 'source-in';
174
+ progressCtx.fillStyle = this.options.progressColor;
175
+ // This rectangle acts as a mask thanks to the composition method
176
+ progressCtx.fillRect(0, 0, canvas.width, canvas.height);
177
+ };
178
+ // Clear the canvas
179
+ this.canvasWrapper.innerHTML = '';
180
+ this.progressWrapper.innerHTML = '';
181
+ // Draw the currently visible part of the waveform
182
+ const { scrollLeft, scrollWidth, clientWidth } = this.scrollContainer;
183
+ const scale = len / scrollWidth;
184
+ const start = Math.floor(scrollLeft * scale);
185
+ const end = Math.ceil(Math.min(scrollWidth, scrollLeft + clientWidth) * scale);
186
+ draw(start, end);
187
+ // Draw the rest of the waveform with a timeout for better performance
188
+ const step = end - start;
189
+ for (let i = end; i < len; i += step) {
190
+ await this.delay(() => {
191
+ draw(i, Math.min(len, i + step));
192
+ });
193
+ }
194
+ for (let i = start - 1; i >= 0; i -= step) {
195
+ await this.delay(() => {
196
+ draw(Math.max(0, i - step), i);
197
+ });
198
+ }
199
+ }
200
+ render(audioData) {
201
+ // Determine the width of the canvas
202
+ const pixelRatio = window.devicePixelRatio || 1;
203
+ const parentWidth = this.options.fillParent ? this.scrollContainer.clientWidth * pixelRatio : 0;
204
+ const scrollWidth = audioData.duration * this.options.minPxPerSec;
205
+ this.isScrolling = scrollWidth > parentWidth;
206
+ const width = Math.max(1, this.isScrolling ? scrollWidth : parentWidth);
207
+ const { height } = this.options;
208
+ // Remember the current cursor position
209
+ const oldCursorPosition = this.progressWrapper.clientWidth;
210
+ this.scrollContainer.style.overflowX = this.isScrolling ? 'auto' : 'hidden';
211
+ this.wrapper.style.width = `${Math.floor(width / pixelRatio)}px`;
212
+ // Adjust the scroll position so that the cursor stays in the same place
213
+ const newCursortPosition = this.progressWrapper.clientWidth;
214
+ this.scrollContainer.scrollLeft += newCursortPosition - oldCursorPosition;
215
+ // First two channels are used
216
+ const channelData = [audioData.getChannelData(0)];
217
+ if (audioData.numberOfChannels > 1) {
218
+ channelData.push(audioData.getChannelData(1));
219
+ }
220
+ this.renderPeaks(channelData, width, height, pixelRatio);
221
+ }
222
+ zoom(audioData, minPxPerSec) {
223
+ this.options.minPxPerSec = minPxPerSec;
224
+ this.render(audioData);
225
+ }
226
+ renderProgress(progress, autoCenter = false) {
227
+ this.progressWrapper.style.width = `${progress * 100}%`;
228
+ if (this.isScrolling) {
229
+ const containerWidth = this.scrollContainer.clientWidth;
230
+ const center = containerWidth / 2;
231
+ const progressWidth = this.progressWrapper.clientWidth;
232
+ const minScroll = autoCenter ? center : containerWidth;
233
+ if (progressWidth > this.scrollContainer.scrollLeft + minScroll) {
234
+ this.scrollContainer.scrollLeft = progressWidth - center;
235
+ }
236
+ }
237
+ }
238
+ }
239
+ export default Renderer;
package/dist/timer.d.ts CHANGED
@@ -4,7 +4,8 @@ type TimerEvents = {
4
4
  };
5
5
  declare class Timer extends EventEmitter<TimerEvents> {
6
6
  private unsubscribe;
7
- constructor();
7
+ start(): void;
8
+ stop(): void;
8
9
  destroy(): void;
9
10
  }
10
11
  export default Timer;