@arraypress/waveform-player 1.0.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/LICENSE +21 -0
- package/README.md +187 -0
- package/dist/waveform-player.css +185 -0
- package/dist/waveform-player.esm.js +42 -0
- package/dist/waveform-player.js +1087 -0
- package/dist/waveform-player.min.js +42 -0
- package/package.json +46 -0
- package/src/audio.js +94 -0
- package/src/bpm.js +92 -0
- package/src/core.js +680 -0
- package/src/drawing.js +365 -0
- package/src/index.js +51 -0
- package/src/themes.js +94 -0
- package/src/utils.js +202 -0
package/src/drawing.js
ADDED
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module drawing
|
|
3
|
+
* @description Core waveform drawing styles optimized for visual distinction at all sizes
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import {resampleData} from './utils.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Draw standard bars waveform - Classic vertical bars
|
|
10
|
+
*/
|
|
11
|
+
export function drawBars(ctx, canvas, peaks, progress, options) {
|
|
12
|
+
const dpr = window.devicePixelRatio || 1;
|
|
13
|
+
const barWidth = options.barWidth * dpr;
|
|
14
|
+
const barSpacing = options.barSpacing * dpr;
|
|
15
|
+
const barCount = Math.floor(canvas.width / (barWidth + barSpacing));
|
|
16
|
+
const resampledPeaks = resampleData(peaks, barCount);
|
|
17
|
+
const height = canvas.height;
|
|
18
|
+
const progressWidth = progress * canvas.width;
|
|
19
|
+
|
|
20
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
21
|
+
|
|
22
|
+
// Draw all bars first
|
|
23
|
+
for (let i = 0; i < resampledPeaks.length; i++) {
|
|
24
|
+
const x = i * (barWidth + barSpacing);
|
|
25
|
+
if (x + barWidth > canvas.width) break;
|
|
26
|
+
|
|
27
|
+
const peakHeight = resampledPeaks[i] * height * 0.9;
|
|
28
|
+
// Draw from bottom up, not centered
|
|
29
|
+
const y = height - peakHeight;
|
|
30
|
+
|
|
31
|
+
ctx.fillStyle = options.color;
|
|
32
|
+
ctx.fillRect(x, y, barWidth, peakHeight);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Progress overlay
|
|
36
|
+
ctx.save();
|
|
37
|
+
ctx.beginPath();
|
|
38
|
+
ctx.rect(0, 0, progressWidth, height);
|
|
39
|
+
ctx.clip();
|
|
40
|
+
|
|
41
|
+
for (let i = 0; i < resampledPeaks.length; i++) {
|
|
42
|
+
const x = i * (barWidth + barSpacing);
|
|
43
|
+
if (x > progressWidth) break;
|
|
44
|
+
|
|
45
|
+
const peakHeight = resampledPeaks[i] * height * 0.9;
|
|
46
|
+
// Draw from bottom up, not centered
|
|
47
|
+
const y = height - peakHeight;
|
|
48
|
+
|
|
49
|
+
ctx.fillStyle = options.progressColor;
|
|
50
|
+
ctx.fillRect(x, y, barWidth, peakHeight);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
ctx.restore();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Draw mirror/SoundCloud style waveform - Symmetrical bars
|
|
58
|
+
*/
|
|
59
|
+
/**
|
|
60
|
+
* Draw mirror/SoundCloud style waveform - Symmetrical bars
|
|
61
|
+
*/
|
|
62
|
+
export function drawMirror(ctx, canvas, peaks, progress, options) {
|
|
63
|
+
const dpr = window.devicePixelRatio || 1;
|
|
64
|
+
const barWidth = options.barWidth * dpr;
|
|
65
|
+
const barSpacing = options.barSpacing * dpr;
|
|
66
|
+
const barCount = Math.floor(canvas.width / (barWidth + barSpacing));
|
|
67
|
+
const resampledPeaks = resampleData(peaks, barCount);
|
|
68
|
+
const height = canvas.height;
|
|
69
|
+
const centerY = height / 2;
|
|
70
|
+
const progressWidth = progress * canvas.width;
|
|
71
|
+
|
|
72
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
73
|
+
|
|
74
|
+
// Draw all bars
|
|
75
|
+
for (let i = 0; i < resampledPeaks.length; i++) {
|
|
76
|
+
const x = i * (barWidth + barSpacing);
|
|
77
|
+
if (x + barWidth > canvas.width) break;
|
|
78
|
+
|
|
79
|
+
const peakHeight = resampledPeaks[i] * height * 0.45;
|
|
80
|
+
|
|
81
|
+
ctx.fillStyle = options.color;
|
|
82
|
+
ctx.fillRect(x, centerY - peakHeight, barWidth, peakHeight);
|
|
83
|
+
ctx.fillRect(x, centerY, barWidth, peakHeight);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Progress overlay
|
|
87
|
+
ctx.save();
|
|
88
|
+
ctx.beginPath();
|
|
89
|
+
ctx.rect(0, 0, progressWidth, height);
|
|
90
|
+
ctx.clip();
|
|
91
|
+
|
|
92
|
+
for (let i = 0; i < resampledPeaks.length; i++) {
|
|
93
|
+
const x = i * (barWidth + barSpacing);
|
|
94
|
+
if (x > progressWidth) break;
|
|
95
|
+
|
|
96
|
+
const peakHeight = resampledPeaks[i] * height * 0.45;
|
|
97
|
+
|
|
98
|
+
ctx.fillStyle = options.progressColor;
|
|
99
|
+
ctx.fillRect(x, centerY - peakHeight, barWidth, peakHeight);
|
|
100
|
+
ctx.fillRect(x, centerY, barWidth, peakHeight);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
ctx.restore();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Draw line/oscilloscope style waveform - Smooth flowing wave with glow
|
|
108
|
+
*/
|
|
109
|
+
export function drawLine(ctx, canvas, peaks, progress, options) {
|
|
110
|
+
const width = canvas.width;
|
|
111
|
+
const height = canvas.height;
|
|
112
|
+
const centerY = height / 2;
|
|
113
|
+
const amplitude = height * 0.35;
|
|
114
|
+
|
|
115
|
+
ctx.clearRect(0, 0, width, height);
|
|
116
|
+
|
|
117
|
+
// Helper to draw a smooth curve through the peaks
|
|
118
|
+
const drawCurve = (color, lineWidth, endProgress = 1, addGlow = false) => {
|
|
119
|
+
if (addGlow) {
|
|
120
|
+
ctx.shadowBlur = 12;
|
|
121
|
+
ctx.shadowColor = color;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
ctx.strokeStyle = color;
|
|
125
|
+
ctx.lineWidth = lineWidth;
|
|
126
|
+
ctx.lineCap = 'round';
|
|
127
|
+
ctx.lineJoin = 'round';
|
|
128
|
+
|
|
129
|
+
ctx.beginPath();
|
|
130
|
+
ctx.moveTo(0, centerY);
|
|
131
|
+
|
|
132
|
+
const points = [];
|
|
133
|
+
const samples = Math.floor(peaks.length * endProgress);
|
|
134
|
+
|
|
135
|
+
// Calculate smoothed points
|
|
136
|
+
for (let i = 0; i < samples; i++) {
|
|
137
|
+
const x = (i / (peaks.length - 1)) * width;
|
|
138
|
+
const peakValue = peaks[i];
|
|
139
|
+
|
|
140
|
+
// Create a smooth wave motion
|
|
141
|
+
const waveOffset = Math.sin(i * 0.1) * peakValue;
|
|
142
|
+
const y = centerY + (waveOffset * amplitude);
|
|
143
|
+
|
|
144
|
+
points.push({x, y});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Draw smooth curve through points using bezier curves
|
|
148
|
+
for (let i = 0; i < points.length - 1; i++) {
|
|
149
|
+
const cp1x = points[i].x + (points[i + 1].x - points[i].x) * 0.5;
|
|
150
|
+
const cp1y = points[i].y;
|
|
151
|
+
const cp2x = points[i + 1].x - (points[i + 1].x - points[i].x) * 0.5;
|
|
152
|
+
const cp2y = points[i + 1].y;
|
|
153
|
+
|
|
154
|
+
ctx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, points[i + 1].x, points[i + 1].y);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
ctx.stroke();
|
|
158
|
+
|
|
159
|
+
if (addGlow) {
|
|
160
|
+
ctx.shadowBlur = 0;
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
// Draw subtle grid for oscilloscope feel
|
|
165
|
+
ctx.strokeStyle = 'rgba(255, 255, 255, 0.03)';
|
|
166
|
+
ctx.lineWidth = 0.5;
|
|
167
|
+
|
|
168
|
+
// Horizontal center line
|
|
169
|
+
ctx.beginPath();
|
|
170
|
+
ctx.moveTo(0, centerY);
|
|
171
|
+
ctx.lineTo(width, centerY);
|
|
172
|
+
ctx.stroke();
|
|
173
|
+
|
|
174
|
+
// Vertical grid lines
|
|
175
|
+
for (let i = 0; i <= 10; i++) {
|
|
176
|
+
const x = (width / 10) * i;
|
|
177
|
+
ctx.beginPath();
|
|
178
|
+
ctx.moveTo(x, 0);
|
|
179
|
+
ctx.lineTo(x, height);
|
|
180
|
+
ctx.stroke();
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Draw background wave
|
|
184
|
+
drawCurve(options.color, 2, 1, false);
|
|
185
|
+
|
|
186
|
+
// Draw progress with glow
|
|
187
|
+
if (progress > 0) {
|
|
188
|
+
drawCurve(options.progressColor, 3, progress, true);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Draw blocks/LED meter style waveform - Segmented blocks
|
|
194
|
+
*/
|
|
195
|
+
export function drawBlocks(ctx, canvas, peaks, progress, options) {
|
|
196
|
+
const dpr = window.devicePixelRatio || 1;
|
|
197
|
+
const barWidth = (options.barWidth || 3) * dpr;
|
|
198
|
+
const barSpacing = (options.barSpacing || 1) * dpr;
|
|
199
|
+
const barCount = Math.floor(canvas.width / (barWidth + barSpacing));
|
|
200
|
+
const resampledPeaks = resampleData(peaks, barCount);
|
|
201
|
+
const height = canvas.height;
|
|
202
|
+
const blockSize = 4 * dpr;
|
|
203
|
+
const blockGap = 2 * dpr;
|
|
204
|
+
const progressWidth = progress * canvas.width;
|
|
205
|
+
const centerY = height / 2;
|
|
206
|
+
|
|
207
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
208
|
+
|
|
209
|
+
for (let i = 0; i < resampledPeaks.length; i++) {
|
|
210
|
+
const x = i * (barWidth + barSpacing);
|
|
211
|
+
if (x + barWidth > canvas.width) break;
|
|
212
|
+
|
|
213
|
+
const peakHeight = resampledPeaks[i] * height * 0.9;
|
|
214
|
+
const blockCount = Math.floor(peakHeight / (blockSize + blockGap));
|
|
215
|
+
|
|
216
|
+
ctx.fillStyle = x < progressWidth ? options.progressColor : options.color;
|
|
217
|
+
|
|
218
|
+
// Draw blocks from center outward
|
|
219
|
+
for (let j = 0; j < blockCount; j++) {
|
|
220
|
+
const blockOffset = j * (blockSize + blockGap);
|
|
221
|
+
|
|
222
|
+
// Upper blocks
|
|
223
|
+
ctx.fillRect(x, centerY - blockOffset - blockSize, barWidth, blockSize);
|
|
224
|
+
|
|
225
|
+
// Lower blocks (skip the center block)
|
|
226
|
+
if (j > 0) {
|
|
227
|
+
ctx.fillRect(x, centerY + blockOffset, barWidth, blockSize);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Draw dots style waveform - Circular points
|
|
235
|
+
*/
|
|
236
|
+
export function drawDots(ctx, canvas, peaks, progress, options) {
|
|
237
|
+
const dpr = window.devicePixelRatio || 1;
|
|
238
|
+
const barWidth = (options.barWidth || 2) * dpr;
|
|
239
|
+
const barSpacing = (options.barSpacing || 3) * dpr;
|
|
240
|
+
const barCount = Math.floor(canvas.width / (barWidth + barSpacing));
|
|
241
|
+
const resampledPeaks = resampleData(peaks, barCount);
|
|
242
|
+
const height = canvas.height;
|
|
243
|
+
const dotRadius = Math.max(1.5 * dpr, barWidth / 2);
|
|
244
|
+
const progressWidth = progress * canvas.width;
|
|
245
|
+
const centerY = height / 2;
|
|
246
|
+
|
|
247
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
248
|
+
|
|
249
|
+
for (let i = 0; i < resampledPeaks.length; i++) {
|
|
250
|
+
const x = i * (barWidth + barSpacing) + barWidth / 2;
|
|
251
|
+
if (x > canvas.width) break;
|
|
252
|
+
|
|
253
|
+
const peakHeight = resampledPeaks[i] * height * 0.9;
|
|
254
|
+
|
|
255
|
+
ctx.fillStyle = x < progressWidth ? options.progressColor : options.color;
|
|
256
|
+
|
|
257
|
+
// Draw upper dot
|
|
258
|
+
ctx.beginPath();
|
|
259
|
+
ctx.arc(x, centerY - peakHeight / 2, dotRadius, 0, Math.PI * 2);
|
|
260
|
+
ctx.fill();
|
|
261
|
+
|
|
262
|
+
// Draw lower dot
|
|
263
|
+
ctx.beginPath();
|
|
264
|
+
ctx.arc(x, centerY + peakHeight / 2, dotRadius, 0, Math.PI * 2);
|
|
265
|
+
ctx.fill();
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Draw seekbar style - Simple progress bar without waveform
|
|
271
|
+
*/
|
|
272
|
+
export function drawSeekbar(ctx, canvas, peaks, progress, options) {
|
|
273
|
+
const width = canvas.width;
|
|
274
|
+
const height = canvas.height;
|
|
275
|
+
const centerY = height / 2;
|
|
276
|
+
const barHeight = 4; // Height of the seekbar in pixels
|
|
277
|
+
const borderRadius = barHeight / 2;
|
|
278
|
+
|
|
279
|
+
ctx.clearRect(0, 0, width, height);
|
|
280
|
+
|
|
281
|
+
// Draw background track
|
|
282
|
+
ctx.fillStyle = options.color || 'rgba(255, 255, 255, 0.2)';
|
|
283
|
+
|
|
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();
|
|
292
|
+
ctx.fill();
|
|
293
|
+
|
|
294
|
+
// Draw progress
|
|
295
|
+
if (progress > 0) {
|
|
296
|
+
const progressWidth = Math.max(borderRadius * 2, progress * width);
|
|
297
|
+
|
|
298
|
+
// Add subtle glow effect
|
|
299
|
+
ctx.shadowBlur = 8;
|
|
300
|
+
ctx.shadowColor = options.progressColor;
|
|
301
|
+
|
|
302
|
+
ctx.fillStyle = options.progressColor || 'rgba(255, 255, 255, 0.9)';
|
|
303
|
+
|
|
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();
|
|
312
|
+
ctx.fill();
|
|
313
|
+
|
|
314
|
+
ctx.shadowBlur = 0;
|
|
315
|
+
|
|
316
|
+
// Draw progress handle/thumb
|
|
317
|
+
const handleRadius = 8;
|
|
318
|
+
const handleX = progressWidth;
|
|
319
|
+
|
|
320
|
+
// Handle shadow
|
|
321
|
+
ctx.shadowBlur = 4;
|
|
322
|
+
ctx.shadowColor = 'rgba(0, 0, 0, 0.3)';
|
|
323
|
+
ctx.shadowOffsetY = 2;
|
|
324
|
+
|
|
325
|
+
// Handle circle
|
|
326
|
+
ctx.fillStyle = '#ffffff';
|
|
327
|
+
ctx.beginPath();
|
|
328
|
+
ctx.arc(handleX, centerY, handleRadius, 0, Math.PI * 2);
|
|
329
|
+
ctx.fill();
|
|
330
|
+
|
|
331
|
+
// Handle inner circle (for depth)
|
|
332
|
+
ctx.shadowBlur = 0;
|
|
333
|
+
ctx.shadowOffsetY = 0;
|
|
334
|
+
ctx.fillStyle = options.progressColor || 'rgba(255, 255, 255, 0.9)';
|
|
335
|
+
ctx.beginPath();
|
|
336
|
+
ctx.arc(handleX, centerY, handleRadius * 0.4, 0, Math.PI * 2);
|
|
337
|
+
ctx.fill();
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Map of style names to drawing functions
|
|
343
|
+
* 6 visually distinct styles including a simple seekbar
|
|
344
|
+
*/
|
|
345
|
+
export const DRAWING_STYLES = {
|
|
346
|
+
'bars': drawBars, // Classic vertical bars
|
|
347
|
+
'mirror': drawMirror, // SoundCloud-style symmetrical
|
|
348
|
+
'line': drawLine, // Smooth oscilloscope wave
|
|
349
|
+
'blocks': drawBlocks, // LED meter segmented
|
|
350
|
+
'dots': drawDots, // Circular points
|
|
351
|
+
'seekbar': drawSeekbar // Simple progress bar (no waveform)
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Main drawing function that delegates to appropriate style
|
|
356
|
+
* @param {CanvasRenderingContext2D} ctx - Canvas context
|
|
357
|
+
* @param {HTMLCanvasElement} canvas - Canvas element
|
|
358
|
+
* @param {number[]} peaks - Waveform peak data
|
|
359
|
+
* @param {number} progress - Progress (0-1)
|
|
360
|
+
* @param {Object} options - Drawing options
|
|
361
|
+
*/
|
|
362
|
+
export function draw(ctx, canvas, peaks, progress, options) {
|
|
363
|
+
const drawFunc = DRAWING_STYLES[options.waveformStyle] || drawBars;
|
|
364
|
+
drawFunc(ctx, canvas, peaks, progress, options);
|
|
365
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WaveformPlayer
|
|
3
|
+
*
|
|
4
|
+
* Modern audio player with waveform visualization
|
|
5
|
+
*
|
|
6
|
+
* @version 1.0.0
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// Import the main class
|
|
10
|
+
import {WaveformPlayer} from './core.js';
|
|
11
|
+
|
|
12
|
+
// Auto-initialization for data attributes
|
|
13
|
+
function autoInit() {
|
|
14
|
+
if (typeof document === 'undefined') return;
|
|
15
|
+
|
|
16
|
+
const elements = document.querySelectorAll('[data-waveform-player]');
|
|
17
|
+
|
|
18
|
+
elements.forEach(element => {
|
|
19
|
+
if (element.dataset.waveformInitialized === 'true') return;
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
new WaveformPlayer(element);
|
|
23
|
+
element.dataset.waveformInitialized = 'true';
|
|
24
|
+
} catch (error) {
|
|
25
|
+
console.error('Failed to initialize WaveformPlayer:', error, element);
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Initialize when DOM is ready
|
|
31
|
+
if (typeof document !== 'undefined') {
|
|
32
|
+
if (document.readyState === 'loading') {
|
|
33
|
+
document.addEventListener('DOMContentLoaded', autoInit);
|
|
34
|
+
} else {
|
|
35
|
+
autoInit();
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Add init method
|
|
40
|
+
WaveformPlayer.init = autoInit;
|
|
41
|
+
|
|
42
|
+
// For CDN/browser usage
|
|
43
|
+
if (typeof window !== 'undefined') {
|
|
44
|
+
window.WaveformPlayer = WaveformPlayer;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Default export for ES modules
|
|
48
|
+
export default WaveformPlayer;
|
|
49
|
+
|
|
50
|
+
// Named exports
|
|
51
|
+
export {WaveformPlayer};
|
package/src/themes.js
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module themes
|
|
3
|
+
* @description Color presets and default options for WaveformPlayer
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Color presets - simple dark/light defaults that can be overridden
|
|
8
|
+
*/
|
|
9
|
+
export const COLOR_PRESETS = {
|
|
10
|
+
dark: {
|
|
11
|
+
waveformColor: 'rgba(255, 255, 255, 0.3)',
|
|
12
|
+
progressColor: 'rgba(255, 255, 255, 0.9)',
|
|
13
|
+
buttonColor: 'rgba(255, 255, 255, 0.9)',
|
|
14
|
+
buttonHoverColor: 'rgba(255, 255, 255, 1)',
|
|
15
|
+
textColor: '#ffffff',
|
|
16
|
+
textSecondaryColor: 'rgba(255, 255, 255, 0.6)',
|
|
17
|
+
backgroundColor: 'rgba(255, 255, 255, 0.03)',
|
|
18
|
+
borderColor: 'rgba(255, 255, 255, 0.1)'
|
|
19
|
+
},
|
|
20
|
+
light: {
|
|
21
|
+
waveformColor: 'rgba(0, 0, 0, 0.2)',
|
|
22
|
+
progressColor: 'rgba(0, 0, 0, 0.8)',
|
|
23
|
+
buttonColor: 'rgba(0, 0, 0, 0.8)',
|
|
24
|
+
buttonHoverColor: 'rgba(0, 0, 0, 0.9)',
|
|
25
|
+
textColor: '#333333',
|
|
26
|
+
textSecondaryColor: 'rgba(0, 0, 0, 0.6)',
|
|
27
|
+
backgroundColor: 'rgba(0, 0, 0, 0.02)',
|
|
28
|
+
borderColor: 'rgba(0, 0, 0, 0.1)'
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Default player options
|
|
34
|
+
*/
|
|
35
|
+
export const DEFAULT_OPTIONS = {
|
|
36
|
+
// Core settings
|
|
37
|
+
url: '',
|
|
38
|
+
height: 60,
|
|
39
|
+
samples: 200,
|
|
40
|
+
|
|
41
|
+
// Default waveform style
|
|
42
|
+
waveformStyle: 'mirror',
|
|
43
|
+
barWidth: 2,
|
|
44
|
+
barSpacing: 0,
|
|
45
|
+
|
|
46
|
+
// Color preset (dark/light or null for custom)
|
|
47
|
+
colorPreset: 'dark',
|
|
48
|
+
|
|
49
|
+
// Individual color overrides (null means use preset)
|
|
50
|
+
waveformColor: null,
|
|
51
|
+
progressColor: null,
|
|
52
|
+
buttonColor: null,
|
|
53
|
+
buttonHoverColor: null,
|
|
54
|
+
textColor: null,
|
|
55
|
+
textSecondaryColor: null,
|
|
56
|
+
backgroundColor: null,
|
|
57
|
+
borderColor: null,
|
|
58
|
+
|
|
59
|
+
// Features
|
|
60
|
+
autoplay: false,
|
|
61
|
+
showTime: true,
|
|
62
|
+
showHoverTime: false,
|
|
63
|
+
showBPM: false,
|
|
64
|
+
singlePlay: true,
|
|
65
|
+
playOnSeek: true,
|
|
66
|
+
|
|
67
|
+
// Content
|
|
68
|
+
title: null,
|
|
69
|
+
subtitle: null,
|
|
70
|
+
|
|
71
|
+
// Icons (SVG)
|
|
72
|
+
playIcon: '<svg viewBox="0 0 24 24" width="16" height="16"><path d="M8 5v14l11-7z"/></svg>',
|
|
73
|
+
pauseIcon: '<svg viewBox="0 0 24 24" width="16" height="16"><path d="M6 4h4v16H6zM14 4h4v16h-4z"/></svg>',
|
|
74
|
+
|
|
75
|
+
// Callbacks
|
|
76
|
+
onLoad: null,
|
|
77
|
+
onPlay: null,
|
|
78
|
+
onPause: null,
|
|
79
|
+
onEnd: null,
|
|
80
|
+
onError: null,
|
|
81
|
+
onTimeUpdate: null
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Style defaults
|
|
86
|
+
*/
|
|
87
|
+
export const STYLE_DEFAULTS = {
|
|
88
|
+
bars: { barWidth: 3, barSpacing: 1 },
|
|
89
|
+
mirror: { barWidth: 2, barSpacing: 0 },
|
|
90
|
+
line: { barWidth: 2, barSpacing: 0 },
|
|
91
|
+
blocks: { barWidth: 4, barSpacing: 2 },
|
|
92
|
+
dots: { barWidth: 3, barSpacing: 3 },
|
|
93
|
+
seekbar: { barWidth: 1, barSpacing: 0 }
|
|
94
|
+
};
|
package/src/utils.js
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module utils
|
|
3
|
+
* @description Utility functions for WaveformPlayer
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Parse data attributes from element
|
|
8
|
+
* @param {HTMLElement} element - Element with data attributes
|
|
9
|
+
* @returns {Object} Parsed options
|
|
10
|
+
*/
|
|
11
|
+
export function parseDataAttributes(element) {
|
|
12
|
+
const options = {};
|
|
13
|
+
|
|
14
|
+
// Core attributes
|
|
15
|
+
if (element.dataset.url) options.url = element.dataset.url;
|
|
16
|
+
if (element.dataset.height) options.height = parseInt(element.dataset.height);
|
|
17
|
+
if (element.dataset.samples) options.samples = parseInt(element.dataset.samples);
|
|
18
|
+
|
|
19
|
+
// Waveform style attributes
|
|
20
|
+
if (element.dataset.waveformStyle) options.waveformStyle = element.dataset.waveformStyle;
|
|
21
|
+
if (element.dataset.barWidth) options.barWidth = parseInt(element.dataset.barWidth);
|
|
22
|
+
if (element.dataset.barSpacing) options.barSpacing = parseInt(element.dataset.barSpacing);
|
|
23
|
+
|
|
24
|
+
// Color preset
|
|
25
|
+
if (element.dataset.colorPreset) options.colorPreset = element.dataset.colorPreset;
|
|
26
|
+
|
|
27
|
+
// Individual color customization
|
|
28
|
+
if (element.dataset.waveformColor) options.waveformColor = element.dataset.waveformColor;
|
|
29
|
+
if (element.dataset.progressColor) options.progressColor = element.dataset.progressColor;
|
|
30
|
+
if (element.dataset.buttonColor) options.buttonColor = element.dataset.buttonColor;
|
|
31
|
+
if (element.dataset.buttonHoverColor) options.buttonHoverColor = element.dataset.buttonHoverColor;
|
|
32
|
+
if (element.dataset.textColor) options.textColor = element.dataset.textColor;
|
|
33
|
+
if (element.dataset.textSecondaryColor) options.textSecondaryColor = element.dataset.textSecondaryColor;
|
|
34
|
+
if (element.dataset.backgroundColor) options.backgroundColor = element.dataset.backgroundColor;
|
|
35
|
+
if (element.dataset.borderColor) options.borderColor = element.dataset.borderColor;
|
|
36
|
+
|
|
37
|
+
// Legacy support for old attribute names
|
|
38
|
+
if (element.dataset.color) options.waveformColor = element.dataset.color;
|
|
39
|
+
if (element.dataset.theme) options.colorPreset = element.dataset.theme;
|
|
40
|
+
|
|
41
|
+
// Feature flags
|
|
42
|
+
if (element.dataset.autoplay) options.autoplay = element.dataset.autoplay === 'true';
|
|
43
|
+
if (element.dataset.showTime) options.showTime = element.dataset.showTime === 'true';
|
|
44
|
+
if (element.dataset.showHoverTime) options.showHoverTime = element.dataset.showHoverTime === 'true';
|
|
45
|
+
if (element.dataset.showBpm) options.showBPM = element.dataset.showBpm === 'true';
|
|
46
|
+
if (element.dataset.singlePlay) options.singlePlay = element.dataset.singlePlay === 'true';
|
|
47
|
+
if (element.dataset.playOnSeek) options.playOnSeek = element.dataset.playOnSeek === 'true';
|
|
48
|
+
|
|
49
|
+
// Content
|
|
50
|
+
if (element.dataset.title) options.title = element.dataset.title;
|
|
51
|
+
if (element.dataset.subtitle) options.subtitle = element.dataset.subtitle;
|
|
52
|
+
|
|
53
|
+
// Waveform data
|
|
54
|
+
if (element.dataset.waveform) options.waveform = element.dataset.waveform;
|
|
55
|
+
|
|
56
|
+
return options;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Format time in MM:SS format
|
|
61
|
+
* @param {number} seconds - Time in seconds
|
|
62
|
+
* @returns {string} Formatted time
|
|
63
|
+
*/
|
|
64
|
+
export function formatTime(seconds) {
|
|
65
|
+
if (!seconds || isNaN(seconds)) return '0:00';
|
|
66
|
+
|
|
67
|
+
const mins = Math.floor(seconds / 60);
|
|
68
|
+
const secs = Math.floor(seconds % 60);
|
|
69
|
+
|
|
70
|
+
return `${mins}:${secs.toString().padStart(2, '0')}`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Generate unique ID from URL
|
|
75
|
+
* @param {string} url - Audio URL
|
|
76
|
+
* @returns {string} Base64 encoded ID
|
|
77
|
+
*/
|
|
78
|
+
export function generateId(url) {
|
|
79
|
+
const str = url || Math.random().toString();
|
|
80
|
+
return btoa(str.substring(0, 10)).replace(/[^a-zA-Z0-9]/g, '');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Extract title from URL
|
|
85
|
+
* @param {string} url - Audio URL
|
|
86
|
+
* @returns {string} Extracted title
|
|
87
|
+
*/
|
|
88
|
+
export function extractTitleFromUrl(url) {
|
|
89
|
+
if (!url) return 'Audio';
|
|
90
|
+
|
|
91
|
+
const parts = url.split('/');
|
|
92
|
+
const filename = parts[parts.length - 1];
|
|
93
|
+
const name = filename.split('.')[0];
|
|
94
|
+
|
|
95
|
+
// Clean up common separators
|
|
96
|
+
return name
|
|
97
|
+
.replace(/[-_]/g, ' ')
|
|
98
|
+
.replace(/\b\w/g, l => l.toUpperCase());
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Merge multiple option objects
|
|
103
|
+
* @param {...Object} sources - Option objects to merge
|
|
104
|
+
* @returns {Object} Merged options
|
|
105
|
+
*/
|
|
106
|
+
export function mergeOptions(...sources) {
|
|
107
|
+
const result = {};
|
|
108
|
+
|
|
109
|
+
for (const source of sources) {
|
|
110
|
+
for (const key in source) {
|
|
111
|
+
if (source[key] !== null && source[key] !== undefined) {
|
|
112
|
+
result[key] = source[key];
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return result;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Debounce function
|
|
122
|
+
* @param {Function} func - Function to debounce
|
|
123
|
+
* @param {number} wait - Wait time in ms
|
|
124
|
+
* @returns {Function} Debounced function
|
|
125
|
+
*/
|
|
126
|
+
export function debounce(func, wait) {
|
|
127
|
+
let timeout;
|
|
128
|
+
|
|
129
|
+
return function executedFunction(...args) {
|
|
130
|
+
const later = () => {
|
|
131
|
+
clearTimeout(timeout);
|
|
132
|
+
func(...args);
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
clearTimeout(timeout);
|
|
136
|
+
timeout = setTimeout(later, wait);
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Resample array data
|
|
142
|
+
* @param {number[]} data - Original data
|
|
143
|
+
* @param {number} targetLength - Target length
|
|
144
|
+
* @returns {number[]} Resampled data
|
|
145
|
+
*/
|
|
146
|
+
export function resampleData(data, targetLength) {
|
|
147
|
+
if (data.length === targetLength) return data;
|
|
148
|
+
if (data.length === 0 || targetLength === 0) return [];
|
|
149
|
+
|
|
150
|
+
const result = [];
|
|
151
|
+
|
|
152
|
+
// If upsampling (target is larger than source)
|
|
153
|
+
if (targetLength > data.length) {
|
|
154
|
+
const ratio = (data.length - 1) / (targetLength - 1);
|
|
155
|
+
|
|
156
|
+
for (let i = 0; i < targetLength; i++) {
|
|
157
|
+
const index = i * ratio;
|
|
158
|
+
const lower = Math.floor(index);
|
|
159
|
+
const upper = Math.ceil(index);
|
|
160
|
+
const fraction = index - lower;
|
|
161
|
+
|
|
162
|
+
// Linear interpolation between samples
|
|
163
|
+
if (upper >= data.length) {
|
|
164
|
+
result.push(data[data.length - 1]);
|
|
165
|
+
} else if (lower === upper) {
|
|
166
|
+
result.push(data[lower]);
|
|
167
|
+
} else {
|
|
168
|
+
const value = data[lower] * (1 - fraction) + data[upper] * fraction;
|
|
169
|
+
result.push(value);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
} else {
|
|
173
|
+
// Downsampling (target is smaller than source)
|
|
174
|
+
const bucketSize = data.length / targetLength;
|
|
175
|
+
|
|
176
|
+
for (let i = 0; i < targetLength; i++) {
|
|
177
|
+
const start = Math.floor(i * bucketSize);
|
|
178
|
+
const end = Math.floor((i + 1) * bucketSize);
|
|
179
|
+
|
|
180
|
+
// Find the maximum value in this bucket
|
|
181
|
+
let max = 0;
|
|
182
|
+
let count = 0;
|
|
183
|
+
|
|
184
|
+
for (let j = start; j <= end && j < data.length; j++) {
|
|
185
|
+
if (data[j] > max) {
|
|
186
|
+
max = data[j];
|
|
187
|
+
}
|
|
188
|
+
count++;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// If no samples were found in this bucket, use nearest neighbor
|
|
192
|
+
if (count === 0) {
|
|
193
|
+
const nearestIndex = Math.min(Math.round(i * bucketSize), data.length - 1);
|
|
194
|
+
max = data[nearestIndex];
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
result.push(max);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return result;
|
|
202
|
+
}
|