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.
@@ -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
- ctx.fillStyle = color;
45
- ctx.fillRect(x, height - barHeight, barWidth, barHeight);
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 sampleCount = Math.min(160, data.length);
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
- ctx.lineWidth = lineWidth;
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 x0 = centerX + Math.cos(angle) * innerRadius;
143
- const y0 = centerY + Math.sin(angle) * innerRadius;
144
- const x1 = centerX + Math.cos(angle) * (innerRadius + amplitude * radialRange);
145
- const y1 = centerY + Math.sin(angle) * (innerRadius + amplitude * radialRange);
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
- ctx.shadowBlur = 0;
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": "1.0.1",
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"