smooth-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/README.md +211 -0
- package/assets/icons/menu.svg +5 -0
- package/assets/icons/next.svg +4 -0
- package/assets/icons/note.svg +3 -0
- package/assets/icons/pause.svg +4 -0
- package/assets/icons/play.svg +3 -0
- package/assets/icons/playlist.svg +7 -0
- package/assets/icons/prev.svg +4 -0
- package/assets/icons/shuffle.svg +7 -0
- package/dist/SmoothPlayer.d.ts +91 -0
- package/dist/SmoothPlayer.js +930 -0
- package/dist/events.d.ts +7 -0
- package/dist/events.js +20 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +3 -0
- package/dist/smooth-player.css +675 -0
- package/dist/types.d.ts +170 -0
- package/dist/types.js +1 -0
- package/dist/ui.d.ts +3 -0
- package/dist/ui.js +119 -0
- package/dist/visualizers.d.ts +46 -0
- package/dist/visualizers.js +182 -0
- package/dist-cjs/SmoothPlayer.js +934 -0
- package/dist-cjs/events.js +24 -0
- package/dist-cjs/index.js +11 -0
- package/dist-cjs/types.js +2 -0
- package/dist-cjs/ui.js +122 -0
- package/dist-cjs/visualizers.js +188 -0
- package/package.json +50 -0
- package/styles/common/_base.scss +487 -0
- package/styles/index.scss +2 -0
- package/styles/themes/_aurora.scss +70 -0
- package/styles/themes/_nocturne.scss +259 -0
- package/styles/themes/_ocean.scss +13 -0
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
export type VisualizerMode = "spectrum" | "waveform" | "none";
|
|
2
|
+
export interface TrackMetadata {
|
|
3
|
+
title?: string;
|
|
4
|
+
artist?: string;
|
|
5
|
+
album?: string;
|
|
6
|
+
albumArtUrl?: string;
|
|
7
|
+
year?: string | number;
|
|
8
|
+
genre?: string;
|
|
9
|
+
[key: string]: unknown;
|
|
10
|
+
}
|
|
11
|
+
export interface AudioTrack {
|
|
12
|
+
id: string;
|
|
13
|
+
src: string;
|
|
14
|
+
type?: string;
|
|
15
|
+
metadata?: TrackMetadata;
|
|
16
|
+
}
|
|
17
|
+
export interface AudioPlaylist {
|
|
18
|
+
id: string;
|
|
19
|
+
title: string;
|
|
20
|
+
tracks: PlaylistEntry[];
|
|
21
|
+
}
|
|
22
|
+
export type PlaylistEntry = AudioTrack | AudioPlaylist;
|
|
23
|
+
export interface AnalyzerOptions {
|
|
24
|
+
fftSize?: number;
|
|
25
|
+
smoothingTimeConstant?: number;
|
|
26
|
+
minDecibels?: number;
|
|
27
|
+
maxDecibels?: number;
|
|
28
|
+
}
|
|
29
|
+
export interface PlaylistMountOptions {
|
|
30
|
+
listRole?: string;
|
|
31
|
+
itemClassName?: string;
|
|
32
|
+
titleClassName?: string;
|
|
33
|
+
artistClassName?: string;
|
|
34
|
+
selectedAriaAttr?: string;
|
|
35
|
+
getTitle?: (track: AudioTrack, index: number) => string;
|
|
36
|
+
getArtist?: (track: AudioTrack, index: number) => string;
|
|
37
|
+
onSelect?: (payload: {
|
|
38
|
+
index: number;
|
|
39
|
+
track: AudioTrack;
|
|
40
|
+
}) => void;
|
|
41
|
+
}
|
|
42
|
+
export interface PlaylistSwitcherMountOptions {
|
|
43
|
+
itemClassName?: string;
|
|
44
|
+
activeClassName?: string;
|
|
45
|
+
onSelect?: (payload: {
|
|
46
|
+
id: string;
|
|
47
|
+
title: string;
|
|
48
|
+
}) => void;
|
|
49
|
+
}
|
|
50
|
+
export interface TrackInfoMountOptions {
|
|
51
|
+
unknownTitle?: string;
|
|
52
|
+
unknownArtist?: string;
|
|
53
|
+
}
|
|
54
|
+
export interface PlaylistTitleMountOptions {
|
|
55
|
+
fallbackTitle?: string;
|
|
56
|
+
}
|
|
57
|
+
export interface PlayButtonMountOptions {
|
|
58
|
+
labelElement?: HTMLElement | null;
|
|
59
|
+
playLabel?: string;
|
|
60
|
+
pauseLabel?: string;
|
|
61
|
+
}
|
|
62
|
+
export interface ProgressMountOptions {
|
|
63
|
+
range: HTMLInputElement;
|
|
64
|
+
currentTimeElement?: HTMLElement | null;
|
|
65
|
+
durationElement?: HTMLElement | null;
|
|
66
|
+
progressRoot?: HTMLElement | null;
|
|
67
|
+
ringElement?: HTMLElement | null;
|
|
68
|
+
}
|
|
69
|
+
export interface TransportControlsMountOptions {
|
|
70
|
+
previousButton: HTMLElement;
|
|
71
|
+
nextButton: HTMLElement;
|
|
72
|
+
}
|
|
73
|
+
export interface ShuffleToggleMountOptions {
|
|
74
|
+
button: HTMLButtonElement;
|
|
75
|
+
labelElement?: HTMLElement | null;
|
|
76
|
+
activeClassName?: string;
|
|
77
|
+
enabledLabel?: string;
|
|
78
|
+
disabledLabel?: string;
|
|
79
|
+
initialEnabled?: boolean;
|
|
80
|
+
}
|
|
81
|
+
export interface PlaylistPanelMountOptions {
|
|
82
|
+
root: HTMLElement;
|
|
83
|
+
toggleButton: HTMLButtonElement;
|
|
84
|
+
panel: HTMLElement;
|
|
85
|
+
closeButton?: HTMLElement | null;
|
|
86
|
+
openClassName?: string;
|
|
87
|
+
openLabel?: string;
|
|
88
|
+
closeLabel?: string;
|
|
89
|
+
}
|
|
90
|
+
export interface PlaylistPanelController {
|
|
91
|
+
destroy: () => void;
|
|
92
|
+
getOpen: () => boolean;
|
|
93
|
+
setOpen: (open: boolean) => void;
|
|
94
|
+
}
|
|
95
|
+
export interface DebugPanelMountOptions {
|
|
96
|
+
enabled?: boolean;
|
|
97
|
+
panel: HTMLElement;
|
|
98
|
+
sourceElement: HTMLElement;
|
|
99
|
+
currentTimeElement: HTMLElement;
|
|
100
|
+
durationElement: HTMLElement;
|
|
101
|
+
readyStateElement: HTMLElement;
|
|
102
|
+
networkStateElement: HTMLElement;
|
|
103
|
+
pausedElement: HTMLElement;
|
|
104
|
+
eventsElement: HTMLElement;
|
|
105
|
+
maxEvents?: number;
|
|
106
|
+
}
|
|
107
|
+
export interface StandardPlayerUIMountOptions {
|
|
108
|
+
debugEnabled?: boolean;
|
|
109
|
+
}
|
|
110
|
+
export interface StandardPlayerUIController {
|
|
111
|
+
destroy: () => void;
|
|
112
|
+
rebuildVisualizer: () => void;
|
|
113
|
+
}
|
|
114
|
+
export interface SmoothPlayerOptions {
|
|
115
|
+
audio?: HTMLAudioElement;
|
|
116
|
+
autoplay?: boolean;
|
|
117
|
+
loop?: boolean;
|
|
118
|
+
debug?: boolean;
|
|
119
|
+
crossOrigin?: HTMLMediaElement["crossOrigin"];
|
|
120
|
+
playlist?: PlaylistEntry[];
|
|
121
|
+
initialVolume?: number;
|
|
122
|
+
initialTrackIndex?: number;
|
|
123
|
+
accentColor?: string;
|
|
124
|
+
analyzer?: AnalyzerOptions;
|
|
125
|
+
visualizer?: VisualizerMode;
|
|
126
|
+
initialShuffle?: boolean;
|
|
127
|
+
durationFallback?: boolean;
|
|
128
|
+
}
|
|
129
|
+
export interface PlaybackState {
|
|
130
|
+
currentTrackIndex: number;
|
|
131
|
+
isPlaying: boolean;
|
|
132
|
+
duration: number;
|
|
133
|
+
currentTime: number;
|
|
134
|
+
volume: number;
|
|
135
|
+
loop: boolean;
|
|
136
|
+
playlistId: string | null;
|
|
137
|
+
playlistTitle: string;
|
|
138
|
+
playlistCount: number;
|
|
139
|
+
visualizer: VisualizerMode;
|
|
140
|
+
accentColor: string;
|
|
141
|
+
shuffle: boolean;
|
|
142
|
+
}
|
|
143
|
+
export interface PlayerEvents {
|
|
144
|
+
ready: undefined;
|
|
145
|
+
play: undefined;
|
|
146
|
+
pause: undefined;
|
|
147
|
+
ended: undefined;
|
|
148
|
+
playlistchange: {
|
|
149
|
+
id: string | null;
|
|
150
|
+
title: string;
|
|
151
|
+
index: number;
|
|
152
|
+
};
|
|
153
|
+
trackchange: {
|
|
154
|
+
index: number;
|
|
155
|
+
track: AudioTrack | null;
|
|
156
|
+
};
|
|
157
|
+
durationchange: {
|
|
158
|
+
duration: number;
|
|
159
|
+
};
|
|
160
|
+
timeupdate: {
|
|
161
|
+
currentTime: number;
|
|
162
|
+
duration: number;
|
|
163
|
+
};
|
|
164
|
+
volumechange: {
|
|
165
|
+
volume: number;
|
|
166
|
+
};
|
|
167
|
+
error: {
|
|
168
|
+
error: Error;
|
|
169
|
+
};
|
|
170
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/ui.d.ts
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { SmoothPlayer } from "./SmoothPlayer.js";
|
|
2
|
+
import { type StandardPlayerUIController, type StandardPlayerUIMountOptions } from "./types.js";
|
|
3
|
+
export declare function mountStandardPlayerUI(player: SmoothPlayer, root: HTMLElement, options?: StandardPlayerUIMountOptions): StandardPlayerUIController;
|
package/dist/ui.js
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { CanvasRadialVisualizer } from "./visualizers.js";
|
|
2
|
+
function requiredElement(scope, selector) {
|
|
3
|
+
const element = scope.querySelector(selector);
|
|
4
|
+
if (!element) {
|
|
5
|
+
throw new Error(`Missing required element: ${selector}`);
|
|
6
|
+
}
|
|
7
|
+
return element;
|
|
8
|
+
}
|
|
9
|
+
export function mountStandardPlayerUI(player, root, options = {}) {
|
|
10
|
+
const doc = root.ownerDocument ?? document;
|
|
11
|
+
const debugEnabled = options.debugEnabled ?? player.getDebug();
|
|
12
|
+
const title = requiredElement(root, "#title");
|
|
13
|
+
const artist = requiredElement(root, "#artist");
|
|
14
|
+
const playlistTitle = requiredElement(root, ".smooth-player__top-title");
|
|
15
|
+
const progress = requiredElement(root, "#progress");
|
|
16
|
+
const timeCurrent = requiredElement(root, "#time-current");
|
|
17
|
+
const timeDuration = requiredElement(root, "#time-duration");
|
|
18
|
+
const playButton = requiredElement(root, "#play");
|
|
19
|
+
const playText = requiredElement(root, "#play-text");
|
|
20
|
+
const prevButton = requiredElement(root, "#prev");
|
|
21
|
+
const nextButton = requiredElement(root, "#next");
|
|
22
|
+
const playlistToggle = requiredElement(root, "#playlist-toggle");
|
|
23
|
+
const shuffleToggle = requiredElement(root, "#shuffle-toggle");
|
|
24
|
+
const shuffleText = requiredElement(root, "#shuffle-text");
|
|
25
|
+
const playlistPanel = requiredElement(root, "#playlist-panel");
|
|
26
|
+
const playlistHead = requiredElement(root, ".smooth-player__playlist-head");
|
|
27
|
+
const playlistClose = requiredElement(root, "#playlist-close");
|
|
28
|
+
const playlistList = requiredElement(root, "#playlist-list");
|
|
29
|
+
const radialCanvas = requiredElement(root, "#radial-visualizer");
|
|
30
|
+
const progressRing = requiredElement(root, "#progress-ring");
|
|
31
|
+
const unmounts = [];
|
|
32
|
+
let radial = null;
|
|
33
|
+
const rebuildVisualizer = () => {
|
|
34
|
+
radial?.stop();
|
|
35
|
+
const mode = player.getVisualizer();
|
|
36
|
+
radialCanvas.hidden = mode === "none";
|
|
37
|
+
if (mode === "none") {
|
|
38
|
+
radial = null;
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
radial = new CanvasRadialVisualizer(radialCanvas, player, {
|
|
42
|
+
mode,
|
|
43
|
+
color: player.getAccentColor(),
|
|
44
|
+
background: "transparent",
|
|
45
|
+
});
|
|
46
|
+
radial.start();
|
|
47
|
+
};
|
|
48
|
+
const playlistPanelController = player.mountPlaylistPanel({
|
|
49
|
+
root,
|
|
50
|
+
toggleButton: playlistToggle,
|
|
51
|
+
panel: playlistPanel,
|
|
52
|
+
closeButton: playlistClose,
|
|
53
|
+
});
|
|
54
|
+
unmounts.push(() => playlistPanelController.destroy());
|
|
55
|
+
unmounts.push(player.mountPlaylistTitle(playlistTitle));
|
|
56
|
+
unmounts.push(player.mountShuffleToggle({
|
|
57
|
+
button: shuffleToggle,
|
|
58
|
+
labelElement: shuffleText,
|
|
59
|
+
initialEnabled: false,
|
|
60
|
+
}));
|
|
61
|
+
unmounts.push(player.mountTransportControls({
|
|
62
|
+
previousButton: prevButton,
|
|
63
|
+
nextButton: nextButton,
|
|
64
|
+
}));
|
|
65
|
+
if (debugEnabled) {
|
|
66
|
+
const debugPanel = requiredElement(doc, "#debug-panel");
|
|
67
|
+
const dbgSrc = requiredElement(doc, "#dbg-src");
|
|
68
|
+
const dbgCurrentTime = requiredElement(doc, "#dbg-current-time");
|
|
69
|
+
const dbgDuration = requiredElement(doc, "#dbg-duration");
|
|
70
|
+
const dbgReadyState = requiredElement(doc, "#dbg-ready-state");
|
|
71
|
+
const dbgNetworkState = requiredElement(doc, "#dbg-network-state");
|
|
72
|
+
const dbgPaused = requiredElement(doc, "#dbg-paused");
|
|
73
|
+
const dbgEvents = requiredElement(doc, "#dbg-events");
|
|
74
|
+
unmounts.push(player.mountDebugPanel({
|
|
75
|
+
enabled: true,
|
|
76
|
+
panel: debugPanel,
|
|
77
|
+
sourceElement: dbgSrc,
|
|
78
|
+
currentTimeElement: dbgCurrentTime,
|
|
79
|
+
durationElement: dbgDuration,
|
|
80
|
+
readyStateElement: dbgReadyState,
|
|
81
|
+
networkStateElement: dbgNetworkState,
|
|
82
|
+
pausedElement: dbgPaused,
|
|
83
|
+
eventsElement: dbgEvents,
|
|
84
|
+
}));
|
|
85
|
+
}
|
|
86
|
+
player.applyAccentColor(root);
|
|
87
|
+
unmounts.push(player.mountTrackInfo(title, artist));
|
|
88
|
+
unmounts.push(player.mountPlayButton(playButton, {
|
|
89
|
+
labelElement: playText,
|
|
90
|
+
playLabel: "Riproduci",
|
|
91
|
+
pauseLabel: "Pausa",
|
|
92
|
+
}));
|
|
93
|
+
unmounts.push(player.mountProgress({
|
|
94
|
+
range: progress,
|
|
95
|
+
currentTimeElement: timeCurrent,
|
|
96
|
+
durationElement: timeDuration,
|
|
97
|
+
progressRoot: root,
|
|
98
|
+
ringElement: progressRing,
|
|
99
|
+
}));
|
|
100
|
+
unmounts.push(player.mountPlaylist(playlistList, {
|
|
101
|
+
onSelect: () => playlistPanelController.setOpen(false),
|
|
102
|
+
}));
|
|
103
|
+
const switcher = doc.createElement("div");
|
|
104
|
+
switcher.className = "smooth-player__playlist-switcher";
|
|
105
|
+
playlistHead.insertAdjacentElement("afterend", switcher);
|
|
106
|
+
unmounts.push(player.mountPlaylistSwitcher(switcher));
|
|
107
|
+
rebuildVisualizer();
|
|
108
|
+
return {
|
|
109
|
+
rebuildVisualizer,
|
|
110
|
+
destroy: () => {
|
|
111
|
+
radial?.stop();
|
|
112
|
+
radial = null;
|
|
113
|
+
for (const unmount of unmounts) {
|
|
114
|
+
unmount();
|
|
115
|
+
}
|
|
116
|
+
switcher.remove();
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { SmoothPlayer } from "./SmoothPlayer.js";
|
|
2
|
+
interface BaseVisualizerOptions {
|
|
3
|
+
width?: number;
|
|
4
|
+
height?: number;
|
|
5
|
+
background?: string;
|
|
6
|
+
color?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface SpectrumVisualizerOptions extends BaseVisualizerOptions {
|
|
9
|
+
barGap?: number;
|
|
10
|
+
barWidth?: number;
|
|
11
|
+
}
|
|
12
|
+
export interface WaveformVisualizerOptions extends BaseVisualizerOptions {
|
|
13
|
+
lineWidth?: number;
|
|
14
|
+
}
|
|
15
|
+
export interface RadialVisualizerOptions extends BaseVisualizerOptions {
|
|
16
|
+
mode?: "spectrum" | "waveform";
|
|
17
|
+
innerRadiusRatio?: number;
|
|
18
|
+
outerRadiusRatio?: number;
|
|
19
|
+
lineWidth?: number;
|
|
20
|
+
waveformAmplitude?: number;
|
|
21
|
+
}
|
|
22
|
+
declare abstract class CanvasVisualizer {
|
|
23
|
+
protected readonly canvas: HTMLCanvasElement;
|
|
24
|
+
protected readonly player: SmoothPlayer;
|
|
25
|
+
protected frameId: number;
|
|
26
|
+
constructor(canvas: HTMLCanvasElement, player: SmoothPlayer);
|
|
27
|
+
start(): void;
|
|
28
|
+
stop(): void;
|
|
29
|
+
protected abstract draw(): void;
|
|
30
|
+
}
|
|
31
|
+
export declare class CanvasSpectrumVisualizer extends CanvasVisualizer {
|
|
32
|
+
private readonly options;
|
|
33
|
+
constructor(canvas: HTMLCanvasElement, player: SmoothPlayer, options?: SpectrumVisualizerOptions);
|
|
34
|
+
protected draw(): void;
|
|
35
|
+
}
|
|
36
|
+
export declare class CanvasWaveformVisualizer extends CanvasVisualizer {
|
|
37
|
+
private readonly options;
|
|
38
|
+
constructor(canvas: HTMLCanvasElement, player: SmoothPlayer, options?: WaveformVisualizerOptions);
|
|
39
|
+
protected draw(): void;
|
|
40
|
+
}
|
|
41
|
+
export declare class CanvasRadialVisualizer extends CanvasVisualizer {
|
|
42
|
+
private readonly options;
|
|
43
|
+
constructor(canvas: HTMLCanvasElement, player: SmoothPlayer, options?: RadialVisualizerOptions);
|
|
44
|
+
protected draw(): void;
|
|
45
|
+
}
|
|
46
|
+
export {};
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
class CanvasVisualizer {
|
|
2
|
+
constructor(canvas, player) {
|
|
3
|
+
this.canvas = canvas;
|
|
4
|
+
this.player = player;
|
|
5
|
+
this.frameId = 0;
|
|
6
|
+
}
|
|
7
|
+
start() {
|
|
8
|
+
this.stop();
|
|
9
|
+
this.draw();
|
|
10
|
+
}
|
|
11
|
+
stop() {
|
|
12
|
+
cancelAnimationFrame(this.frameId);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export class CanvasSpectrumVisualizer extends CanvasVisualizer {
|
|
16
|
+
constructor(canvas, player, options = {}) {
|
|
17
|
+
super(canvas, player);
|
|
18
|
+
this.options = {
|
|
19
|
+
width: options.width ?? canvas.width ?? 640,
|
|
20
|
+
height: options.height ?? canvas.height ?? 160,
|
|
21
|
+
background: options.background ?? "#0b1220",
|
|
22
|
+
color: options.color ?? "#2db6c8",
|
|
23
|
+
barGap: options.barGap ?? 1,
|
|
24
|
+
barWidth: options.barWidth ?? 3,
|
|
25
|
+
};
|
|
26
|
+
this.canvas.width = this.options.width;
|
|
27
|
+
this.canvas.height = this.options.height;
|
|
28
|
+
}
|
|
29
|
+
draw() {
|
|
30
|
+
const ctx = this.canvas.getContext("2d");
|
|
31
|
+
if (!ctx)
|
|
32
|
+
return;
|
|
33
|
+
const data = this.player.getSpectrumData();
|
|
34
|
+
const { width, height, background, color, barGap, barWidth } = this.options;
|
|
35
|
+
ctx.fillStyle = background;
|
|
36
|
+
ctx.fillRect(0, 0, width, height);
|
|
37
|
+
let x = 0;
|
|
38
|
+
for (let i = 0; i < data.length && x < width; i += 1) {
|
|
39
|
+
const value = (data[i] ?? 0) / 255;
|
|
40
|
+
const barHeight = Math.max(2, value * height);
|
|
41
|
+
ctx.fillStyle = color;
|
|
42
|
+
ctx.fillRect(x, height - barHeight, barWidth, barHeight);
|
|
43
|
+
x += barWidth + barGap;
|
|
44
|
+
}
|
|
45
|
+
this.frameId = requestAnimationFrame(() => this.draw());
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
export class CanvasWaveformVisualizer extends CanvasVisualizer {
|
|
49
|
+
constructor(canvas, player, options = {}) {
|
|
50
|
+
super(canvas, player);
|
|
51
|
+
this.options = {
|
|
52
|
+
width: options.width ?? canvas.width ?? 640,
|
|
53
|
+
height: options.height ?? canvas.height ?? 120,
|
|
54
|
+
background: options.background ?? "#0b1220",
|
|
55
|
+
color: options.color ?? "#f3f5f9",
|
|
56
|
+
lineWidth: options.lineWidth ?? 2,
|
|
57
|
+
};
|
|
58
|
+
this.canvas.width = this.options.width;
|
|
59
|
+
this.canvas.height = this.options.height;
|
|
60
|
+
}
|
|
61
|
+
draw() {
|
|
62
|
+
const ctx = this.canvas.getContext("2d");
|
|
63
|
+
if (!ctx)
|
|
64
|
+
return;
|
|
65
|
+
const data = this.player.getWaveformData();
|
|
66
|
+
const { width, height, background, color, lineWidth } = this.options;
|
|
67
|
+
ctx.fillStyle = background;
|
|
68
|
+
ctx.fillRect(0, 0, width, height);
|
|
69
|
+
ctx.lineWidth = lineWidth;
|
|
70
|
+
ctx.strokeStyle = color;
|
|
71
|
+
ctx.beginPath();
|
|
72
|
+
const sliceWidth = width / data.length;
|
|
73
|
+
let x = 0;
|
|
74
|
+
for (let i = 0; i < data.length; i += 1) {
|
|
75
|
+
const normalized = (data[i] ?? 0) / 128;
|
|
76
|
+
const y = (normalized * height) / 2;
|
|
77
|
+
if (i === 0) {
|
|
78
|
+
ctx.moveTo(x, y);
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
ctx.lineTo(x, y);
|
|
82
|
+
}
|
|
83
|
+
x += sliceWidth;
|
|
84
|
+
}
|
|
85
|
+
ctx.lineTo(width, height / 2);
|
|
86
|
+
ctx.stroke();
|
|
87
|
+
this.frameId = requestAnimationFrame(() => this.draw());
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
export class CanvasRadialVisualizer extends CanvasVisualizer {
|
|
91
|
+
constructor(canvas, player, options = {}) {
|
|
92
|
+
super(canvas, player);
|
|
93
|
+
this.options = {
|
|
94
|
+
width: options.width ?? canvas.width ?? 220,
|
|
95
|
+
height: options.height ?? canvas.height ?? 220,
|
|
96
|
+
background: options.background ?? "transparent",
|
|
97
|
+
color: options.color ?? "#2db6c8",
|
|
98
|
+
mode: options.mode ?? "spectrum",
|
|
99
|
+
innerRadiusRatio: options.innerRadiusRatio ?? 0.36,
|
|
100
|
+
outerRadiusRatio: options.outerRadiusRatio ?? 0.96,
|
|
101
|
+
lineWidth: options.lineWidth ?? 1.4,
|
|
102
|
+
waveformAmplitude: options.waveformAmplitude ?? 0.52,
|
|
103
|
+
};
|
|
104
|
+
this.canvas.width = this.options.width;
|
|
105
|
+
this.canvas.height = this.options.height;
|
|
106
|
+
}
|
|
107
|
+
draw() {
|
|
108
|
+
const ctx = this.canvas.getContext("2d");
|
|
109
|
+
if (!ctx)
|
|
110
|
+
return;
|
|
111
|
+
const { width, height, background, color, mode, innerRadiusRatio, outerRadiusRatio, lineWidth, waveformAmplitude } = this.options;
|
|
112
|
+
const centerX = width / 2;
|
|
113
|
+
const centerY = height / 2;
|
|
114
|
+
const maxRadius = Math.min(width, height) / 2;
|
|
115
|
+
const innerRadius = maxRadius * innerRadiusRatio;
|
|
116
|
+
const outerRadius = maxRadius * outerRadiusRatio;
|
|
117
|
+
const radialRange = Math.max(2, outerRadius - innerRadius);
|
|
118
|
+
if (background === "transparent") {
|
|
119
|
+
ctx.clearRect(0, 0, width, height);
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
ctx.fillStyle = background;
|
|
123
|
+
ctx.fillRect(0, 0, width, height);
|
|
124
|
+
}
|
|
125
|
+
if (mode === "spectrum") {
|
|
126
|
+
const data = this.player.getSpectrumData();
|
|
127
|
+
const sampleCount = Math.min(160, data.length);
|
|
128
|
+
const step = (Math.PI * 2) / sampleCount;
|
|
129
|
+
ctx.lineWidth = lineWidth;
|
|
130
|
+
ctx.strokeStyle = color;
|
|
131
|
+
ctx.shadowColor = color;
|
|
132
|
+
ctx.shadowBlur = 8;
|
|
133
|
+
for (let i = 0; i < sampleCount; i += 1) {
|
|
134
|
+
const value = (data[i] ?? 0) / 255;
|
|
135
|
+
const amplitude = Math.max(0.04, value);
|
|
136
|
+
const angle = i * step - Math.PI / 2;
|
|
137
|
+
const x0 = centerX + Math.cos(angle) * innerRadius;
|
|
138
|
+
const y0 = centerY + Math.sin(angle) * innerRadius;
|
|
139
|
+
const x1 = centerX + Math.cos(angle) * (innerRadius + amplitude * radialRange);
|
|
140
|
+
const y1 = centerY + Math.sin(angle) * (innerRadius + amplitude * radialRange);
|
|
141
|
+
ctx.globalAlpha = 0.25 + amplitude * 0.75;
|
|
142
|
+
ctx.beginPath();
|
|
143
|
+
ctx.moveTo(x0, y0);
|
|
144
|
+
ctx.lineTo(x1, y1);
|
|
145
|
+
ctx.stroke();
|
|
146
|
+
}
|
|
147
|
+
ctx.shadowBlur = 0;
|
|
148
|
+
ctx.globalAlpha = 1;
|
|
149
|
+
this.frameId = requestAnimationFrame(() => this.draw());
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
const data = this.player.getWaveformData();
|
|
153
|
+
const sampleCount = Math.min(220, data.length);
|
|
154
|
+
const step = (Math.PI * 2) / sampleCount;
|
|
155
|
+
const amplitudeRange = radialRange * waveformAmplitude;
|
|
156
|
+
const baseRadius = innerRadius + radialRange * 0.5;
|
|
157
|
+
ctx.lineWidth = lineWidth;
|
|
158
|
+
ctx.strokeStyle = color;
|
|
159
|
+
ctx.shadowColor = color;
|
|
160
|
+
ctx.shadowBlur = 10;
|
|
161
|
+
ctx.globalAlpha = 0.95;
|
|
162
|
+
ctx.beginPath();
|
|
163
|
+
for (let i = 0; i < sampleCount; i += 1) {
|
|
164
|
+
const normalized = ((data[i] ?? 128) - 128) / 128;
|
|
165
|
+
const r = baseRadius + normalized * amplitudeRange;
|
|
166
|
+
const angle = i * step - Math.PI / 2;
|
|
167
|
+
const x = centerX + Math.cos(angle) * r;
|
|
168
|
+
const y = centerY + Math.sin(angle) * r;
|
|
169
|
+
if (i === 0) {
|
|
170
|
+
ctx.moveTo(x, y);
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
ctx.lineTo(x, y);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
ctx.closePath();
|
|
177
|
+
ctx.stroke();
|
|
178
|
+
ctx.shadowBlur = 0;
|
|
179
|
+
ctx.globalAlpha = 1;
|
|
180
|
+
this.frameId = requestAnimationFrame(() => this.draw());
|
|
181
|
+
}
|
|
182
|
+
}
|