@arraypress/waveform-player 1.7.2 → 1.8.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.
- package/README.md +176 -377
- package/dist/waveform-player.cjs +2208 -0
- package/dist/waveform-player.cjs.map +7 -0
- package/dist/waveform-player.css +1 -1
- package/dist/waveform-player.esm.js +8 -8
- package/dist/waveform-player.esm.js.map +7 -0
- package/dist/waveform-player.js +553 -275
- package/dist/waveform-player.min.js +8 -8
- package/dist/waveform-player.min.js.map +7 -0
- package/index.d.ts +344 -0
- package/package.json +18 -8
- package/src/css/waveform-player.css +21 -3
- package/src/js/audio.js +61 -25
- package/src/js/bpm.js +26 -5
- package/src/js/core.js +417 -185
- package/src/js/drawing.js +208 -44
- package/src/js/index.js +56 -11
- package/src/js/themes.js +88 -47
- package/src/js/utils.js +231 -65
package/src/js/drawing.js
CHANGED
|
@@ -3,10 +3,113 @@
|
|
|
3
3
|
* @description Core waveform drawing styles optimized for visual distinction at all sizes
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import {resampleData} from './utils.js';
|
|
6
|
+
import {resampleData, clamp} from './utils.js';
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
|
-
*
|
|
9
|
+
* Resolve a fill value that may be a CSS colour string OR an array of colour
|
|
10
|
+
* stops (rendered as a vertical canvas gradient). Bundle-light gradient
|
|
11
|
+
* support: pass e.g. `waveformColor: ['#fafafa', '#71717a']`.
|
|
12
|
+
* A single-element array collapses to that one colour; a multi-element array
|
|
13
|
+
* is spread evenly from top (y=0) to bottom (y=height).
|
|
14
|
+
* @private
|
|
15
|
+
* @param {CanvasRenderingContext2D} ctx - Canvas context used to build the gradient.
|
|
16
|
+
* @param {string|string[]} value - A CSS colour string, or an array of colour stops.
|
|
17
|
+
* @param {number} height - Canvas height in device pixels (gradient span).
|
|
18
|
+
* @returns {string|CanvasGradient} The original string, or a vertical linear gradient.
|
|
19
|
+
*/
|
|
20
|
+
function makeFill(ctx, value, height) {
|
|
21
|
+
if (!Array.isArray(value)) return value;
|
|
22
|
+
if (value.length === 1) return value[0];
|
|
23
|
+
const grad = ctx.createLinearGradient(0, 0, 0, height);
|
|
24
|
+
value.forEach((c, i) => grad.addColorStop(i / (value.length - 1), c));
|
|
25
|
+
return grad;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Fill a bar rect, optionally with rounded caps (`barRadius`). Falls back to
|
|
30
|
+
* a plain fillRect where `roundRect` is unavailable (older Safari) — square
|
|
31
|
+
* bars, no error. Radii are clamped to half the rect's width/height so a
|
|
32
|
+
* large `barRadius` never overflows a thin or short bar.
|
|
33
|
+
* @private
|
|
34
|
+
* @param {CanvasRenderingContext2D} ctx - Canvas context (current fillStyle is used).
|
|
35
|
+
* @param {number} x - Left edge of the bar in device pixels.
|
|
36
|
+
* @param {number} y - Top edge of the bar in device pixels.
|
|
37
|
+
* @param {number} w - Bar width in device pixels.
|
|
38
|
+
* @param {number} h - Bar height in device pixels (may be negative for upward fills).
|
|
39
|
+
* @param {number|number[]} radii - Corner radius (number, or [tl, tr, br, bl]).
|
|
40
|
+
* @returns {void}
|
|
41
|
+
*/
|
|
42
|
+
function fillBar(ctx, x, y, w, h, radii) {
|
|
43
|
+
const any = Array.isArray(radii) ? radii.some(r => r > 0) : radii > 0;
|
|
44
|
+
if (any && typeof ctx.roundRect === 'function') {
|
|
45
|
+
const max = Math.min(w / 2, Math.abs(h) / 2);
|
|
46
|
+
const clampR = (r) => clamp(r, 0, max);
|
|
47
|
+
ctx.beginPath();
|
|
48
|
+
ctx.roundRect(x, y, w, h, Array.isArray(radii) ? radii.map(clampR) : clampR(radii));
|
|
49
|
+
ctx.fill();
|
|
50
|
+
} else {
|
|
51
|
+
ctx.fillRect(x, y, w, h);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Scale the configured `barRadius` into device pixels (scalar).
|
|
57
|
+
* @private
|
|
58
|
+
* @param {Object} options - Drawing options (`barRadius` in CSS pixels, defaults to 0).
|
|
59
|
+
* @param {number} dpr - Device pixel ratio multiplier.
|
|
60
|
+
* @returns {number} The bar corner radius in device pixels.
|
|
61
|
+
*/
|
|
62
|
+
function barRadiusPx(options, dpr) {
|
|
63
|
+
return (options.barRadius || 0) * dpr;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Top-rounded corner radii for bottom-anchored bars: [tl, tr, br, bl].
|
|
68
|
+
* Only the top two corners are rounded so bars sit flush on the baseline.
|
|
69
|
+
* @private
|
|
70
|
+
* @param {Object} options - Drawing options (supplies `barRadius`).
|
|
71
|
+
* @param {number} dpr - Device pixel ratio multiplier.
|
|
72
|
+
* @returns {number[]} Corner radii in device pixels as [tl, tr, br, bl].
|
|
73
|
+
*/
|
|
74
|
+
function barRadii(options, dpr) {
|
|
75
|
+
const r = barRadiusPx(options, dpr);
|
|
76
|
+
return [r, r, 0, 0];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Trace a horizontal rounded-capsule (stadium) path from `startX` to `endX`,
|
|
81
|
+
* ready to fill. The end caps are semicircles of radius `barHeight / 2`.
|
|
82
|
+
* @private
|
|
83
|
+
* @param {CanvasRenderingContext2D} ctx - Canvas context.
|
|
84
|
+
* @param {number} startX - Left edge x (also the left cap centre).
|
|
85
|
+
* @param {number} endX - Right edge x.
|
|
86
|
+
* @param {number} centerY - Vertical centre of the capsule.
|
|
87
|
+
* @param {number} barHeight - Capsule thickness in pixels.
|
|
88
|
+
* @returns {void}
|
|
89
|
+
*/
|
|
90
|
+
function capsulePath(ctx, startX, endX, centerY, barHeight) {
|
|
91
|
+
const r = barHeight / 2;
|
|
92
|
+
ctx.beginPath();
|
|
93
|
+
ctx.moveTo(startX, centerY - r);
|
|
94
|
+
ctx.lineTo(endX - r, centerY - r);
|
|
95
|
+
ctx.arc(endX - r, centerY, r, -Math.PI / 2, Math.PI / 2);
|
|
96
|
+
ctx.lineTo(startX, centerY + r);
|
|
97
|
+
ctx.arc(startX, centerY, r, Math.PI / 2, -Math.PI / 2);
|
|
98
|
+
ctx.closePath();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Draw standard bars waveform - classic vertical bars anchored to the baseline.
|
|
103
|
+
* Peaks are resampled to fit the available bar slots, drawn at 90% of canvas
|
|
104
|
+
* height, then the progress portion is repainted in `progressColor` via a
|
|
105
|
+
* left-anchored clip rect.
|
|
106
|
+
* @param {CanvasRenderingContext2D} ctx - Canvas 2D context to draw into.
|
|
107
|
+
* @param {HTMLCanvasElement} canvas - Canvas element (provides device-pixel dimensions).
|
|
108
|
+
* @param {number[]} peaks - Normalised waveform peak values (0-1).
|
|
109
|
+
* @param {number} progress - Playback progress (0-1) that drives the colour overlay.
|
|
110
|
+
* @param {Object} options - Drawing options: `barWidth`, `barSpacing`, `barRadius`,
|
|
111
|
+
* `color`, `progressColor` (colour strings or gradient stop arrays).
|
|
112
|
+
* @returns {void}
|
|
10
113
|
*/
|
|
11
114
|
export function drawBars(ctx, canvas, peaks, progress, options) {
|
|
12
115
|
const dpr = window.devicePixelRatio || 1;
|
|
@@ -16,10 +119,14 @@ export function drawBars(ctx, canvas, peaks, progress, options) {
|
|
|
16
119
|
const resampledPeaks = resampleData(peaks, barCount);
|
|
17
120
|
const height = canvas.height;
|
|
18
121
|
const progressWidth = progress * canvas.width;
|
|
122
|
+
const radii = barRadii(options, dpr);
|
|
123
|
+
const baseFill = makeFill(ctx, options.color, height);
|
|
124
|
+
const progFill = makeFill(ctx, options.progressColor, height);
|
|
19
125
|
|
|
20
126
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
21
127
|
|
|
22
128
|
// Draw all bars first
|
|
129
|
+
ctx.fillStyle = baseFill;
|
|
23
130
|
for (let i = 0; i < resampledPeaks.length; i++) {
|
|
24
131
|
const x = i * (barWidth + barSpacing);
|
|
25
132
|
if (x + barWidth > canvas.width) break;
|
|
@@ -28,8 +135,7 @@ export function drawBars(ctx, canvas, peaks, progress, options) {
|
|
|
28
135
|
// Draw from bottom up, not centered
|
|
29
136
|
const y = height - peakHeight;
|
|
30
137
|
|
|
31
|
-
ctx
|
|
32
|
-
ctx.fillRect(x, y, barWidth, peakHeight);
|
|
138
|
+
fillBar(ctx, x, y, barWidth, peakHeight, radii);
|
|
33
139
|
}
|
|
34
140
|
|
|
35
141
|
// Progress overlay
|
|
@@ -38,6 +144,7 @@ export function drawBars(ctx, canvas, peaks, progress, options) {
|
|
|
38
144
|
ctx.rect(0, 0, progressWidth, height);
|
|
39
145
|
ctx.clip();
|
|
40
146
|
|
|
147
|
+
ctx.fillStyle = progFill;
|
|
41
148
|
for (let i = 0; i < resampledPeaks.length; i++) {
|
|
42
149
|
const x = i * (barWidth + barSpacing);
|
|
43
150
|
if (x > progressWidth) break;
|
|
@@ -46,18 +153,24 @@ export function drawBars(ctx, canvas, peaks, progress, options) {
|
|
|
46
153
|
// Draw from bottom up, not centered
|
|
47
154
|
const y = height - peakHeight;
|
|
48
155
|
|
|
49
|
-
ctx
|
|
50
|
-
ctx.fillRect(x, y, barWidth, peakHeight);
|
|
156
|
+
fillBar(ctx, x, y, barWidth, peakHeight, radii);
|
|
51
157
|
}
|
|
52
158
|
|
|
53
159
|
ctx.restore();
|
|
54
160
|
}
|
|
55
161
|
|
|
56
162
|
/**
|
|
57
|
-
* Draw mirror/SoundCloud style waveform -
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
*
|
|
163
|
+
* Draw mirror/SoundCloud style waveform - symmetrical bars about the centre line.
|
|
164
|
+
* Each peak is drawn twice (45% of height up and down) with the upper cap rounded
|
|
165
|
+
* on top and the lower cap rounded on the bottom; the progress portion is then
|
|
166
|
+
* repainted in `progressColor` through a left-anchored clip rect.
|
|
167
|
+
* @param {CanvasRenderingContext2D} ctx - Canvas 2D context to draw into.
|
|
168
|
+
* @param {HTMLCanvasElement} canvas - Canvas element (provides device-pixel dimensions).
|
|
169
|
+
* @param {number[]} peaks - Normalised waveform peak values (0-1).
|
|
170
|
+
* @param {number} progress - Playback progress (0-1) that drives the colour overlay.
|
|
171
|
+
* @param {Object} options - Drawing options: `barWidth`, `barSpacing`, `barRadius`,
|
|
172
|
+
* `color`, `progressColor`.
|
|
173
|
+
* @returns {void}
|
|
61
174
|
*/
|
|
62
175
|
export function drawMirror(ctx, canvas, peaks, progress, options) {
|
|
63
176
|
const dpr = window.devicePixelRatio || 1;
|
|
@@ -68,19 +181,24 @@ export function drawMirror(ctx, canvas, peaks, progress, options) {
|
|
|
68
181
|
const height = canvas.height;
|
|
69
182
|
const centerY = height / 2;
|
|
70
183
|
const progressWidth = progress * canvas.width;
|
|
184
|
+
const r = barRadiusPx(options, dpr);
|
|
185
|
+
const topRadii = [r, r, 0, 0]; // round the upper cap
|
|
186
|
+
const botRadii = [0, 0, r, r]; // round the lower cap
|
|
187
|
+
const baseFill = makeFill(ctx, options.color, height);
|
|
188
|
+
const progFill = makeFill(ctx, options.progressColor, height);
|
|
71
189
|
|
|
72
190
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
73
191
|
|
|
74
192
|
// Draw all bars
|
|
193
|
+
ctx.fillStyle = baseFill;
|
|
75
194
|
for (let i = 0; i < resampledPeaks.length; i++) {
|
|
76
195
|
const x = i * (barWidth + barSpacing);
|
|
77
196
|
if (x + barWidth > canvas.width) break;
|
|
78
197
|
|
|
79
198
|
const peakHeight = resampledPeaks[i] * height * 0.45;
|
|
80
199
|
|
|
81
|
-
ctx
|
|
82
|
-
ctx
|
|
83
|
-
ctx.fillRect(x, centerY, barWidth, peakHeight);
|
|
200
|
+
fillBar(ctx, x, centerY - peakHeight, barWidth, peakHeight, topRadii);
|
|
201
|
+
fillBar(ctx, x, centerY, barWidth, peakHeight, botRadii);
|
|
84
202
|
}
|
|
85
203
|
|
|
86
204
|
// Progress overlay
|
|
@@ -89,22 +207,32 @@ export function drawMirror(ctx, canvas, peaks, progress, options) {
|
|
|
89
207
|
ctx.rect(0, 0, progressWidth, height);
|
|
90
208
|
ctx.clip();
|
|
91
209
|
|
|
210
|
+
ctx.fillStyle = progFill;
|
|
92
211
|
for (let i = 0; i < resampledPeaks.length; i++) {
|
|
93
212
|
const x = i * (barWidth + barSpacing);
|
|
94
213
|
if (x > progressWidth) break;
|
|
95
214
|
|
|
96
215
|
const peakHeight = resampledPeaks[i] * height * 0.45;
|
|
97
216
|
|
|
98
|
-
ctx
|
|
99
|
-
ctx
|
|
100
|
-
ctx.fillRect(x, centerY, barWidth, peakHeight);
|
|
217
|
+
fillBar(ctx, x, centerY - peakHeight, barWidth, peakHeight, topRadii);
|
|
218
|
+
fillBar(ctx, x, centerY, barWidth, peakHeight, botRadii);
|
|
101
219
|
}
|
|
102
220
|
|
|
103
221
|
ctx.restore();
|
|
104
222
|
}
|
|
105
223
|
|
|
106
224
|
/**
|
|
107
|
-
* Draw line/oscilloscope style waveform -
|
|
225
|
+
* Draw line/oscilloscope style waveform - smooth flowing wave with glow.
|
|
226
|
+
* Renders a faint oscilloscope grid (centre line + 10 vertical divisions), the
|
|
227
|
+
* full waveform as a bezier-smoothed curve, then the played portion on top with
|
|
228
|
+
* a coloured shadow glow. Peaks are modulated by a sine term so the line undulates
|
|
229
|
+
* rather than reading as static bars.
|
|
230
|
+
* @param {CanvasRenderingContext2D} ctx - Canvas 2D context to draw into.
|
|
231
|
+
* @param {HTMLCanvasElement} canvas - Canvas element (provides device-pixel dimensions).
|
|
232
|
+
* @param {number[]} peaks - Normalised waveform peak values (0-1).
|
|
233
|
+
* @param {number} progress - Playback progress (0-1); the glowing curve is only drawn when > 0.
|
|
234
|
+
* @param {Object} options - Drawing options: `color` (base wave), `progressColor` (played wave).
|
|
235
|
+
* @returns {void}
|
|
108
236
|
*/
|
|
109
237
|
export function drawLine(ctx, canvas, peaks, progress, options) {
|
|
110
238
|
const width = canvas.width;
|
|
@@ -114,7 +242,15 @@ export function drawLine(ctx, canvas, peaks, progress, options) {
|
|
|
114
242
|
|
|
115
243
|
ctx.clearRect(0, 0, width, height);
|
|
116
244
|
|
|
117
|
-
|
|
245
|
+
/**
|
|
246
|
+
* Stroke a bezier-smoothed curve through the (optionally sine-modulated) peaks.
|
|
247
|
+
* @private
|
|
248
|
+
* @param {string} color - Stroke colour (and shadow colour when glowing).
|
|
249
|
+
* @param {number} lineWidth - Stroke width in pixels.
|
|
250
|
+
* @param {number} [endProgress=1] - Fraction (0-1) of the peaks to draw, left to right.
|
|
251
|
+
* @param {boolean} [addGlow=false] - When true, applies a coloured shadow blur for a glow effect.
|
|
252
|
+
* @returns {void}
|
|
253
|
+
*/
|
|
118
254
|
const drawCurve = (color, lineWidth, endProgress = 1, addGlow = false) => {
|
|
119
255
|
if (addGlow) {
|
|
120
256
|
ctx.shadowBlur = 12;
|
|
@@ -190,7 +326,18 @@ export function drawLine(ctx, canvas, peaks, progress, options) {
|
|
|
190
326
|
}
|
|
191
327
|
|
|
192
328
|
/**
|
|
193
|
-
* Draw blocks/LED meter style waveform -
|
|
329
|
+
* Draw blocks/LED meter style waveform - segmented blocks growing from the centre.
|
|
330
|
+
* Each bar's height is quantised into fixed-size blocks separated by gaps, drawn
|
|
331
|
+
* symmetrically up and down from the centre line (the shared centre block is not
|
|
332
|
+
* duplicated downward). Per-bar colour is chosen by comparing the bar's x against
|
|
333
|
+
* the played width — there is no clip overlay here.
|
|
334
|
+
* @param {CanvasRenderingContext2D} ctx - Canvas 2D context to draw into.
|
|
335
|
+
* @param {HTMLCanvasElement} canvas - Canvas element (provides device-pixel dimensions).
|
|
336
|
+
* @param {number[]} peaks - Normalised waveform peak values (0-1).
|
|
337
|
+
* @param {number} progress - Playback progress (0-1) used to pick each bar's colour.
|
|
338
|
+
* @param {Object} options - Drawing options: `barWidth` (default 3), `barSpacing` (default 1),
|
|
339
|
+
* `color`, `progressColor`.
|
|
340
|
+
* @returns {void}
|
|
194
341
|
*/
|
|
195
342
|
export function drawBlocks(ctx, canvas, peaks, progress, options) {
|
|
196
343
|
const dpr = window.devicePixelRatio || 1;
|
|
@@ -203,6 +350,8 @@ export function drawBlocks(ctx, canvas, peaks, progress, options) {
|
|
|
203
350
|
const blockGap = 2 * dpr;
|
|
204
351
|
const progressWidth = progress * canvas.width;
|
|
205
352
|
const centerY = height / 2;
|
|
353
|
+
const baseFill = makeFill(ctx, options.color, height);
|
|
354
|
+
const progFill = makeFill(ctx, options.progressColor, height);
|
|
206
355
|
|
|
207
356
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
208
357
|
|
|
@@ -213,7 +362,7 @@ export function drawBlocks(ctx, canvas, peaks, progress, options) {
|
|
|
213
362
|
const peakHeight = resampledPeaks[i] * height * 0.9;
|
|
214
363
|
const blockCount = Math.floor(peakHeight / (blockSize + blockGap));
|
|
215
364
|
|
|
216
|
-
ctx.fillStyle = x < progressWidth ?
|
|
365
|
+
ctx.fillStyle = x < progressWidth ? progFill : baseFill;
|
|
217
366
|
|
|
218
367
|
// Draw blocks from center outward
|
|
219
368
|
for (let j = 0; j < blockCount; j++) {
|
|
@@ -231,7 +380,17 @@ export function drawBlocks(ctx, canvas, peaks, progress, options) {
|
|
|
231
380
|
}
|
|
232
381
|
|
|
233
382
|
/**
|
|
234
|
-
* Draw dots style waveform -
|
|
383
|
+
* Draw dots style waveform - pairs of circular points mirrored about the centre.
|
|
384
|
+
* For each sample a dot is drawn above and below the centre line at half the peak
|
|
385
|
+
* height; dot radius scales with bar width but is floored at 1.5 device pixels.
|
|
386
|
+
* Per-dot colour is chosen by comparing x against the played width (no clip overlay).
|
|
387
|
+
* @param {CanvasRenderingContext2D} ctx - Canvas 2D context to draw into.
|
|
388
|
+
* @param {HTMLCanvasElement} canvas - Canvas element (provides device-pixel dimensions).
|
|
389
|
+
* @param {number[]} peaks - Normalised waveform peak values (0-1).
|
|
390
|
+
* @param {number} progress - Playback progress (0-1) used to pick each dot's colour.
|
|
391
|
+
* @param {Object} options - Drawing options: `barWidth` (default 2), `barSpacing` (default 3),
|
|
392
|
+
* `color`, `progressColor`.
|
|
393
|
+
* @returns {void}
|
|
235
394
|
*/
|
|
236
395
|
export function drawDots(ctx, canvas, peaks, progress, options) {
|
|
237
396
|
const dpr = window.devicePixelRatio || 1;
|
|
@@ -243,6 +402,8 @@ export function drawDots(ctx, canvas, peaks, progress, options) {
|
|
|
243
402
|
const dotRadius = Math.max(1.5 * dpr, barWidth / 2);
|
|
244
403
|
const progressWidth = progress * canvas.width;
|
|
245
404
|
const centerY = height / 2;
|
|
405
|
+
const baseFill = makeFill(ctx, options.color, height);
|
|
406
|
+
const progFill = makeFill(ctx, options.progressColor, height);
|
|
246
407
|
|
|
247
408
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
248
409
|
|
|
@@ -252,7 +413,7 @@ export function drawDots(ctx, canvas, peaks, progress, options) {
|
|
|
252
413
|
|
|
253
414
|
const peakHeight = resampledPeaks[i] * height * 0.9;
|
|
254
415
|
|
|
255
|
-
ctx.fillStyle = x < progressWidth ?
|
|
416
|
+
ctx.fillStyle = x < progressWidth ? progFill : baseFill;
|
|
256
417
|
|
|
257
418
|
// Draw upper dot
|
|
258
419
|
ctx.beginPath();
|
|
@@ -267,7 +428,17 @@ export function drawDots(ctx, canvas, peaks, progress, options) {
|
|
|
267
428
|
}
|
|
268
429
|
|
|
269
430
|
/**
|
|
270
|
-
* Draw seekbar style -
|
|
431
|
+
* Draw seekbar style - a simple rounded progress bar with no waveform.
|
|
432
|
+
* Renders a pill-shaped background track, a glowing pill-shaped filled portion
|
|
433
|
+
* (clamped to at least one full pill width so it never collapses), and a draggable
|
|
434
|
+
* circular handle/thumb at the playhead with a drop shadow and inner accent dot.
|
|
435
|
+
* The `peaks` argument is accepted for signature parity but is unused by this style.
|
|
436
|
+
* @param {CanvasRenderingContext2D} ctx - Canvas 2D context to draw into.
|
|
437
|
+
* @param {HTMLCanvasElement} canvas - Canvas element (provides device-pixel dimensions).
|
|
438
|
+
* @param {number[]} peaks - Ignored; present to match the shared draw-function signature.
|
|
439
|
+
* @param {number} progress - Playback progress (0-1); the fill and handle are only drawn when > 0.
|
|
440
|
+
* @param {Object} options - Drawing options: `color` (track), `progressColor` (fill/glow/accent).
|
|
441
|
+
* @returns {void}
|
|
271
442
|
*/
|
|
272
443
|
export function drawSeekbar(ctx, canvas, peaks, progress, options) {
|
|
273
444
|
const width = canvas.width;
|
|
@@ -281,14 +452,8 @@ export function drawSeekbar(ctx, canvas, peaks, progress, options) {
|
|
|
281
452
|
// Draw background track
|
|
282
453
|
ctx.fillStyle = options.color || 'rgba(255, 255, 255, 0.2)';
|
|
283
454
|
|
|
284
|
-
//
|
|
285
|
-
ctx
|
|
286
|
-
ctx.moveTo(borderRadius, centerY - barHeight / 2);
|
|
287
|
-
ctx.lineTo(width - borderRadius, centerY - barHeight / 2);
|
|
288
|
-
ctx.arc(width - borderRadius, centerY, barHeight / 2, -Math.PI / 2, Math.PI / 2);
|
|
289
|
-
ctx.lineTo(borderRadius, centerY + barHeight / 2);
|
|
290
|
-
ctx.arc(borderRadius, centerY, barHeight / 2, Math.PI / 2, -Math.PI / 2);
|
|
291
|
-
ctx.closePath();
|
|
455
|
+
// Rounded background track
|
|
456
|
+
capsulePath(ctx, borderRadius, width, centerY, barHeight);
|
|
292
457
|
ctx.fill();
|
|
293
458
|
|
|
294
459
|
// Draw progress
|
|
@@ -301,14 +466,8 @@ export function drawSeekbar(ctx, canvas, peaks, progress, options) {
|
|
|
301
466
|
|
|
302
467
|
ctx.fillStyle = options.progressColor || 'rgba(255, 255, 255, 0.9)';
|
|
303
468
|
|
|
304
|
-
//
|
|
305
|
-
ctx
|
|
306
|
-
ctx.moveTo(borderRadius, centerY - barHeight / 2);
|
|
307
|
-
ctx.lineTo(progressWidth - borderRadius, centerY - barHeight / 2);
|
|
308
|
-
ctx.arc(progressWidth - borderRadius, centerY, barHeight / 2, -Math.PI / 2, Math.PI / 2);
|
|
309
|
-
ctx.lineTo(borderRadius, centerY + barHeight / 2);
|
|
310
|
-
ctx.arc(borderRadius, centerY, barHeight / 2, Math.PI / 2, -Math.PI / 2);
|
|
311
|
-
ctx.closePath();
|
|
469
|
+
// Rounded progress fill
|
|
470
|
+
capsulePath(ctx, borderRadius, progressWidth, centerY, barHeight);
|
|
312
471
|
ctx.fill();
|
|
313
472
|
|
|
314
473
|
ctx.shadowBlur = 0;
|
|
@@ -339,8 +498,10 @@ export function drawSeekbar(ctx, canvas, peaks, progress, options) {
|
|
|
339
498
|
}
|
|
340
499
|
|
|
341
500
|
/**
|
|
342
|
-
* Map of style names to drawing functions
|
|
343
|
-
*
|
|
501
|
+
* Map of style names (and singular aliases) to their drawing functions.
|
|
502
|
+
* Six visually distinct styles including a simple seekbar; keys are matched
|
|
503
|
+
* against `options.waveformStyle` by {@link draw}.
|
|
504
|
+
* @type {Object.<string, function(CanvasRenderingContext2D, HTMLCanvasElement, number[], number, Object): void>}
|
|
344
505
|
*/
|
|
345
506
|
export const DRAWING_STYLES = {
|
|
346
507
|
'bars': drawBars, // Classic vertical bars
|
|
@@ -355,12 +516,15 @@ export const DRAWING_STYLES = {
|
|
|
355
516
|
};
|
|
356
517
|
|
|
357
518
|
/**
|
|
358
|
-
* Main drawing
|
|
519
|
+
* Main drawing entry point that delegates to the style named by
|
|
520
|
+
* `options.waveformStyle`, falling back to {@link drawBars} for unknown styles.
|
|
359
521
|
* @param {CanvasRenderingContext2D} ctx - Canvas context
|
|
360
522
|
* @param {HTMLCanvasElement} canvas - Canvas element
|
|
361
|
-
* @param {number[]} peaks - Waveform peak data
|
|
523
|
+
* @param {number[]} peaks - Waveform peak data (0-1)
|
|
362
524
|
* @param {number} progress - Progress (0-1)
|
|
363
|
-
* @param {Object} options - Drawing options
|
|
525
|
+
* @param {Object} options - Drawing options, including `waveformStyle` plus the
|
|
526
|
+
* per-style fields (`barWidth`, `barSpacing`, `barRadius`, `color`, `progressColor`).
|
|
527
|
+
* @returns {void}
|
|
364
528
|
*/
|
|
365
529
|
export function draw(ctx, canvas, peaks, progress, options) {
|
|
366
530
|
const drawFunc = DRAWING_STYLES[options.waveformStyle] || drawBars;
|
package/src/js/index.js
CHANGED
|
@@ -1,15 +1,47 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* @module index
|
|
3
|
+
* @description Public entry point for the WaveformPlayer library.
|
|
3
4
|
*
|
|
4
|
-
*
|
|
5
|
+
* Wires together the runtime surfaces for the player: it re-exports the
|
|
6
|
+
* {@link WaveformPlayer} class (default and named), exposes a static
|
|
7
|
+
* `WaveformPlayer.init` hook, scans the DOM for declarative `[data-waveform-player]`
|
|
8
|
+
* markup and auto-instantiates a player for each match, and attaches the class
|
|
9
|
+
* to `window` for plain `<script>`/CDN usage. Loading this module is enough to
|
|
10
|
+
* make any markup-driven players on the page come alive once the DOM is ready.
|
|
5
11
|
*/
|
|
6
12
|
|
|
7
13
|
// Import the main class
|
|
8
14
|
import {WaveformPlayer} from './core.js';
|
|
15
|
+
import {formatTime, extractTitleFromUrl, escapeHtml, isSafeHref} from './utils.js';
|
|
9
16
|
|
|
10
|
-
//
|
|
17
|
+
// Expose a small set of pure helpers as a single source of truth so consumers
|
|
18
|
+
// (e.g. @arraypress/waveform-bar) can reuse them instead of shipping divergent
|
|
19
|
+
// copies. Attached to the class so it's reachable from the IIFE global too.
|
|
20
|
+
WaveformPlayer.utils = {formatTime, extractTitleFromUrl, escapeHtml, isSafeHref};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Whether we're running in a browser (vs. SSR / Node), where `window` and
|
|
24
|
+
* `document` are available. Guards the auto-init and global-attach steps.
|
|
25
|
+
* @returns {boolean}
|
|
26
|
+
*/
|
|
27
|
+
const isBrowser = () => typeof window !== 'undefined' && typeof document !== 'undefined';
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Scan the document for declarative player markup and instantiate one
|
|
31
|
+
* {@link WaveformPlayer} per matching element.
|
|
32
|
+
*
|
|
33
|
+
* Finds every element carrying the `data-waveform-player` attribute and, for
|
|
34
|
+
* each one not already initialized, constructs a player from it (the constructor
|
|
35
|
+
* reads the element's `data-*` attributes for configuration). Each successfully
|
|
36
|
+
* initialized element is flagged with `data-waveform-initialized="true"` so
|
|
37
|
+
* repeat calls are idempotent and never double-initialize the same element.
|
|
38
|
+
* Construction errors are caught and logged so one broken element cannot abort
|
|
39
|
+
* the rest of the scan. A no-op in non-DOM environments (e.g. SSR).
|
|
40
|
+
*
|
|
41
|
+
* @returns {void}
|
|
42
|
+
*/
|
|
11
43
|
function autoInit() {
|
|
12
|
-
if (
|
|
44
|
+
if (!isBrowser()) return;
|
|
13
45
|
|
|
14
46
|
const elements = document.querySelectorAll('[data-waveform-player]');
|
|
15
47
|
|
|
@@ -20,13 +52,14 @@ function autoInit() {
|
|
|
20
52
|
new WaveformPlayer(element);
|
|
21
53
|
element.dataset.waveformInitialized = 'true';
|
|
22
54
|
} catch (error) {
|
|
23
|
-
console.error('Failed to initialize
|
|
55
|
+
console.error('[WaveformPlayer] Failed to initialize:', error, element);
|
|
24
56
|
}
|
|
25
57
|
});
|
|
26
58
|
}
|
|
27
59
|
|
|
28
|
-
// Initialize when DOM is ready
|
|
29
|
-
|
|
60
|
+
// Initialize when DOM is ready: defer until DOMContentLoaded if the document is
|
|
61
|
+
// still parsing, otherwise run the scan immediately on import.
|
|
62
|
+
if (isBrowser()) {
|
|
30
63
|
if (document.readyState === 'loading') {
|
|
31
64
|
document.addEventListener('DOMContentLoaded', autoInit);
|
|
32
65
|
} else {
|
|
@@ -34,15 +67,27 @@ if (typeof document !== 'undefined') {
|
|
|
34
67
|
}
|
|
35
68
|
}
|
|
36
69
|
|
|
37
|
-
|
|
70
|
+
/**
|
|
71
|
+
* Static re-scan hook.
|
|
72
|
+
*
|
|
73
|
+
* Exposes {@link autoInit} as `WaveformPlayer.init` so callers can manually
|
|
74
|
+
* (re-)scan the DOM after dynamically injecting `[data-waveform-player]` markup.
|
|
75
|
+
* Already-initialized elements are skipped on subsequent calls.
|
|
76
|
+
*
|
|
77
|
+
* @type {typeof autoInit}
|
|
78
|
+
*/
|
|
38
79
|
WaveformPlayer.init = autoInit;
|
|
39
80
|
|
|
40
|
-
// For CDN/browser usage
|
|
41
|
-
|
|
81
|
+
// For CDN/browser usage: expose the class as a global so it is reachable from a
|
|
82
|
+
// plain <script> tag without an ES module loader.
|
|
83
|
+
if (isBrowser()) {
|
|
42
84
|
window.WaveformPlayer = WaveformPlayer;
|
|
43
85
|
}
|
|
44
86
|
|
|
45
|
-
|
|
87
|
+
/**
|
|
88
|
+
* The {@link WaveformPlayer} class.
|
|
89
|
+
* @type {typeof WaveformPlayer}
|
|
90
|
+
*/
|
|
46
91
|
export default WaveformPlayer;
|
|
47
92
|
|
|
48
93
|
// Named exports
|