@arraypress/waveform-player 1.7.2 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
- * Draw standard bars waveform - Classic vertical bars
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.fillStyle = options.color;
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.fillStyle = options.progressColor;
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 - Symmetrical bars
58
- */
59
- /**
60
- * Draw mirror/SoundCloud style waveform - Symmetrical bars
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.fillStyle = options.color;
82
- ctx.fillRect(x, centerY - peakHeight, barWidth, peakHeight);
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.fillStyle = options.progressColor;
99
- ctx.fillRect(x, centerY - peakHeight, barWidth, peakHeight);
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 - Smooth flowing wave with glow
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
- // Helper to draw a smooth curve through the peaks
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 - Segmented blocks
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 ? options.progressColor : options.color;
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 - Circular points
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 ? options.progressColor : options.color;
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 - Simple progress bar without waveform
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
- // Create rounded rectangle for background
285
- ctx.beginPath();
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
- // Create rounded rectangle for progress
305
- ctx.beginPath();
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
- * 6 visually distinct styles including a simple seekbar
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 function that delegates to appropriate style
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
- * WaveformPlayer
2
+ * @module index
3
+ * @description Public entry point for the WaveformPlayer library.
3
4
  *
4
- * Modern audio player with waveform visualization
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
- // Auto-initialization for data attributes
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 (typeof document === 'undefined') return;
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 WaveformPlayer:', error, element);
55
+ console.error('[WaveformPlayer] Failed to initialize:', error, element);
24
56
  }
25
57
  });
26
58
  }
27
59
 
28
- // Initialize when DOM is ready
29
- if (typeof document !== 'undefined') {
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
- // Add init method
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
- if (typeof window !== 'undefined') {
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
- // Default export for ES modules
87
+ /**
88
+ * The {@link WaveformPlayer} class.
89
+ * @type {typeof WaveformPlayer}
90
+ */
46
91
  export default WaveformPlayer;
47
92
 
48
93
  // Named exports