diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..a547bf3
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,24 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..cd934a9
--- /dev/null
+++ b/index.html
@@ -0,0 +1,77 @@
+
+
+
+
+
+
+ 音乐播放器 - 波形图可视化
+
+
+
+
+
音乐播放器
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 列表循环
+
+
+
+
+
+ 条形图
+
+
+
+
+
+
+ 50%
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..c4d88b6
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,1105 @@
+{
+ "name": "audio-player",
+ "version": "0.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "audio-player",
+ "version": "0.0.0",
+ "devDependencies": {
+ "vite": "^7.2.4"
+ }
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz",
+ "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz",
+ "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz",
+ "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz",
+ "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz",
+ "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz",
+ "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz",
+ "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz",
+ "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz",
+ "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz",
+ "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz",
+ "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz",
+ "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz",
+ "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz",
+ "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz",
+ "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz",
+ "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz",
+ "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz",
+ "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz",
+ "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz",
+ "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz",
+ "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openharmony-arm64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz",
+ "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz",
+ "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz",
+ "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz",
+ "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz",
+ "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@rollup/rollup-android-arm-eabi": {
+ "version": "4.57.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.0.tgz",
+ "integrity": "sha512-tPgXB6cDTndIe1ah7u6amCI1T0SsnlOuKgg10Xh3uizJk4e5M1JGaUMk7J4ciuAUcFpbOiNhm2XIjP9ON0dUqA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-android-arm64": {
+ "version": "4.57.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.0.tgz",
+ "integrity": "sha512-sa4LyseLLXr1onr97StkU1Nb7fWcg6niokTwEVNOO7awaKaoRObQ54+V/hrF/BP1noMEaaAW6Fg2d/CfLiq3Mg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-arm64": {
+ "version": "4.57.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.0.tgz",
+ "integrity": "sha512-/NNIj9A7yLjKdmkx5dC2XQ9DmjIECpGpwHoGmA5E1AhU0fuICSqSWScPhN1yLCkEdkCwJIDu2xIeLPs60MNIVg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-x64": {
+ "version": "4.57.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.0.tgz",
+ "integrity": "sha512-xoh8abqgPrPYPr7pTYipqnUi1V3em56JzE/HgDgitTqZBZ3yKCWI+7KUkceM6tNweyUKYru1UMi7FC060RyKwA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-arm64": {
+ "version": "4.57.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.0.tgz",
+ "integrity": "sha512-PCkMh7fNahWSbA0OTUQ2OpYHpjZZr0hPr8lId8twD7a7SeWrvT3xJVyza+dQwXSSq4yEQTMoXgNOfMCsn8584g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-x64": {
+ "version": "4.57.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.0.tgz",
+ "integrity": "sha512-1j3stGx+qbhXql4OCDZhnK7b01s6rBKNybfsX+TNrEe9JNq4DLi1yGiR1xW+nL+FNVvI4D02PUnl6gJ/2y6WJA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+ "version": "4.57.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.0.tgz",
+ "integrity": "sha512-eyrr5W08Ms9uM0mLcKfM/Uzx7hjhz2bcjv8P2uynfj0yU8GGPdz8iYrBPhiLOZqahoAMB8ZiolRZPbbU2MAi6Q==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+ "version": "4.57.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.0.tgz",
+ "integrity": "sha512-Xds90ITXJCNyX9pDhqf85MKWUI4lqjiPAipJ8OLp8xqI2Ehk+TCVhF9rvOoN8xTbcafow3QOThkNnrM33uCFQA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
+ "version": "4.57.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.0.tgz",
+ "integrity": "sha512-Xws2KA4CLvZmXjy46SQaXSejuKPhwVdaNinldoYfqruZBaJHqVo6hnRa8SDo9z7PBW5x84SH64+izmldCgbezw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
+ "version": "4.57.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.0.tgz",
+ "integrity": "sha512-hrKXKbX5FdaRJj7lTMusmvKbhMJSGWJ+w++4KmjiDhpTgNlhYobMvKfDoIWecy4O60K6yA4SnztGuNTQF+Lplw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-gnu": {
+ "version": "4.57.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.0.tgz",
+ "integrity": "sha512-6A+nccfSDGKsPm00d3xKcrsBcbqzCTAukjwWK6rbuAnB2bHaL3r9720HBVZ/no7+FhZLz/U3GwwZZEh6tOSI8Q==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-musl": {
+ "version": "4.57.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.0.tgz",
+ "integrity": "sha512-4P1VyYUe6XAJtQH1Hh99THxr0GKMMwIXsRNOceLrJnaHTDgk1FTcTimDgneRJPvB3LqDQxUmroBclQ1S0cIJwQ==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
+ "version": "4.57.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.0.tgz",
+ "integrity": "sha512-8Vv6pLuIZCMcgXre6c3nOPhE0gjz1+nZP6T+hwWjr7sVH8k0jRkH+XnfjjOTglyMBdSKBPPz54/y1gToSKwrSQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-musl": {
+ "version": "4.57.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.0.tgz",
+ "integrity": "sha512-r1te1M0Sm2TBVD/RxBPC6RZVwNqUTwJTA7w+C/IW5v9Ssu6xmxWEi+iJQlpBhtUiT1raJ5b48pI8tBvEjEFnFA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+ "version": "4.57.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.0.tgz",
+ "integrity": "sha512-say0uMU/RaPm3CDQLxUUTF2oNWL8ysvHkAjcCzV2znxBr23kFfaxocS9qJm+NdkRhF8wtdEEAJuYcLPhSPbjuQ==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
+ "version": "4.57.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.0.tgz",
+ "integrity": "sha512-/MU7/HizQGsnBREtRpcSbSV1zfkoxSTR7wLsRmBPQ8FwUj5sykrP1MyJTvsxP5KBq9SyE6kH8UQQQwa0ASeoQQ==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
+ "version": "4.57.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.0.tgz",
+ "integrity": "sha512-Q9eh+gUGILIHEaJf66aF6a414jQbDnn29zeu0eX3dHMuysnhTvsUvZTCAyZ6tJhUjnvzBKE4FtuaYxutxRZpOg==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
+ "version": "4.57.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.0.tgz",
+ "integrity": "sha512-OR5p5yG5OKSxHReWmwvM0P+VTPMwoBS45PXTMYaskKQqybkS3Kmugq1W+YbNWArF8/s7jQScgzXUhArzEQ7x0A==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-musl": {
+ "version": "4.57.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.0.tgz",
+ "integrity": "sha512-XeatKzo4lHDsVEbm1XDHZlhYZZSQYym6dg2X/Ko0kSFgio+KXLsxwJQprnR48GvdIKDOpqWqssC3iBCjoMcMpw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-openbsd-x64": {
+ "version": "4.57.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.0.tgz",
+ "integrity": "sha512-Lu71y78F5qOfYmubYLHPcJm74GZLU6UJ4THkf/a1K7Tz2ycwC2VUbsqbJAXaR6Bx70SRdlVrt2+n5l7F0agTUw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-openharmony-arm64": {
+ "version": "4.57.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.0.tgz",
+ "integrity": "sha512-v5xwKDWcu7qhAEcsUubiav7r+48Uk/ENWdr82MBZZRIm7zThSxCIVDfb3ZeRRq9yqk+oIzMdDo6fCcA5DHfMyA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
+ "version": "4.57.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.0.tgz",
+ "integrity": "sha512-XnaaaSMGSI6Wk8F4KK3QP7GfuuhjGchElsVerCplUuxRIzdvZ7hRBpLR0omCmw+kI2RFJB80nenhOoGXlJ5TfQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
+ "version": "4.57.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.0.tgz",
+ "integrity": "sha512-3K1lP+3BXY4t4VihLw5MEg6IZD3ojSYzqzBG571W3kNQe4G4CcFpSUQVgurYgib5d+YaCjeFow8QivWp8vuSvA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-gnu": {
+ "version": "4.57.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.0.tgz",
+ "integrity": "sha512-MDk610P/vJGc5L5ImE4k5s+GZT3en0KoK1MKPXCRgzmksAMk79j4h3k1IerxTNqwDLxsGxStEZVBqG0gIqZqoA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
+ "version": "4.57.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.0.tgz",
+ "integrity": "sha512-Zv7v6q6aV+VslnpwzqKAmrk5JdVkLUzok2208ZXGipjb+msxBr/fJPZyeEXiFgH7k62Ak0SLIfxQRZQvTuf7rQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/esbuild": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz",
+ "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.27.2",
+ "@esbuild/android-arm": "0.27.2",
+ "@esbuild/android-arm64": "0.27.2",
+ "@esbuild/android-x64": "0.27.2",
+ "@esbuild/darwin-arm64": "0.27.2",
+ "@esbuild/darwin-x64": "0.27.2",
+ "@esbuild/freebsd-arm64": "0.27.2",
+ "@esbuild/freebsd-x64": "0.27.2",
+ "@esbuild/linux-arm": "0.27.2",
+ "@esbuild/linux-arm64": "0.27.2",
+ "@esbuild/linux-ia32": "0.27.2",
+ "@esbuild/linux-loong64": "0.27.2",
+ "@esbuild/linux-mips64el": "0.27.2",
+ "@esbuild/linux-ppc64": "0.27.2",
+ "@esbuild/linux-riscv64": "0.27.2",
+ "@esbuild/linux-s390x": "0.27.2",
+ "@esbuild/linux-x64": "0.27.2",
+ "@esbuild/netbsd-arm64": "0.27.2",
+ "@esbuild/netbsd-x64": "0.27.2",
+ "@esbuild/openbsd-arm64": "0.27.2",
+ "@esbuild/openbsd-x64": "0.27.2",
+ "@esbuild/openharmony-arm64": "0.27.2",
+ "@esbuild/sunos-x64": "0.27.2",
+ "@esbuild/win32-arm64": "0.27.2",
+ "@esbuild/win32-ia32": "0.27.2",
+ "@esbuild/win32-x64": "0.27.2"
+ }
+ },
+ "node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.11",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/picomatch": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
+ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.5.6",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
+ "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.11",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/rollup": {
+ "version": "4.57.0",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.0.tgz",
+ "integrity": "sha512-e5lPJi/aui4TO1LpAXIRLySmwXSE8k3b9zoGfd42p67wzxog4WHjiZF3M2uheQih4DGyc25QEV4yRBbpueNiUA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "1.0.8"
+ },
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "@rollup/rollup-android-arm-eabi": "4.57.0",
+ "@rollup/rollup-android-arm64": "4.57.0",
+ "@rollup/rollup-darwin-arm64": "4.57.0",
+ "@rollup/rollup-darwin-x64": "4.57.0",
+ "@rollup/rollup-freebsd-arm64": "4.57.0",
+ "@rollup/rollup-freebsd-x64": "4.57.0",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.57.0",
+ "@rollup/rollup-linux-arm-musleabihf": "4.57.0",
+ "@rollup/rollup-linux-arm64-gnu": "4.57.0",
+ "@rollup/rollup-linux-arm64-musl": "4.57.0",
+ "@rollup/rollup-linux-loong64-gnu": "4.57.0",
+ "@rollup/rollup-linux-loong64-musl": "4.57.0",
+ "@rollup/rollup-linux-ppc64-gnu": "4.57.0",
+ "@rollup/rollup-linux-ppc64-musl": "4.57.0",
+ "@rollup/rollup-linux-riscv64-gnu": "4.57.0",
+ "@rollup/rollup-linux-riscv64-musl": "4.57.0",
+ "@rollup/rollup-linux-s390x-gnu": "4.57.0",
+ "@rollup/rollup-linux-x64-gnu": "4.57.0",
+ "@rollup/rollup-linux-x64-musl": "4.57.0",
+ "@rollup/rollup-openbsd-x64": "4.57.0",
+ "@rollup/rollup-openharmony-arm64": "4.57.0",
+ "@rollup/rollup-win32-arm64-msvc": "4.57.0",
+ "@rollup/rollup-win32-ia32-msvc": "4.57.0",
+ "@rollup/rollup-win32-x64-gnu": "4.57.0",
+ "@rollup/rollup-win32-x64-msvc": "4.57.0",
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.15",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
+ "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/vite": {
+ "version": "7.3.1",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz",
+ "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "^0.27.0",
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.3",
+ "postcss": "^8.5.6",
+ "rollup": "^4.43.0",
+ "tinyglobby": "^0.2.15"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^20.19.0 || >=22.12.0",
+ "jiti": ">=1.21.0",
+ "less": "^4.0.0",
+ "lightningcss": "^1.21.0",
+ "sass": "^1.70.0",
+ "sass-embedded": "^1.70.0",
+ "stylus": ">=0.54.8",
+ "sugarss": "^5.0.0",
+ "terser": "^5.16.0",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "jiti": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "lightningcss": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
+ }
+ }
+ }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..8c98dd4
--- /dev/null
+++ b/package.json
@@ -0,0 +1,14 @@
+{
+ "name": "audio-player",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "preview": "vite preview"
+ },
+ "devDependencies": {
+ "vite": "^7.2.4"
+ }
+}
diff --git a/public/vite.svg b/public/vite.svg
new file mode 100644
index 0000000..e7b8dfb
--- /dev/null
+++ b/public/vite.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/counter.js b/src/counter.js
new file mode 100644
index 0000000..881e2d7
--- /dev/null
+++ b/src/counter.js
@@ -0,0 +1,9 @@
+export function setupCounter(element) {
+ let counter = 0
+ const setCounter = (count) => {
+ counter = count
+ element.innerHTML = `count is ${counter}`
+ }
+ element.addEventListener('click', () => setCounter(counter + 1))
+ setCounter(0)
+}
diff --git a/src/javascript.svg b/src/javascript.svg
new file mode 100644
index 0000000..f9abb2b
--- /dev/null
+++ b/src/javascript.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/main.js b/src/main.js
new file mode 100644
index 0000000..dfe2177
--- /dev/null
+++ b/src/main.js
@@ -0,0 +1,773 @@
+import './style.css'
+import WaveformVisualizer from './waveformVisualizer.js'
+
+// 音频播放器类
+class AudioPlayer {
+ constructor() {
+ this.audio = document.getElementById('audio');
+ this.canvas = document.getElementById('waveform');
+ this.ctx = this.canvas.getContext('2d');
+ this.audioContext = null;
+ this.analyser = null;
+ this.source = null;
+ this.dataArray = null;
+ this.animationId = null;
+ this.waveformVisualizer = null;
+
+ // 播放列表相关
+ this.playlist = [];
+ this.currentTrackIndex = 0;
+ this.isPlayingFolder = false;
+ this.playbackMode = 'loop'; // 默认列表循环模式:'single', 'loop', 'random'
+
+ // Web Audio API警告标志
+ this.webAudioApiWarningShown = false;
+
+ // 自动播放状态跟踪
+ this.shouldAutoPlay = false;
+
+ this.initializeElements();
+ this.setupEventListeners();
+ this.setupCanvas();
+ this.setupKeyboardShortcuts();
+ this.initializePlayerState();
+ }
+
+ initializeElements() {
+ // 获取所有DOM元素
+ this.fileInput = document.getElementById('audioFile');
+ this.folderInput = document.getElementById('audioFolder');
+ this.playPauseBtn = document.getElementById('playPauseBtn');
+ this.prevBtn = document.getElementById('prevBtn');
+ this.nextBtn = document.getElementById('nextBtn');
+ this.stopBtn = document.getElementById('stopBtn');
+ this.progress = document.getElementById('progress');
+ this.currentTime = document.getElementById('currentTime');
+ this.duration = document.getElementById('duration');
+ this.volumeSlider = document.getElementById('volumeSlider');
+ this.volumeValue = document.getElementById('volumeValue');
+ this.trackName = document.getElementById('trackName');
+ this.muteBtn = document.getElementById('muteBtn');
+ this.visualizationBtn = document.getElementById('visualizationBtn');
+ this.visualizationMode = document.getElementById('visualizationMode');
+ this.playbackModeBtn = document.getElementById('playbackModeBtn');
+ this.playbackModeText = document.getElementById('playbackModeText');
+ this.progressBar = document.getElementById('progressBar');
+ this.progressHandle = document.getElementById('progressHandle');
+
+ // 初始化音频音量和状态
+ this.audio.volume = 0.5; // 设置默认音量为50%
+ this.previousVolume = 0.5; // 用于静音前的音量记忆
+ this.isMuted = false;
+ this.isDraggingProgress = false; // 进度条拖动状态
+ }
+
+ setupCanvas() {
+ // 设置canvas尺寸
+ const rect = this.canvas.getBoundingClientRect();
+ this.canvas.width = rect.width * window.devicePixelRatio;
+ this.canvas.height = rect.height * window.devicePixelRatio;
+ this.ctx.scale(window.devicePixelRatio, window.devicePixelRatio);
+ this.canvas.style.width = rect.width + 'px';
+ this.canvas.style.height = rect.height + 'px';
+ }
+
+ setupEventListeners() {
+ // 文件选择
+ this.fileInput.addEventListener('change', (e) => this.handleFileSelect(e));
+ this.folderInput.addEventListener('change', (e) => this.handleFolderSelect(e));
+
+ // 音频结束事件(用于自动播放下一首)
+ this.audio.addEventListener('ended', () => this.handleTrackEnd());
+
+ // 播放控制
+ this.playPauseBtn.addEventListener('click', () => this.togglePlayPause());
+ this.prevBtn.addEventListener('click', () => this.previousTrack());
+ this.nextBtn.addEventListener('click', () => this.nextTrack());
+ this.stopBtn.addEventListener('click', () => this.stop());
+
+ // 播放模式切换
+ this.playbackModeBtn.addEventListener('click', () => this.togglePlaybackMode());
+
+ // 音频事件
+ this.audio.addEventListener('loadedmetadata', () => this.updateDuration());
+ this.audio.addEventListener('timeupdate', () => this.updateProgress());
+ this.audio.addEventListener('ended', () => this.stop());
+ this.audio.addEventListener('volumechange', () => this.syncVolumeSlider());
+
+ // 音量控制 - 双向联动
+ this.volumeSlider.addEventListener('input', (e) => this.updateVolume(e));
+
+ // 静音按钮
+ this.muteBtn.addEventListener('click', () => this.toggleMute());
+
+ // Canvas点击切换可视化模式(不再用于跳转进度)
+ this.canvas.addEventListener('click', () => this.switchVisualization());
+
+ // 可视化模式切换按钮
+ this.visualizationBtn.addEventListener('click', () => this.switchVisualization());
+
+ // 进度条拖动功能
+ this.setupProgressDrag();
+ }
+
+ setupProgressDrag() {
+ // 进度条拖动功能
+ let isDragging = false;
+
+ const handleProgressDrag = (e) => {
+ if (!this.audio.duration) return;
+
+ const rect = this.progressBar.getBoundingClientRect();
+ const x = e.clientX - rect.left;
+ const percentage = Math.max(0, Math.min(1, x / rect.width));
+ const newTime = percentage * this.audio.duration;
+
+ this.audio.currentTime = newTime;
+ this.updateProgressBar(percentage);
+ };
+
+ const startDrag = (e) => {
+ isDragging = true;
+ this.isDraggingProgress = true;
+ document.addEventListener('mousemove', handleProgressDrag);
+ document.addEventListener('mouseup', stopDrag);
+ handleProgressDrag(e);
+ };
+
+ const stopDrag = () => {
+ isDragging = false;
+ this.isDraggingProgress = false;
+ document.removeEventListener('mousemove', handleProgressDrag);
+ document.removeEventListener('mouseup', stopDrag);
+ };
+
+ // 进度条点击
+ this.progressBar.addEventListener('click', handleProgressDrag);
+
+ // 拖动手柄
+ this.progressHandle.addEventListener('mousedown', startDrag);
+
+ // 防止拖动时选中文本
+ this.progressHandle.addEventListener('selectstart', (e) => e.preventDefault());
+ }
+
+ updateProgressBar(percentage) {
+ // 更新进度条显示(不依赖于timeupdate事件)
+ this.progress.style.width = (percentage * 100) + '%';
+
+ // 更新手柄位置
+ if (this.progressHandle && this.progressBar) {
+ const handleX = percentage * this.progressBar.offsetWidth - 8; // 8是手柄宽度的一半
+ this.progressHandle.style.left = Math.max(-8, Math.min(this.progressBar.offsetWidth - 8, handleX)) + 'px';
+ }
+ }
+
+ handleFileSelect(event) {
+ const file = event.target.files[0];
+ if (file) {
+ // 单个文件播放模式
+ this.isPlayingFolder = false;
+ this.playlist = [];
+ this.currentTrackIndex = 0;
+
+ // 清理旧的URL
+ if (this.audio.src) {
+ URL.revokeObjectURL(this.audio.src);
+ }
+
+ const url = URL.createObjectURL(file);
+ this.audio.src = url;
+ this.trackName.textContent = file.name;
+
+ // 确保音量设置保持一致
+ const currentVolume = this.volumeSlider.value / 100;
+ this.audio.volume = currentVolume;
+
+ // 设置音频上下文(如果尚未初始化)
+ this.setupAudioContext();
+ }
+ }
+
+ handleFolderSelect(event) {
+ const files = Array.from(event.target.files);
+ if (files.length > 0) {
+ // 筛选音频文件 - 支持更多格式
+ const audioExtensions = ['mp3', 'wav', 'ogg', 'm4a', 'flac', 'aac', 'wma', 'opus', 'webm'];
+ const audioFiles = files.filter(file => {
+ const extension = file.name.split('.').pop().toLowerCase();
+ return file.type.startsWith('audio/') || audioExtensions.includes(extension);
+ });
+
+ if (audioFiles.length > 0) {
+ // 按文件名排序
+ audioFiles.sort((a, b) => a.name.localeCompare(b.name));
+
+ this.playlist = audioFiles;
+ this.currentTrackIndex = 0;
+ this.isPlayingFolder = true;
+
+ // 确保播放模式显示正确(默认列表循环)
+ this.playbackMode = 'loop';
+ this.updatePlaybackModeDisplay();
+
+ // 确保音频上下文已初始化
+ this.setupAudioContext();
+
+ // 播放第一首
+ this.loadTrack(0);
+ console.log(`已加载文件夹,共${audioFiles.length}首音乐`);
+
+ // 显示成功提示
+ this.showFolderLoadedFeedback(audioFiles.length);
+ } else {
+ alert('文件夹中没有找到音频文件');
+ }
+ }
+ }
+
+ showFolderLoadedFeedback(count) {
+ const feedback = document.createElement('div');
+ feedback.textContent = `已加载 ${count} 首音乐`;
+ feedback.className = 'visualization-feedback';
+ feedback.style.background = 'rgba(76, 175, 80, 0.9)';
+
+ document.body.appendChild(feedback);
+
+ setTimeout(() => {
+ if (feedback.parentNode) {
+ feedback.parentNode.removeChild(feedback);
+ }
+ }, 2000);
+ }
+
+ loadTrack(index, shouldAutoPlay = true) {
+ if (index < 0 || index >= this.playlist.length) return;
+
+ this.currentTrackIndex = index;
+ const file = this.playlist[index];
+
+ // 停止当前播放和可视化
+ this.stopVisualization();
+
+ // 清理旧的URL
+ if (this.audio.src) {
+ URL.revokeObjectURL(this.audio.src);
+ }
+
+ const url = URL.createObjectURL(file);
+ this.audio.src = url;
+ this.trackName.textContent = `${index + 1}/${this.playlist.length}: ${file.name}`;
+
+ // 确保音量设置保持一致
+ const currentVolume = this.volumeSlider.value / 100;
+ this.audio.volume = currentVolume;
+
+ // 设置音频上下文(如果尚未初始化)
+ this.setupAudioContext();
+
+ this.updateNavigationButtons();
+
+ // 设置自动播放状态
+ this.shouldAutoPlay = shouldAutoPlay;
+
+ // 等待音频数据加载完成后播放
+ const tryAutoPlay = () => {
+ if (this.audio.readyState >= 2 && (!this.audio.paused || this.shouldAutoPlay)) {
+ this.play();
+ }
+ };
+ this.audio.addEventListener('loadedmetadata', tryAutoPlay, { once: true });
+ this.audio.addEventListener('canplay', tryAutoPlay, { once: true });
+ this.audio.addEventListener('loadeddata', tryAutoPlay, { once: true });
+ }
+
+ handleTrackEnd() {
+ if (!this.isPlayingFolder || this.playlist.length === 0) return;
+
+ let nextIndex;
+ switch (this.playbackMode) {
+ case 'single':
+ // 单曲循环
+ nextIndex = this.currentTrackIndex;
+ break;
+ case 'random':
+ // 随机播放
+ nextIndex = Math.floor(Math.random() * this.playlist.length);
+ break;
+ case 'loop':
+ default:
+ // 列表循环
+ nextIndex = (this.currentTrackIndex + 1) % this.playlist.length;
+ break;
+ }
+
+ // 自动播放下一首
+ this.loadTrack(nextIndex, true);
+ }
+
+ nextTrack() {
+ if (!this.isPlayingFolder || this.playlist.length === 0) return;
+
+ const nextIndex = (this.currentTrackIndex + 1) % this.playlist.length;
+ this.loadTrack(nextIndex, true); // 自动播放
+ }
+
+ previousTrack() {
+ if (!this.isPlayingFolder || this.playlist.length === 0) return;
+
+ const prevIndex = this.currentTrackIndex === 0 ? this.playlist.length - 1 : this.currentTrackIndex - 1;
+ this.loadTrack(prevIndex, true); // 自动播放
+ }
+
+ setupAudioContext() {
+ try {
+ // 首次创建音频上下文
+ if (!this.audioContext) {
+ this.audioContext = new (window.AudioContext || window.webkitAudioContext)();
+ this.analyser = this.audioContext.createAnalyser();
+ this.analyser.fftSize = 2048;
+
+ const bufferLength = this.analyser.frequencyBinCount;
+ this.dataArray = new Uint8Array(bufferLength);
+
+ // 初始化高级可视化器
+ this.waveformVisualizer = new WaveformVisualizer(this.canvas, this.audioContext, this.analyser);
+ console.log('音频上下文和可视化器初始化成功');
+ }
+
+ // 连接音频源(只在首次或需要重新连接时)
+ if (this.audioContext && this.analyser && !this.source) {
+ this.source = this.audioContext.createMediaElementSource(this.audio);
+ this.source.connect(this.analyser);
+ this.analyser.connect(this.audioContext.destination);
+ console.log('音频源连接成功');
+ }
+ } catch (error) {
+ console.error('音频上下文初始化失败:', error);
+ if (!this.webAudioApiWarningShown) {
+ alert('您的浏览器不支持Web Audio API,部分功能可能无法正常使用');
+ this.webAudioApiWarningShown = true;
+ }
+ }
+ }
+
+ play() {
+ if (this.audio.src) {
+ // 确保音频上下文处于运行状态
+ if (this.audioContext && this.audioContext.state === 'suspended') {
+ this.audioContext.resume();
+ }
+
+ this.audio.play().then(() => {
+ this.startVisualization();
+ this.updatePlayPauseButton(true);
+ document.querySelector('.audio-player').classList.add('playing');
+ }).catch(error => {
+ console.error('播放失败:', error);
+ alert('播放失败,请检查音频文件');
+ });
+ }
+ }
+
+ pause() {
+ this.audio.pause();
+ this.stopVisualization();
+ this.updatePlayPauseButton(false);
+ document.querySelector('.audio-player').classList.remove('playing');
+ }
+
+ // 移除旧的togglePlayPause方法,因为已经移到了上面
+
+ updatePlayPauseButton(isPlaying) {
+ if (this.playPauseBtn) {
+ this.playPauseBtn.textContent = isPlaying ? '⏸️' : '▶️';
+ }
+ }
+
+ stop() {
+ this.audio.pause();
+ this.audio.currentTime = 0;
+ this.stopVisualization();
+ this.clearCanvas();
+ this.updatePlayPauseButton(false);
+ document.querySelector('.audio-player').classList.remove('playing');
+ }
+
+ updateDuration() {
+ const duration = this.audio.duration;
+ this.duration.textContent = this.formatTime(duration);
+ }
+
+ updateProgress() {
+ // 只在不拖动时更新进度条
+ if (this.isDraggingProgress) return;
+ const currentTime = this.audio.currentTime;
+ const duration = this.audio.duration;
+ const progressPercent = (currentTime / duration) * 100;
+ this.progress.style.width = progressPercent + '%';
+ this.currentTime.textContent = this.formatTime(currentTime);
+ // 更新手柄位置
+ if (this.progressHandle && this.progressBar) {
+ const handleX = (currentTime / duration) * this.progressBar.offsetWidth - 8;
+ this.progressHandle.style.left = Math.max(-8, Math.min(this.progressBar.offsetWidth - 8, handleX)) + 'px';
+ }
+ }
+
+ updateVolume(event) {
+ const volume = event.target.value / 100;
+ this.audio.volume = volume;
+ this.volumeValue.textContent = event.target.value + '%';
+
+ // 更新静音状态
+ if (volume > 0 && this.isMuted) {
+ this.isMuted = false;
+ } else if (volume === 0 && !this.isMuted) {
+ this.isMuted = true;
+ }
+
+ // 如果音量大于0且不是静音状态,保存当前音量
+ if (volume > 0 && !this.isMuted) {
+ this.previousVolume = volume;
+ }
+
+ this.updateVolumeIcon(Math.round(volume * 100));
+ console.log('音量设置为:', volume, '静音状态:', this.isMuted);
+ }
+
+ syncVolumeSlider() {
+ // 同步滑块位置与音频实际音量(处理音频元素自身的音量变化)
+ const currentVolume = Math.round(this.audio.volume * 100);
+ this.volumeSlider.value = currentVolume;
+ this.volumeValue.textContent = currentVolume + '%';
+ this.updateVolumeIcon(currentVolume);
+ }
+
+ updateVolumeIcon(volume) {
+ if (this.muteBtn) {
+ if (volume === 0 || this.isMuted) {
+ this.muteBtn.textContent = '🔇';
+ this.muteBtn.classList.add('muted');
+ } else if (volume < 30) {
+ this.muteBtn.textContent = '🔈';
+ this.muteBtn.classList.remove('muted');
+ } else if (volume < 70) {
+ this.muteBtn.textContent = '🔉';
+ this.muteBtn.classList.remove('muted');
+ } else {
+ this.muteBtn.textContent = '🔊';
+ this.muteBtn.classList.remove('muted');
+ }
+ }
+ }
+
+ toggleMute() {
+ if (this.isMuted) {
+ // 取消静音,恢复到之前的音量
+ this.audio.volume = this.previousVolume;
+ this.volumeSlider.value = Math.round(this.previousVolume * 100);
+ this.volumeValue.textContent = Math.round(this.previousVolume * 100) + '%';
+ this.isMuted = false;
+ } else {
+ // 静音,保存当前音量
+ this.previousVolume = this.audio.volume;
+ this.audio.volume = 0;
+ this.volumeSlider.value = 0;
+ this.volumeValue.textContent = '0%';
+ this.isMuted = true;
+ }
+ this.updateVolumeIcon(this.audio.volume);
+ console.log(this.isMuted ? '已静音' : '取消静音', '音量:', this.audio.volume);
+ }
+
+ formatTime(seconds) {
+ const mins = Math.floor(seconds / 60);
+ const secs = Math.floor(seconds % 60);
+ return `${mins}:${secs.toString().padStart(2, '0')}`;
+ }
+
+ startVisualization() {
+ if (!this.waveformVisualizer) {
+ console.warn('可视化器未初始化,使用备用方案');
+ this.startBasicVisualization();
+ return;
+ }
+
+ const draw = () => {
+ this.animationId = requestAnimationFrame(draw);
+ this.waveformVisualizer.draw();
+ };
+
+ draw();
+ }
+
+ startBasicVisualization() {
+ // 备用基础可视化方案
+ if (!this.analyser) return;
+
+ const draw = () => {
+ this.animationId = requestAnimationFrame(draw);
+
+ this.analyser.getByteFrequencyData(this.dataArray);
+
+ this.ctx.fillStyle = 'rgb(0, 0, 0)';
+ this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
+
+ const barWidth = (this.canvas.width / this.dataArray.length) * 2.5;
+ let barHeight;
+ let x = 0;
+
+ for (let i = 0; i < this.dataArray.length; i++) {
+ barHeight = (this.dataArray[i] / 255) * this.canvas.height;
+
+ const r = barHeight + (25 * (i / this.dataArray.length));
+ const g = 250 * (i / this.dataArray.length);
+ const b = 50;
+
+ this.ctx.fillStyle = `rgb(${r},${g},${b})`;
+ this.ctx.fillRect(x, this.canvas.height - barHeight, barWidth, barHeight);
+
+ x += barWidth + 1;
+ }
+ };
+
+ draw();
+ }
+
+ stopVisualization() {
+ if (this.animationId) {
+ cancelAnimationFrame(this.animationId);
+ this.animationId = null;
+ }
+ }
+
+ clearCanvas() {
+ if (this.waveformVisualizer) {
+ this.ctx.fillStyle = 'rgb(0, 0, 0)';
+ this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
+ }
+ }
+
+ switchVisualization() {
+ if (this.waveformVisualizer) {
+ this.waveformVisualizer.nextVisualization();
+ const currentMode = this.waveformVisualizer.getCurrentVisualization();
+ const modeNames = {
+ 'bars': '条形图',
+ 'wave': '波形图',
+ 'circular': '圆形图'
+ };
+ if (this.visualizationMode) {
+ this.visualizationMode.textContent = modeNames[currentMode] || '条形图';
+ console.log('切换到可视化模式:', currentMode);
+ }
+
+ // 添加视觉反馈
+ this.showVisualizationFeedback(currentMode);
+ } else {
+ console.warn('可视化器未初始化');
+ }
+ }
+
+ showVisualizationFeedback(mode) {
+ // 创建切换反馈效果
+ const feedback = document.createElement('div');
+ feedback.textContent = this.getModeDisplayName(mode);
+ feedback.className = 'visualization-feedback';
+
+ document.body.appendChild(feedback);
+
+ // 1.5秒后移除反馈元素
+ setTimeout(() => {
+ if (feedback.parentNode) {
+ feedback.parentNode.removeChild(feedback);
+ }
+ }, 1500);
+ }
+
+ getModeDisplayName(mode) {
+ const modeNames = {
+ 'bars': '🔊 条形图模式',
+ 'wave': '〰️ 波形图模式',
+ 'circular': '⭕ 圆形图模式'
+ };
+ return modeNames[mode] || '条形图模式';
+ }
+
+ // 键盘快捷键相关方法
+ setupKeyboardShortcuts() {
+ // 键盘快捷键支持
+ document.addEventListener('keydown', (e) => {
+ // 防止在输入框中触发快捷键
+ if (e.target.tagName === 'INPUT') return;
+
+ switch (e.key.toLowerCase()) {
+ case ' ':
+ e.preventDefault();
+ this.togglePlayPause();
+ break;
+ case 'm':
+ this.toggleMute();
+ break;
+ case 'arrowup':
+ e.preventDefault();
+ this.adjustVolume(5);
+ break;
+ case 'arrowdown':
+ e.preventDefault();
+ this.adjustVolume(-5);
+ break;
+ case 'arrowleft':
+ e.preventDefault();
+ if (e.ctrlKey && this.isPlayingFolder) {
+ this.previousTrack();
+ } else {
+ this.skipTime(-5);
+ }
+ break;
+ case 'arrowright':
+ e.preventDefault();
+ if (e.ctrlKey && this.isPlayingFolder) {
+ this.nextTrack();
+ } else {
+ this.skipTime(5);
+ }
+ break;
+ case 'n':
+ if (this.isPlayingFolder) {
+ this.nextTrack();
+ }
+ break;
+ case 'p':
+ if (this.isPlayingFolder) {
+ this.previousTrack();
+ }
+ break;
+ }
+ });
+ }
+
+ initializePlayerState() {
+ // 初始化播放器状态
+ this.updatePlayPauseButton(false);
+ document.querySelector('.audio-player').classList.remove('playing');
+
+ // 初始化播放模式显示(直接设置为列表循环)
+ this.playbackMode = 'loop';
+ this.updatePlaybackModeDisplay();
+
+ // 初始状态下禁用导航按钮
+ this.updateNavigationButtons();
+ }
+
+ updateNavigationButtons() {
+ const hasPlaylist = this.isPlayingFolder && this.playlist.length > 0;
+ this.prevBtn.disabled = !hasPlaylist;
+ this.nextBtn.disabled = !hasPlaylist;
+ this.playbackModeBtn.disabled = !hasPlaylist;
+
+ if (!hasPlaylist) {
+ this.prevBtn.style.opacity = '0.5';
+ this.nextBtn.style.opacity = '0.5';
+ this.playbackModeBtn.style.opacity = '0.5';
+ } else {
+ this.prevBtn.style.opacity = '1';
+ this.nextBtn.style.opacity = '1';
+ this.playbackModeBtn.style.opacity = '1';
+ }
+ }
+
+ togglePlayPause() {
+ if (this.audio.paused) {
+ this.play();
+ } else {
+ this.pause();
+ }
+ }
+
+ adjustVolume(change) {
+ const currentVolume = Math.round(this.audio.volume * 100);
+ const newVolume = Math.max(0, Math.min(100, currentVolume + change));
+
+ this.volumeSlider.value = newVolume;
+ this.audio.volume = newVolume / 100;
+ this.volumeValue.textContent = newVolume + '%';
+
+ // 更新静音状态
+ if (newVolume > 0 && this.isMuted) {
+ this.isMuted = false;
+ } else if (newVolume === 0 && !this.isMuted) {
+ this.isMuted = true;
+ }
+
+ if (newVolume > 0 && !this.isMuted) {
+ this.previousVolume = newVolume / 100;
+ }
+
+ this.updateVolumeIcon(newVolume);
+ console.log('键盘调节音量:', newVolume);
+ }
+
+ skipTime(seconds) {
+ if (this.audio.duration) {
+ const newTime = Math.max(0, Math.min(this.audio.duration, this.audio.currentTime + seconds));
+ this.audio.currentTime = newTime;
+ }
+ }
+
+ updatePlaybackModeDisplay() {
+ const modeIcons = {
+ 'loop': '🔁',
+ 'single': '🔂',
+ 'random': '🔀'
+ };
+
+ const modeTexts = {
+ 'loop': '列表循环',
+ 'single': '单曲循环',
+ 'random': '随机播放'
+ };
+
+ if (this.playbackModeBtn && this.playbackModeText) {
+ this.playbackModeBtn.textContent = modeIcons[this.playbackMode];
+ this.playbackModeText.textContent = modeTexts[this.playbackMode];
+ }
+ }
+
+ togglePlaybackMode() {
+ const modes = ['loop', 'single', 'random'];
+ const currentIndex = modes.indexOf(this.playbackMode);
+ this.playbackMode = modes[(currentIndex + 1) % modes.length];
+
+ this.updatePlaybackModeDisplay();
+
+ const modeTexts = {
+ 'loop': '列表循环',
+ 'single': '单曲循环',
+ 'random': '随机播放'
+ };
+
+ // 添加视觉反馈
+ this.showPlaybackModeFeedback(modeTexts[this.playbackMode]);
+ }
+
+ showPlaybackModeFeedback(modeText) {
+ const feedback = document.createElement('div');
+ feedback.textContent = `播放模式: ${modeText}`;
+ feedback.className = 'visualization-feedback';
+ feedback.style.background = 'rgba(102, 126, 234, 0.9)';
+
+ document.body.appendChild(feedback);
+
+ setTimeout(() => {
+ if (feedback.parentNode) {
+ feedback.parentNode.removeChild(feedback);
+ }
+ }, 1500);
+ }
+}
+
+// 初始化播放器
+document.addEventListener('DOMContentLoaded', () => {
+ new AudioPlayer();
+});
\ No newline at end of file
diff --git a/src/style.css b/src/style.css
new file mode 100644
index 0000000..8c6b0da
--- /dev/null
+++ b/src/style.css
@@ -0,0 +1,583 @@
+/* 音乐播放器样式 */
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+}
+
+body {
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ min-height: 100vh;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ padding: 20px;
+}
+
+.audio-player {
+ background: rgba(255, 255, 255, 0.1);
+ backdrop-filter: blur(10px);
+ border-radius: 20px;
+ padding: 30px;
+ box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.37);
+ border: 1px solid rgba(255, 255, 255, 0.18);
+ width: 100%;
+ max-width: 900px;
+ color: white;
+}
+
+.audio-player h1 {
+ text-align: center;
+ margin-bottom: 20px;
+ font-size: 2.5em;
+ font-weight: 300;
+ text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
+}
+
+/* 文件选择器 */
+.file-selector {
+ margin-bottom: 15px;
+ text-align: center;
+ display: flex;
+ justify-content: center;
+ gap: 15px;
+ flex-wrap: wrap;
+}
+
+.folder-label {
+ background: linear-gradient(45deg, #667eea, #764ba2);
+}
+
+.file-selector input[type="file"] {
+ display: none;
+}
+
+.file-label {
+ display: inline-block;
+ padding: 12px 30px;
+ background: linear-gradient(45deg, #ff6b6b, #ee5a24);
+ color: white;
+ border-radius: 25px;
+ cursor: pointer;
+ transition: all 0.3s ease;
+ font-weight: 500;
+ box-shadow: 0 4px 15px rgba(238, 90, 36, 0.4);
+}
+
+.file-label:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 6px 20px rgba(238, 90, 36, 0.6);
+}
+
+/* 波形图容器 */
+.waveform-container {
+ margin: 20px 0;
+ text-align: center;
+ border-radius: 15px;
+ overflow: hidden;
+ box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
+ height: 320px;
+}
+
+#waveform {
+ width: 100%;
+ height: 100%;
+ background: #000;
+ border-radius: 15px;
+ cursor: pointer;
+}
+
+#waveform:hover {
+ transform: scale(1.01);
+}
+
+/* 播放控制和进度条容器 */
+.playback-progress-container {
+ display: flex;
+ align-items: center;
+ gap: 15px;
+ margin: 20px 0;
+}
+
+.control-btn {
+ padding: 12px 25px;
+ border: none;
+ border-radius: 25px;
+ background: linear-gradient(45deg, #4facfe, #00f2fe);
+ color: white;
+ font-size: 16px;
+ font-weight: 500;
+ cursor: pointer;
+ transition: all 0.3s ease;
+ box-shadow: 0 4px 15px rgba(79, 172, 254, 0.4);
+ white-space: nowrap;
+}
+
+/* 播放/暂停按钮 */
+.play-pause-btn {
+ padding: 12px;
+ border-radius: 50%;
+ width: 40px;
+ height: 40px;
+ font-size: 20px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: linear-gradient(90deg, #ff6b6b, #4ecdc4);
+ box-shadow: 0 3px 12px rgba(255, 107, 107, 0.4);
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
+ border: 2px solid rgba(255, 255, 255, 0.3);
+}
+
+.play-pause-btn:hover {
+ box-shadow: 0 5px 18px rgba(255, 107, 107, 0.6);
+ transform: scale(1.08);
+ border-color: rgba(255, 255, 255, 0.5);
+}
+
+.play-pause-btn:active {
+ transform: scale(0.95);
+}
+
+/* 停止按钮 */
+.stop-btn {
+ padding: 10px;
+ border-radius: 50%;
+ width: 40px;
+ height: 40px;
+ font-size: 16px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: linear-gradient(90deg, #667eea, #764ba2);
+ box-shadow: 0 2px 10px rgba(102, 126, 234, 0.4);
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
+ border: 2px solid rgba(255, 255, 255, 0.2);
+}
+
+.stop-btn:hover {
+ box-shadow: 0 4px 15px rgba(102, 126, 234, 0.6);
+ transform: scale(1.08);
+ border-color: rgba(255, 255, 255, 0.4);
+}
+
+.stop-btn:active {
+ transform: scale(0.95);
+}
+
+/* 导航按钮(上一首/下一首) */
+.nav-btn {
+ padding: 10px;
+ border-radius: 50%;
+ width: 40px;
+ height: 40px;
+ font-size: 16px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: linear-gradient(45deg, #4facfe, #00f2fe);
+ box-shadow: 0 2px 10px rgba(79, 172, 254, 0.4);
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
+ border: 2px solid rgba(255, 255, 255, 0.2);
+}
+
+.nav-btn:hover {
+ box-shadow: 0 4px 15px rgba(79, 172, 254, 0.6);
+ transform: scale(1.08);
+ border-color: rgba(255, 255, 255, 0.4);
+}
+
+.nav-btn:active {
+ transform: scale(0.95);
+}
+
+.nav-btn:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+ transform: none !important;
+}
+
+.control-btn:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 6px 20px rgba(79, 172, 254, 0.6);
+}
+
+.control-btn:active {
+ transform: translateY(0);
+}
+
+/* 进度条 */
+.progress-container {
+ flex: 1;
+ user-select: none; /* 防止拖动时选中文本 */
+ margin-top: 15px;
+}
+
+.progress-bar {
+ width: 100%;
+ height: 6px;
+ background: rgba(255, 255, 255, 0.15);
+ border-radius: 3px;
+ position: relative;
+ cursor: pointer;
+ margin-bottom: 8px;
+ transition: all 0.3s ease;
+ box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.2);
+}
+
+.progress-bar:hover {
+ height: 8px;
+ box-shadow: 0 2px 8px rgba(255, 255, 255, 0.2), inset 0 1px 3px rgba(0, 0, 0, 0.2);
+}
+
+.progress {
+ height: 100%;
+ background: linear-gradient(90deg, #ff6b6b, #4ecdc4);
+ width: 0%;
+ transition: width 0.1s ease;
+ border-radius: 3px;
+ position: relative;
+ box-shadow: 0 1px 3px rgba(255, 107, 107, 0.4);
+}
+
+.progress-handle {
+ position: absolute;
+ top: 50%;
+ right: -6px;
+ transform: translateY(-50%);
+ width: 12px;
+ height: 12px;
+ background: linear-gradient(45deg, #ff6b6b, #4ecdc4);
+ border-radius: 50%;
+ cursor: grab;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
+ transition: all 0.2s ease;
+ z-index: 10;
+ border: 1px solid rgba(255, 255, 255, 0.8);
+}
+
+.progress-handle:hover {
+ transform: translateY(-50%) scale(1.4);
+ box-shadow: 0 3px 8px rgba(255, 107, 107, 0.4);
+ border-width: 2px;
+}
+
+.progress-handle:active {
+ cursor: grabbing;
+ transform: translateY(-50%) scale(1.2);
+ box-shadow: 0 2px 6px rgba(255, 107, 107, 0.6);
+}
+
+.time-display {
+ text-align: center;
+ font-size: 14px;
+ color: rgba(255, 255, 255, 0.8);
+}
+
+/* 底部控制栏 */
+.bottom-controls {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ gap: 20px;
+ margin: 20px 0;
+}
+
+/* 播放模式控制 */
+.playback-mode-control {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ background: rgba(255, 255, 255, 0.05);
+ padding: 8px 12px;
+ border-radius: 20px;
+ border: 1px solid rgba(255, 255, 255, 0.1);
+}
+
+#playbackModeBtn {
+ background: linear-gradient(45deg, #ff6b6b, #ee5a24);
+ padding: 8px 12px;
+ border-radius: 50%;
+ width: 36px;
+ height: 36px;
+ font-size: 16px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border: none;
+ min-width: 36px;
+}
+
+#playbackModeText {
+ font-size: 14px;
+ font-weight: 500;
+ white-space: nowrap;
+}
+
+/* 可视化控制 */
+.visualization-control {
+ display: flex;
+ align-items: center;
+ gap: 15px;
+}
+
+#visualizationBtn {
+ background: linear-gradient(45deg, #667eea, #764ba2);
+}
+
+#visualizationMode {
+ font-weight: 500;
+ color: rgba(255, 255, 255, 0.9);
+}
+
+/* 音量控制 */
+.volume-control {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+}
+
+.volume-control label {
+ font-weight: 500;
+ white-space: nowrap;
+}
+
+#volumeSlider {
+ width: 120px;
+ height: 5px;
+ background: rgba(255, 255, 255, 0.3);
+ border-radius: 5px;
+ outline: none;
+ -webkit-appearance: none;
+}
+
+#volumeSlider::-webkit-slider-thumb {
+ -webkit-appearance: none;
+ appearance: none;
+ width: 20px;
+ height: 20px;
+ background: linear-gradient(45deg, #ff6b6b, #4ecdc4);
+ border-radius: 50%;
+ cursor: pointer;
+ box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
+}
+
+#volumeSlider::-moz-range-thumb {
+ width: 20px;
+ height: 20px;
+ background: linear-gradient(45deg, #ff6b6b, #4ecdc4);
+ border-radius: 50%;
+ cursor: pointer;
+ border: none;
+ box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
+}
+
+#volumeValue {
+ min-width: 40px;
+ font-weight: 500;
+}
+
+/* 音量按钮 */
+.volume-btn {
+ background: linear-gradient(45deg, #667eea, #764ba2);
+ padding: 8px;
+ font-size: 16px;
+ border-radius: 50%;
+ width: 36px;
+ height: 36px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: all 0.3s ease;
+ border: none;
+ min-width: 36px;
+}
+
+.volume-btn:hover {
+ transform: scale(1.1);
+ box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
+}
+
+.volume-btn:active {
+ transform: scale(0.95);
+}
+
+.volume-btn.muted {
+ background: linear-gradient(45deg, #ff6b6b, #ee5a24);
+}
+
+.volume-btn.muted:hover {
+ box-shadow: 0 4px 12px rgba(255, 107, 107, 0.4);
+}
+
+/* 曲目信息 */
+.track-info {
+ text-align: center;
+ margin: 15px 0;
+ padding: 10px;
+ background: rgba(255, 255, 255, 0.1);
+ border-radius: 10px;
+ border: 1px solid rgba(255, 255, 255, 0.2);
+}
+
+#trackName {
+ font-size: 16px;
+ font-weight: 500;
+ color: rgba(255, 255, 255, 0.9);
+ margin: 0;
+}
+
+/* 响应式设计 */
+@media (max-width: 768px) {
+ .audio-player {
+ padding: 20px;
+ margin: 10px;
+ }
+
+ .audio-player h1 {
+ font-size: 2em;
+ }
+
+ .playback-progress-container {
+ flex-direction: row;
+ align-items: center;
+ gap: 10px;
+ }
+
+ .play-pause-btn {
+ width: 42px;
+ height: 42px;
+ font-size: 16px;
+ }
+
+ .stop-btn {
+ width: 32px;
+ height: 32px;
+ font-size: 12px;
+ }
+
+ .waveform-container {
+ height: 250px;
+ }
+
+ .bottom-controls {
+ flex-direction: column;
+ gap: 15px;
+ }
+
+ .playback-mode-control {
+ justify-content: center;
+ width: 100%;
+ }
+
+ .visualization-control,
+ .volume-control {
+ justify-content: center;
+ width: 100%;
+ }
+
+ #volumeSlider {
+ width: 150px;
+ }
+
+ .control-btn {
+ font-size: 14px;
+ padding: 10px 20px;
+ }
+}
+
+@media (max-width: 480px) {
+ .audio-player {
+ padding: 15px;
+ }
+
+ .audio-player h1 {
+ font-size: 1.8em;
+ }
+
+ .waveform-container {
+ height: 200px;
+ }
+
+ .play-pause-btn {
+ width: 45px;
+ height: 45px;
+ font-size: 18px;
+ }
+
+ .stop-btn {
+ width: 35px;
+ height: 35px;
+ font-size: 14px;
+ }
+
+ #volumeSlider {
+ width: 120px;
+ }
+}
+
+/* 动画效果 */
+@keyframes pulse {
+ 0% {
+ transform: scale(1);
+ }
+ 50% {
+ transform: scale(1.05);
+ }
+ 100% {
+ transform: scale(1);
+ }
+}
+
+@keyframes glow {
+ 0% {
+ box-shadow: 0 0 5px rgba(255, 107, 107, 0.5);
+ }
+ 50% {
+ box-shadow: 0 0 20px rgba(255, 107, 107, 0.8), 0 0 30px rgba(255, 107, 107, 0.6);
+ }
+ 100% {
+ box-shadow: 0 0 5px rgba(255, 107, 107, 0.5);
+ }
+}
+
+.audio-player.playing .play-pause-btn {
+ animation: glow 2s infinite;
+ background: linear-gradient(90deg, #ff6b6b, #ff8e53);
+}
+
+@keyframes fadeInOut {
+ 0% {
+ opacity: 0;
+ transform: translate(-50%, -50%) scale(0.8);
+ }
+ 20% {
+ opacity: 1;
+ transform: translate(-50%, -50%) scale(1);
+ }
+ 80% {
+ opacity: 1;
+ transform: translate(-50%, -50%) scale(1);
+ }
+ 100% {
+ opacity: 0;
+ transform: translate(-50%, -50%) scale(0.8);
+ }
+}
+
+.visualization-feedback {
+ position: fixed;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ background: rgba(0, 0, 0, 0.8);
+ color: white;
+ padding: 15px 25px;
+ border-radius: 25px;
+ font-size: 18px;
+ font-weight: bold;
+ z-index: 1000;
+ pointer-events: none;
+ animation: fadeInOut 1.5s ease-in-out;
+}
\ No newline at end of file
diff --git a/src/waveformVisualizer.js b/src/waveformVisualizer.js
new file mode 100644
index 0000000..3f891f5
--- /dev/null
+++ b/src/waveformVisualizer.js
@@ -0,0 +1,224 @@
+// 高级波形图可视化器
+class WaveformVisualizer {
+ constructor(canvas, audioContext, analyser) {
+ this.canvas = canvas;
+ this.ctx = canvas.getContext('2d');
+ this.audioContext = audioContext;
+ this.analyser = analyser;
+
+ this.bufferLength = this.analyser.frequencyBinCount;
+ this.dataArray = new Uint8Array(this.bufferLength);
+
+ this.setupCanvas();
+ this.visualizationTypes = ['bars', 'wave', 'circular'];
+ this.currentVisualization = 0;
+ }
+
+ setupCanvas() {
+ const rect = this.canvas.getBoundingClientRect();
+ this.canvas.width = rect.width * window.devicePixelRatio;
+ this.canvas.height = rect.height * window.devicePixelRatio;
+ this.ctx.scale(window.devicePixelRatio, window.devicePixelRatio);
+ this.canvas.style.width = rect.width + 'px';
+ this.canvas.style.height = rect.height + 'px';
+ }
+
+ drawBars() {
+ this.analyser.getByteFrequencyData(this.dataArray);
+
+ this.ctx.fillStyle = 'rgb(0, 0, 0)';
+ this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
+
+ const barWidth = (this.canvas.width / this.bufferLength) * 2.5;
+ let barHeight;
+ let x = 0;
+
+ for (let i = 0; i < this.bufferLength; i++) {
+ barHeight = (this.dataArray[i] / 255) * this.canvas.height;
+
+ // 创建渐变效果
+ const gradient = this.ctx.createLinearGradient(0, this.canvas.height - barHeight, 0, this.canvas.height);
+ const r = barHeight + (25 * (i / this.bufferLength));
+ const g = 250 * (i / this.bufferLength);
+ const b = 50;
+
+ gradient.addColorStop(0, `rgb(${r}, ${g}, ${b})`);
+ gradient.addColorStop(1, `rgb(${Math.floor(r/2)}, ${Math.floor(g/2)}, ${Math.floor(b/2)})`);
+
+ this.ctx.fillStyle = gradient;
+ this.ctx.fillRect(x, this.canvas.height - barHeight, barWidth, barHeight);
+
+ x += barWidth + 1;
+ }
+ }
+
+ drawWave() {
+ this.analyser.getByteTimeDomainData(this.dataArray);
+
+ this.ctx.fillStyle = 'rgb(0, 0, 0)';
+ this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
+
+ this.ctx.lineWidth = 2;
+ this.ctx.strokeStyle = 'rgb(0, 255, 0)';
+ this.ctx.beginPath();
+
+ const sliceWidth = this.canvas.width * 1.0 / this.bufferLength;
+ let x = 0;
+
+ for (let i = 0; i < this.bufferLength; i++) {
+ const v = this.dataArray[i] / 128.0;
+ const y = v * this.canvas.height / 2;
+
+ if (i === 0) {
+ this.ctx.moveTo(x, y);
+ } else {
+ this.ctx.lineTo(x, y);
+ }
+
+ x += sliceWidth;
+ }
+
+ this.ctx.lineTo(this.canvas.width, this.canvas.height / 2);
+ this.ctx.stroke();
+ }
+
+ drawCircular() {
+ this.drawCompactCircular();
+ }
+
+ drawCompactCircular() {
+ // 更紧凑的圆形图版本 - 圆心在画布中央,大小适中
+ this.analyser.getByteFrequencyData(this.dataArray);
+
+ this.ctx.fillStyle = 'rgb(0, 0, 0)';
+ this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
+
+ // 获取画布的实际显示尺寸
+ const displayWidth = this.canvas.width / window.devicePixelRatio;
+ const displayHeight = this.canvas.height / window.devicePixelRatio;
+
+ // 圆心位置在画布中央
+ const centerX = displayWidth / 2;
+ const centerY = displayHeight / 2;
+
+ // 适中的圆形图大小 - 占画布最小尺寸的30-35%
+ const baseRadius = Math.min(centerX, centerY) * 0.32;
+ const maxAmplitude = Math.min(centerX, centerY) * 0.22;
+
+ // 绘制频谱线条 - 平衡视觉效果和性能
+ const step = Math.max(1, Math.floor(this.bufferLength / 128));
+
+ for (let i = 0; i < this.bufferLength; i += step) {
+ const angle = (i / this.bufferLength) * Math.PI * 2;
+ const amplitude = (this.dataArray[i] / 255) * maxAmplitude;
+
+ // 起始点(基础圆上)
+ const x1 = centerX + Math.cos(angle) * baseRadius;
+ const y1 = centerY + Math.sin(angle) * baseRadius;
+
+ // 结束点(根据频率数据延伸)
+ const x2 = centerX + Math.cos(angle) * (baseRadius + amplitude);
+ const y2 = centerY + Math.sin(angle) * (baseRadius + amplitude);
+
+ // 根据频率强度计算颜色
+ const intensity = this.dataArray[i] / 255;
+ const hue = (i / this.bufferLength) * 360;
+ const saturation = 75 + (intensity * 25);
+ const lightness = 50 + (intensity * 30);
+
+ this.ctx.strokeStyle = `hsl(${hue}, ${saturation}%, ${lightness}%)`;
+ this.ctx.lineWidth = 1 + (intensity * 1.5);
+ this.ctx.beginPath();
+ this.ctx.moveTo(x1, y1);
+ this.ctx.lineTo(x2, y2);
+ this.ctx.stroke();
+ }
+ }
+
+ drawCircular() {
+ this.analyser.getByteFrequencyData(this.dataArray);
+
+ this.ctx.fillStyle = 'rgb(0, 0, 0)';
+ this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
+
+ // 获取画布的实际显示尺寸(考虑设备像素比)
+ const displayWidth = this.canvas.width / window.devicePixelRatio;
+ const displayHeight = this.canvas.height / window.devicePixelRatio;
+
+ // 圆心位置在画布中央
+ const centerX = displayWidth / 2;
+ const centerY = displayHeight / 2;
+
+ // 缩小圆形图大小,使用较小的半径
+ const baseRadius = Math.min(centerX, centerY) * 0.5; // 从0.8改为0.5,缩小一半
+ const maxAmplitude = Math.min(centerX, centerY) * 0.5; // 最大振幅也相应缩小
+
+ // 绘制内部基础圆形(可选,增加层次感)
+ this.ctx.strokeStyle = 'rgba(255, 255, 255, 0.1)';
+ this.ctx.lineWidth = 1;
+ this.ctx.beginPath();
+ this.ctx.arc(centerX, centerY, baseRadius, 0, 2 * Math.PI);
+ this.ctx.stroke();
+
+ // 绘制频谱线条
+ const step = Math.max(1, Math.floor(this.bufferLength / 128)); // 减少线条数量,提高性能
+
+ for (let i = 0; i < this.bufferLength; i += step) {
+ const angle = (i / this.bufferLength) * Math.PI * 2;
+ const amplitude = (this.dataArray[i] / 255) * maxAmplitude;
+
+ // 起始点(基础圆上)
+ const x1 = centerX + Math.cos(angle) * baseRadius;
+ const y1 = centerY + Math.sin(angle) * baseRadius;
+
+ // 结束点(根据频率数据延伸)
+ const x2 = centerX + Math.cos(angle) * (baseRadius + amplitude);
+ const y2 = centerY + Math.sin(angle) * (baseRadius + amplitude);
+
+ // 根据频率数据计算颜色
+ const intensity = this.dataArray[i] / 255;
+ const hue = (i / this.bufferLength) * 360; // 彩虹色调
+ const saturation = 80 + (intensity * 20); // 饱和度随强度变化
+ const lightness = 40 + (intensity * 40); // 亮度随强度变化
+
+ this.ctx.strokeStyle = `hsl(${hue}, ${saturation}%, ${lightness}%)`;
+ this.ctx.lineWidth = 1.5 + (intensity * 1.5); // 线条宽度随强度变化
+ this.ctx.beginPath();
+ this.ctx.moveTo(x1, y1);
+ this.ctx.lineTo(x2, y2);
+ this.ctx.stroke();
+ }
+
+ // 绘制中心点(可选,增加视觉焦点)
+ this.ctx.fillStyle = 'rgba(255, 255, 255, 0.8)';
+ this.ctx.beginPath();
+ this.ctx.arc(centerX, centerY, 2, 0, 2 * Math.PI);
+ this.ctx.fill();
+ }
+
+ draw() {
+ switch (this.visualizationTypes[this.currentVisualization]) {
+ case 'bars':
+ this.drawBars();
+ break;
+ case 'wave':
+ this.drawWave();
+ break;
+ case 'circular':
+ this.drawCircular();
+ break;
+ default:
+ this.drawBars();
+ }
+ }
+
+ nextVisualization() {
+ this.currentVisualization = (this.currentVisualization + 1) % this.visualizationTypes.length;
+ }
+
+ getCurrentVisualization() {
+ return this.visualizationTypes[this.currentVisualization];
+ }
+}
+
+export default WaveformVisualizer;
\ No newline at end of file
diff --git a/vite.config.js b/vite.config.js
new file mode 100644
index 0000000..5979927
--- /dev/null
+++ b/vite.config.js
@@ -0,0 +1,13 @@
+import { defineConfig } from 'vite'
+
+export default defineConfig({
+ // Vite配置选项
+ server: {
+ port: 3000,
+ open: true
+ },
+ build: {
+ outDir: 'dist',
+ assetsDir: 'assets'
+ }
+})
\ No newline at end of file