smooth-player 1.0.1 → 2.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/README.md +116 -12
- package/assets/icons/logo-wave-preview.svg +28 -0
- package/assets/icons/logo.svg +8 -0
- package/assets/icons/stop.svg +3 -0
- package/assets/icons/upload.svg +5 -0
- package/assets/icons/visualizer.svg +7 -0
- package/dist/SmoothPlayer.d.ts +16 -1
- package/dist/SmoothPlayer.js +295 -13
- package/dist/i18n/en.generated.d.ts +50 -0
- package/dist/i18n/en.generated.js +51 -0
- package/dist/i18n/strings.d.ts +51 -0
- package/dist/i18n/strings.js +2 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.js +2 -1
- package/dist/smooth-player.css +575 -154
- package/dist/types.d.ts +31 -0
- package/dist/ui.d.ts +1 -1
- package/dist/ui.js +567 -14
- package/dist/visualizers.d.ts +2 -0
- package/dist/visualizers.js +150 -15
- package/dist-cjs/SmoothPlayer.js +295 -13
- package/dist-cjs/i18n/en.generated.js +54 -0
- package/dist-cjs/i18n/strings.js +5 -0
- package/dist-cjs/index.js +4 -2
- package/dist-cjs/ui.js +568 -15
- package/dist-cjs/visualizers.js +150 -15
- package/package.json +15 -3
- package/styles/common/_base.scss +216 -95
- package/styles/themes/_nocturne.scss +408 -62
- package/styles/themes/_aurora.scss +0 -70
- package/styles/themes/_ocean.scss +0 -13
package/dist-cjs/visualizers.js
CHANGED
|
@@ -15,9 +15,40 @@ class CanvasVisualizer {
|
|
|
15
15
|
cancelAnimationFrame(this.frameId);
|
|
16
16
|
}
|
|
17
17
|
}
|
|
18
|
+
function withAlpha(color, alpha) {
|
|
19
|
+
const safeAlpha = Math.max(0, Math.min(1, alpha));
|
|
20
|
+
const hex = color.trim();
|
|
21
|
+
if (/^#[\da-f]{3}$/i.test(hex)) {
|
|
22
|
+
const rChar = hex.charAt(1);
|
|
23
|
+
const gChar = hex.charAt(2);
|
|
24
|
+
const bChar = hex.charAt(3);
|
|
25
|
+
const r = parseInt(rChar + rChar, 16);
|
|
26
|
+
const g = parseInt(gChar + gChar, 16);
|
|
27
|
+
const b = parseInt(bChar + bChar, 16);
|
|
28
|
+
return `rgba(${r}, ${g}, ${b}, ${safeAlpha})`;
|
|
29
|
+
}
|
|
30
|
+
if (/^#[\da-f]{6}$/i.test(hex)) {
|
|
31
|
+
const r = parseInt(hex.slice(1, 3), 16);
|
|
32
|
+
const g = parseInt(hex.slice(3, 5), 16);
|
|
33
|
+
const b = parseInt(hex.slice(5, 7), 16);
|
|
34
|
+
return `rgba(${r}, ${g}, ${b}, ${safeAlpha})`;
|
|
35
|
+
}
|
|
36
|
+
const rgbMatch = hex.match(/^rgba?\(([^)]+)\)$/i);
|
|
37
|
+
if (rgbMatch) {
|
|
38
|
+
const parts = rgbMatch[1]?.split(",").map((part) => part.trim()) ?? [];
|
|
39
|
+
const r = Number(parts[0] ?? 255);
|
|
40
|
+
const g = Number(parts[1] ?? 255);
|
|
41
|
+
const b = Number(parts[2] ?? 255);
|
|
42
|
+
if (Number.isFinite(r) && Number.isFinite(g) && Number.isFinite(b)) {
|
|
43
|
+
return `rgba(${r}, ${g}, ${b}, ${safeAlpha})`;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return color;
|
|
47
|
+
}
|
|
18
48
|
class CanvasSpectrumVisualizer extends CanvasVisualizer {
|
|
19
49
|
constructor(canvas, player, options = {}) {
|
|
20
50
|
super(canvas, player);
|
|
51
|
+
this.ghostHeights = [];
|
|
21
52
|
this.options = {
|
|
22
53
|
width: options.width ?? canvas.width ?? 640,
|
|
23
54
|
height: options.height ?? canvas.height ?? 160,
|
|
@@ -35,18 +66,50 @@ class CanvasSpectrumVisualizer extends CanvasVisualizer {
|
|
|
35
66
|
return;
|
|
36
67
|
const data = this.player.getSpectrumData();
|
|
37
68
|
const { width, height, background, color, barGap, barWidth } = this.options;
|
|
69
|
+
const style = this.player.getSpectrumStyle();
|
|
70
|
+
const isActive = !this.player.getAudioElement().paused;
|
|
38
71
|
ctx.fillStyle = background;
|
|
39
72
|
ctx.fillRect(0, 0, width, height);
|
|
73
|
+
if (!isActive) {
|
|
74
|
+
this.frameId = requestAnimationFrame(() => this.draw());
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
40
77
|
let x = 0;
|
|
41
78
|
for (let i = 0; i < data.length && x < width; i += 1) {
|
|
42
79
|
const value = (data[i] ?? 0) / 255;
|
|
43
80
|
const barHeight = Math.max(2, value * height);
|
|
44
|
-
|
|
45
|
-
|
|
81
|
+
const baseY = style.inverted ? 0 : height;
|
|
82
|
+
const clampedHeight = barHeight;
|
|
83
|
+
if (style.dualLayer) {
|
|
84
|
+
const previous = this.ghostHeights[i] ?? clampedHeight;
|
|
85
|
+
const ghostHeight = previous * 0.88 + clampedHeight * 0.12;
|
|
86
|
+
this.ghostHeights[i] = ghostHeight;
|
|
87
|
+
ctx.globalAlpha = 0.34;
|
|
88
|
+
this.drawBar(ctx, x, baseY, barWidth, barGap, ghostHeight, color, style);
|
|
89
|
+
}
|
|
90
|
+
ctx.globalAlpha = 0.94;
|
|
91
|
+
this.drawBar(ctx, x, baseY, barWidth, barGap, clampedHeight, color, style);
|
|
46
92
|
x += barWidth + barGap;
|
|
47
93
|
}
|
|
94
|
+
ctx.globalAlpha = 1;
|
|
95
|
+
if (this.ghostHeights.length > data.length) {
|
|
96
|
+
this.ghostHeights = this.ghostHeights.slice(0, data.length);
|
|
97
|
+
}
|
|
48
98
|
this.frameId = requestAnimationFrame(() => this.draw());
|
|
49
99
|
}
|
|
100
|
+
drawBar(ctx, x, baseY, width, gap, height, color, style) {
|
|
101
|
+
const widthFactor = style.barWidth === "thin"
|
|
102
|
+
? 1.9
|
|
103
|
+
: style.barWidth === "medium"
|
|
104
|
+
? 5.7
|
|
105
|
+
: 17.1;
|
|
106
|
+
const maxDrawWidth = Math.max(1, width + gap - 1.8);
|
|
107
|
+
const drawWidth = Math.max(1, Math.min(width * widthFactor, maxDrawWidth));
|
|
108
|
+
const drawX = x - (drawWidth - width) / 2;
|
|
109
|
+
const topY = style.inverted ? baseY : baseY - height;
|
|
110
|
+
ctx.fillStyle = color;
|
|
111
|
+
ctx.fillRect(drawX, topY, drawWidth, height);
|
|
112
|
+
}
|
|
50
113
|
}
|
|
51
114
|
exports.CanvasSpectrumVisualizer = CanvasSpectrumVisualizer;
|
|
52
115
|
class CanvasWaveformVisualizer extends CanvasVisualizer {
|
|
@@ -68,8 +131,13 @@ class CanvasWaveformVisualizer extends CanvasVisualizer {
|
|
|
68
131
|
return;
|
|
69
132
|
const data = this.player.getWaveformData();
|
|
70
133
|
const { width, height, background, color, lineWidth } = this.options;
|
|
134
|
+
const isActive = !this.player.getAudioElement().paused;
|
|
71
135
|
ctx.fillStyle = background;
|
|
72
136
|
ctx.fillRect(0, 0, width, height);
|
|
137
|
+
if (!isActive) {
|
|
138
|
+
this.frameId = requestAnimationFrame(() => this.draw());
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
73
141
|
ctx.lineWidth = lineWidth;
|
|
74
142
|
ctx.strokeStyle = color;
|
|
75
143
|
ctx.beginPath();
|
|
@@ -114,6 +182,7 @@ class CanvasRadialVisualizer extends CanvasVisualizer {
|
|
|
114
182
|
if (!ctx)
|
|
115
183
|
return;
|
|
116
184
|
const { width, height, background, color, mode, innerRadiusRatio, outerRadiusRatio, lineWidth, waveformAmplitude } = this.options;
|
|
185
|
+
const isActive = !this.player.getAudioElement().paused;
|
|
117
186
|
const centerX = width / 2;
|
|
118
187
|
const centerY = height / 2;
|
|
119
188
|
const maxRadius = Math.min(width, height) / 2;
|
|
@@ -127,42 +196,80 @@ class CanvasRadialVisualizer extends CanvasVisualizer {
|
|
|
127
196
|
ctx.fillStyle = background;
|
|
128
197
|
ctx.fillRect(0, 0, width, height);
|
|
129
198
|
}
|
|
199
|
+
if (!isActive) {
|
|
200
|
+
this.frameId = requestAnimationFrame(() => this.draw());
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
130
203
|
if (mode === "spectrum") {
|
|
131
204
|
const data = this.player.getSpectrumData();
|
|
132
|
-
const
|
|
205
|
+
const spectrumStyle = this.player.getSpectrumStyle();
|
|
206
|
+
const sampleCount = Math.min(spectrumStyle.barWidth === "thin"
|
|
207
|
+
? 136
|
|
208
|
+
: spectrumStyle.barWidth === "medium"
|
|
209
|
+
? 114
|
|
210
|
+
: 92, data.length);
|
|
133
211
|
const step = (Math.PI * 2) / sampleCount;
|
|
134
|
-
|
|
212
|
+
const widthFactor = spectrumStyle.barWidth === "thin"
|
|
213
|
+
? 2.05
|
|
214
|
+
: spectrumStyle.barWidth === "medium"
|
|
215
|
+
? 6.15
|
|
216
|
+
: 18.45;
|
|
217
|
+
const ringCircumference = Math.PI * 2 * outerRadius;
|
|
218
|
+
const targetGap = spectrumStyle.barWidth === "thin"
|
|
219
|
+
? 2.2
|
|
220
|
+
: spectrumStyle.barWidth === "medium"
|
|
221
|
+
? 4.2
|
|
222
|
+
: 6.8;
|
|
223
|
+
const maxStrokeForGap = Math.max(1, ringCircumference / sampleCount - targetGap);
|
|
224
|
+
ctx.lineWidth = Math.min(lineWidth * widthFactor, maxStrokeForGap);
|
|
225
|
+
ctx.lineCap = "round";
|
|
135
226
|
ctx.strokeStyle = color;
|
|
136
|
-
ctx.shadowColor = color;
|
|
137
|
-
ctx.shadowBlur = 8;
|
|
138
227
|
for (let i = 0; i < sampleCount; i += 1) {
|
|
139
228
|
const value = (data[i] ?? 0) / 255;
|
|
140
229
|
const amplitude = Math.max(0.04, value);
|
|
141
230
|
const angle = i * step - Math.PI / 2;
|
|
142
|
-
const
|
|
143
|
-
const
|
|
144
|
-
const
|
|
145
|
-
const
|
|
231
|
+
const outwardStart = innerRadius;
|
|
232
|
+
const outwardEnd = outwardStart + amplitude * radialRange;
|
|
233
|
+
const inwardStart = outerRadius;
|
|
234
|
+
const inwardEnd = Math.max(innerRadius, inwardStart - amplitude * radialRange);
|
|
235
|
+
const startRadius = spectrumStyle.inverted ? inwardStart : outwardStart;
|
|
236
|
+
const primaryEndRadius = spectrumStyle.inverted ? inwardEnd : outwardEnd;
|
|
237
|
+
const x0 = centerX + Math.cos(angle) * startRadius;
|
|
238
|
+
const y0 = centerY + Math.sin(angle) * startRadius;
|
|
239
|
+
const x1 = centerX + Math.cos(angle) * primaryEndRadius;
|
|
240
|
+
const y1 = centerY + Math.sin(angle) * primaryEndRadius;
|
|
146
241
|
ctx.globalAlpha = 0.25 + amplitude * 0.75;
|
|
147
242
|
ctx.beginPath();
|
|
148
243
|
ctx.moveTo(x0, y0);
|
|
149
244
|
ctx.lineTo(x1, y1);
|
|
150
245
|
ctx.stroke();
|
|
246
|
+
if (spectrumStyle.dualLayer) {
|
|
247
|
+
const altStart = spectrumStyle.inverted ? outwardStart : inwardStart;
|
|
248
|
+
const altEnd = spectrumStyle.inverted ? outwardEnd : inwardEnd;
|
|
249
|
+
const inverseAngle = -i * step - Math.PI / 2;
|
|
250
|
+
const ax0 = centerX + Math.cos(inverseAngle) * altStart;
|
|
251
|
+
const ay0 = centerY + Math.sin(inverseAngle) * altStart;
|
|
252
|
+
const ax1 = centerX + Math.cos(inverseAngle) * altEnd;
|
|
253
|
+
const ay1 = centerY + Math.sin(inverseAngle) * altEnd;
|
|
254
|
+
ctx.globalAlpha = 0.18 + amplitude * 0.52;
|
|
255
|
+
ctx.beginPath();
|
|
256
|
+
ctx.moveTo(ax0, ay0);
|
|
257
|
+
ctx.lineTo(ax1, ay1);
|
|
258
|
+
ctx.stroke();
|
|
259
|
+
}
|
|
151
260
|
}
|
|
152
|
-
ctx.shadowBlur = 0;
|
|
153
261
|
ctx.globalAlpha = 1;
|
|
154
262
|
this.frameId = requestAnimationFrame(() => this.draw());
|
|
155
263
|
return;
|
|
156
264
|
}
|
|
157
265
|
const data = this.player.getWaveformData();
|
|
266
|
+
const waveformStyle = this.player.getWaveformStyle();
|
|
158
267
|
const sampleCount = Math.min(220, data.length);
|
|
159
268
|
const step = (Math.PI * 2) / sampleCount;
|
|
160
269
|
const amplitudeRange = radialRange * waveformAmplitude;
|
|
161
270
|
const baseRadius = innerRadius + radialRange * 0.5;
|
|
162
|
-
ctx.lineWidth = lineWidth;
|
|
271
|
+
ctx.lineWidth = waveformStyle.thickLine ? lineWidth * 2.25 : lineWidth;
|
|
163
272
|
ctx.strokeStyle = color;
|
|
164
|
-
ctx.shadowColor = color;
|
|
165
|
-
ctx.shadowBlur = 10;
|
|
166
273
|
ctx.globalAlpha = 0.95;
|
|
167
274
|
ctx.beginPath();
|
|
168
275
|
for (let i = 0; i < sampleCount; i += 1) {
|
|
@@ -180,7 +287,35 @@ class CanvasRadialVisualizer extends CanvasVisualizer {
|
|
|
180
287
|
}
|
|
181
288
|
ctx.closePath();
|
|
182
289
|
ctx.stroke();
|
|
183
|
-
|
|
290
|
+
if (waveformStyle.fill) {
|
|
291
|
+
const fillGradient = ctx.createRadialGradient(centerX, centerY, Math.max(1, innerRadius * 0.12), centerX, centerY, Math.max(innerRadius + radialRange * 0.65, 2));
|
|
292
|
+
fillGradient.addColorStop(0, withAlpha(color, 0));
|
|
293
|
+
fillGradient.addColorStop(0.58, withAlpha(color, 0.08));
|
|
294
|
+
fillGradient.addColorStop(1, withAlpha(color, 0.24));
|
|
295
|
+
ctx.globalAlpha = 1;
|
|
296
|
+
ctx.fillStyle = fillGradient;
|
|
297
|
+
ctx.fill();
|
|
298
|
+
}
|
|
299
|
+
if (waveformStyle.doubleLine) {
|
|
300
|
+
ctx.globalAlpha = 0.42;
|
|
301
|
+
ctx.lineWidth = Math.max(1, ctx.lineWidth * 0.72);
|
|
302
|
+
ctx.beginPath();
|
|
303
|
+
for (let i = 0; i < sampleCount; i += 1) {
|
|
304
|
+
const normalized = ((data[i] ?? 128) - 128) / 128;
|
|
305
|
+
const r = baseRadius + normalized * amplitudeRange * 0.72 + radialRange * 0.1;
|
|
306
|
+
const angle = i * step - Math.PI / 2;
|
|
307
|
+
const x = centerX + Math.cos(angle) * r;
|
|
308
|
+
const y = centerY + Math.sin(angle) * r;
|
|
309
|
+
if (i === 0) {
|
|
310
|
+
ctx.moveTo(x, y);
|
|
311
|
+
}
|
|
312
|
+
else {
|
|
313
|
+
ctx.lineTo(x, y);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
ctx.closePath();
|
|
317
|
+
ctx.stroke();
|
|
318
|
+
}
|
|
184
319
|
ctx.globalAlpha = 1;
|
|
185
320
|
this.frameId = requestAnimationFrame(() => this.draw());
|
|
186
321
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "smooth-player",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "Typed audio player with spectrum and waveform analyzers",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"workspaces": [
|
|
@@ -26,17 +26,21 @@
|
|
|
26
26
|
"assets"
|
|
27
27
|
],
|
|
28
28
|
"scripts": {
|
|
29
|
+
"i18n:sync": "node scripts/sync-i18n.mjs",
|
|
29
30
|
"clean": "rm -rf dist dist-cjs",
|
|
30
31
|
"build:esm": "tsc -p tsconfig.build.esm.json",
|
|
31
32
|
"build:cjs": "tsc -p tsconfig.build.cjs.json",
|
|
32
33
|
"build:types": "tsc -p tsconfig.build.json",
|
|
33
34
|
"build:css": "sass --no-source-map styles/index.scss dist/smooth-player.css",
|
|
34
|
-
"build": "npm run clean && npm run build:esm && npm run build:cjs && npm run build:types && npm run build:css",
|
|
35
|
+
"build": "npm run i18n:sync && npm run clean && npm run build:esm && npm run build:cjs && npm run build:types && npm run build:css",
|
|
35
36
|
"dev": "tsc -p tsconfig.build.esm.json --watch",
|
|
36
|
-
"typecheck": "tsc --noEmit",
|
|
37
|
+
"typecheck": "npm run i18n:sync && tsc --noEmit",
|
|
37
38
|
"demo": "npm run build && node scripts/demo-server.mjs",
|
|
38
39
|
"workspaces:build": "npm run -ws --if-present build",
|
|
39
40
|
"workspaces:typecheck": "npm run -ws --if-present typecheck",
|
|
41
|
+
"release:patch": "npm version patch --workspaces --include-workspace-root --no-git-tag-version",
|
|
42
|
+
"release:minor": "npm version minor --workspaces --include-workspace-root --no-git-tag-version",
|
|
43
|
+
"release:major": "npm version major --workspaces --include-workspace-root --no-git-tag-version",
|
|
40
44
|
"publish:core": "npm publish",
|
|
41
45
|
"publish:react": "npm publish -w @smooth-player/react --access public",
|
|
42
46
|
"publish:vue": "npm publish -w @smooth-player/vue --access public",
|
|
@@ -50,6 +54,14 @@
|
|
|
50
54
|
"web-audio"
|
|
51
55
|
],
|
|
52
56
|
"license": "MIT",
|
|
57
|
+
"repository": {
|
|
58
|
+
"type": "git",
|
|
59
|
+
"url": "https://github.com/marlenesco/smooth-player"
|
|
60
|
+
},
|
|
61
|
+
"homepage": "https://github.com/marlenesco/smooth-player",
|
|
62
|
+
"bugs": {
|
|
63
|
+
"url": "https://github.com/marlenesco/smooth-player/issues"
|
|
64
|
+
},
|
|
53
65
|
"devDependencies": {
|
|
54
66
|
"sass": "^1.93.2",
|
|
55
67
|
"typescript": "^5.7.3"
|