From 3d2b13810d98a1b705598074cb05836410b348c1 Mon Sep 17 00:00:00 2001 From: sanderdw Date: Tue, 30 Jun 2026 22:44:34 +0200 Subject: [PATCH] Particles Stream visualizer --- CHANGELOG.md | 8 + package-lock.json | 416 +++++++------- package.json | 20 +- src/App.tsx | 5 +- .../visualizers/ParticlesStream.tsx | 517 ++++++++++++++++++ 5 files changed, 747 insertions(+), 219 deletions(-) create mode 100644 src/components/visualizers/ParticlesStream.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index ce9fab1..b3f6374 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## [0.20.0] - 2026-06-30 + +### Added +- Particles Stream visualizer – flies a luma-sliced pixel-particle field of the album artwork toward the camera with afterimage motion blur; bass-reactive speed and brightness, with image upload (Three.js/WebGL). + +### Changed +- Dependency bumps + ## [0.19.1] - 2026-06-15 ### Added diff --git a/package-lock.json b/package-lock.json index 0b89ae5..424eceb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,32 +1,32 @@ { "name": "voltviz", - "version": "0.19.1", + "version": "0.20.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "voltviz", - "version": "0.19.1", + "version": "0.20.0", "dependencies": { "@sendspin/sendspin-js": "^3.2.0", "d3-geo": "^3.1.1", - "lucide-react": "^1.18.0", + "lucide-react": "^1.22.0", "react": "^19.2.7", "react-dom": "^19.2.7", - "three": "^0.184.0" + "three": "^0.185.0" }, "devDependencies": { - "@playwright/test": "^1.61.0", - "@tailwindcss/vite": "^4.3.1", + "@playwright/test": "^1.61.1", + "@tailwindcss/vite": "^4.3.2", "@types/d3-geo": "^3.1.0", - "@types/node": "^25.9.3", + "@types/node": "^26.0.1", "@types/react": "^19.2.17", "@types/react-dom": "^19.2.3", - "@types/three": "^0.184.1", - "@vitejs/plugin-react": "^6.0.2", - "tailwindcss": "^4.3.1", + "@types/three": "^0.185.0", + "@vitejs/plugin-react": "^6.0.3", + "tailwindcss": "^4.3.2", "typescript": "~6.0.3", - "vite": "^8.0.16" + "vite": "^8.1.2" } }, "node_modules/@dimforge/rapier3d-compat": { @@ -37,21 +37,21 @@ "license": "Apache-2.0" }, "node_modules/@emnapi/core": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", - "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", + "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", "dev": true, "license": "MIT", "optional": true, "dependencies": { - "@emnapi/wasi-threads": "1.2.1", + "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" } }, "node_modules/@emnapi/runtime": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", - "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", + "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", "dev": true, "license": "MIT", "optional": true, @@ -60,9 +60,9 @@ } }, "node_modules/@emnapi/wasi-threads": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", - "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", + "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", "dev": true, "license": "MIT", "optional": true, @@ -121,14 +121,14 @@ } }, "node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.5.tgz", - "integrity": "sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==", + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", + "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==", "dev": true, "license": "MIT", "optional": true, "dependencies": { - "@tybys/wasm-util": "^0.10.2" + "@tybys/wasm-util": "^0.10.3" }, "funding": { "type": "github", @@ -140,9 +140,9 @@ } }, "node_modules/@oxc-project/types": { - "version": "0.133.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.133.0.tgz", - "integrity": "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==", + "version": "0.137.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.137.0.tgz", + "integrity": "sha512-WT+Gb24i8hmvo85AIv2oEYouEXkRlKAlT9WaCa3TfLgNCN+GhrJOGZuIlMouAh38Qe4QOx26eUOVsq70qXrywA==", "dev": true, "license": "MIT", "funding": { @@ -150,13 +150,13 @@ } }, "node_modules/@playwright/test": { - "version": "1.61.0", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.61.0.tgz", - "integrity": "sha512-cKA5B6lpFEMyMGjxF54QihfYpB4FkEGH+qZhtArDEG+wezQAJY8Pq6C7T1SjWz+FFzt3TbyoXBQYk/0292TdJA==", + "version": "1.61.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.61.1.tgz", + "integrity": "sha512-8nKv6+0RJSL9FE4jYOEGXnPeM/Hg12qZpmqzZjRh3qM0Y7c3z1mrOTfFLids72RDQYVh9WpLEfR5WdpNX4fkig==", "dev": true, "license": "Apache-2.0", "dependencies": { - "playwright": "1.61.0" + "playwright": "1.61.1" }, "bin": { "playwright": "cli.js" @@ -166,9 +166,9 @@ } }, "node_modules/@rolldown/binding-android-arm64": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz", - "integrity": "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.3.tgz", + "integrity": "sha512-DT6Z3PhvioeHMvxo+xHc3KtqggrI7CCTXCmC2h/5zUlp5jVitv7XEy+9q5/7v8IolhlioawpMo8Kg0EEBy7J0g==", "cpu": [ "arm64" ], @@ -183,9 +183,9 @@ } }, "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.3.tgz", - "integrity": "sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.3.tgz", + "integrity": "sha512-0NwgwsjM7LrsuVnXMK3koTpagBNOhloc/BNjKqZjv4V5zI5r13qx69uVhRx+o5Z0yy4Hzq+lpy7TAgUG/ocvrw==", "cpu": [ "arm64" ], @@ -200,9 +200,9 @@ } }, "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.3.tgz", - "integrity": "sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.3.tgz", + "integrity": "sha512-YtiBp4disu6V560loT6PjMdiRaWmVvDNrUunAalbiFx2ggeJwxdAsgZMcoGP17uyAsTwAj5V1niksxlHnVQ1Sw==", "cpu": [ "x64" ], @@ -217,9 +217,9 @@ } }, "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.3.tgz", - "integrity": "sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.3.tgz", + "integrity": "sha512-yD3EkEdXk2LypPxnf/kSZHirarsI8gcPzc62SukhR9VJTyvV+F9Q/GxWNuCojc7sXyuVC4DxRGhdDK4X8VSsbw==", "cpu": [ "x64" ], @@ -234,9 +234,9 @@ } }, "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.3.tgz", - "integrity": "sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.3.tgz", + "integrity": "sha512-c+8vieQbsD7HNAHKIA34w0GJ9FedFFuJGD+7E6vz7Q3uqAIugL5p45fhlsj4UaAsHpcmlqugBWMhA0/j7o0sIg==", "cpu": [ "arm" ], @@ -251,9 +251,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.3.tgz", - "integrity": "sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.3.tgz", + "integrity": "sha512-50jD0uUwLvur7Zz9LHz17kaAdTPjn5wN93hEgjvmYFRZwiR7ZJYovTd5ipyWJDAnXKvZ+wgc+/Ika6dwSF5OcA==", "cpu": [ "arm64" ], @@ -271,9 +271,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.3.tgz", - "integrity": "sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.3.tgz", + "integrity": "sha512-BO9+oPL8K9poZJBfYPsXNtYjPE5uM3qeehT3aFcW4LITOl+iSqhp0abzjR2nWBUNjIZeKXjAEWBZ64WjNoHd6w==", "cpu": [ "arm64" ], @@ -291,9 +291,9 @@ } }, "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.3.tgz", - "integrity": "sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.3.tgz", + "integrity": "sha512-f3VpLB1vQ0Eo6ecr/6cekLnvYMFF4YBFoVGkfkvPLq1bAkbAwHYQPZKoAmG6OJyTcxxoC+AvezGx/S1obNC0Mw==", "cpu": [ "ppc64" ], @@ -311,9 +311,9 @@ } }, "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.3.tgz", - "integrity": "sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.3.tgz", + "integrity": "sha512-AmurZ26Pqx/RI9N1gzEOCklkKXl927yjfXWUUS0O7Puh8ARM/Ob8qfrD3qnWksScdw6cSrW5PSHE9DyLu7+PtA==", "cpu": [ "s390x" ], @@ -331,9 +331,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.3.tgz", - "integrity": "sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.3.tgz", + "integrity": "sha512-JJpqs8bRGITDOdbkNKnlojzBabbOHrqjSvDr0IVsZObE1lBcPjxItUEY9eWIDbxaJ3cGrXPWGfGkIxFijg/URg==", "cpu": [ "x64" ], @@ -351,9 +351,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.3.tgz", - "integrity": "sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.3.tgz", + "integrity": "sha512-rSJcdjPxzA/by/6/rYs+v+bXU7UjvnbUWz8MJb6kh6+knqB1dCrtHg0uu7C/4haqJvqdkYHQ5IGn+tCH9GLW/g==", "cpu": [ "x64" ], @@ -371,9 +371,9 @@ } }, "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.3.tgz", - "integrity": "sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.3.tgz", + "integrity": "sha512-hQ3/PYkDJICgevvyNcVrihVeqq7k1Pp3VZ9lY+dauAYUJKO+auqApvANhvR1An9BhmqYKvW2Mu1F9u4DXSMLxQ==", "cpu": [ "arm64" ], @@ -388,9 +388,9 @@ } }, "node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.3.tgz", - "integrity": "sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.3.tgz", + "integrity": "sha512-Elcv/BtML9lXrV6JuKITc/grN2kYV9gjsQpW8Jfw4ioK0TOkjBjye0nnyqQNy9STNaI20lXNaQBRrD5gSgR0Yg==", "cpu": [ "wasm32" ], @@ -398,18 +398,18 @@ "license": "MIT", "optional": true, "dependencies": { - "@emnapi/core": "1.10.0", - "@emnapi/runtime": "1.10.0", - "@napi-rs/wasm-runtime": "^1.1.4" + "@emnapi/core": "1.11.1", + "@emnapi/runtime": "1.11.1", + "@napi-rs/wasm-runtime": "^1.1.6" }, "engines": { "node": "^20.19.0 || >=22.12.0" } }, "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.3.tgz", - "integrity": "sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.3.tgz", + "integrity": "sha512-2DrEfhluH9yhiaFApmsjsjwrSYbNcY1oFTzYSP1a535jDbV98zCFanA/96TBUd0iDFcxGmw9QRExwGCXz3U+/g==", "cpu": [ "arm64" ], @@ -424,9 +424,9 @@ } }, "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.3.tgz", - "integrity": "sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.3.tgz", + "integrity": "sha512-OL4OMk7UPXOeVGGd3qo5zJyPIljf4AFgk5QAkPPS+OoLuOOozhuaQGC18MxVTnw/06q93gShAJzlwnSCY9YtqA==", "cpu": [ "x64" ], @@ -460,9 +460,9 @@ } }, "node_modules/@tailwindcss/node": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.1.tgz", - "integrity": "sha512-6NDaqRoAMSXD1mr/RXu0HBvNE9a2n5tHPsxu9XHLws8o4Twes5rBM2205SUUiJ9goAtadrN6xTGX0UDEwp/N4A==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.2.tgz", + "integrity": "sha512-yWP/sqEcBLaD8JuA6zNwxoYKr75qxTioYwlRwekj5Jr/I5GXnoJfjetH/psLUIv74cYTH2lBUEzBkinthoYcBg==", "dev": true, "license": "MIT", "dependencies": { @@ -472,37 +472,37 @@ "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", - "tailwindcss": "4.3.1" + "tailwindcss": "4.3.2" } }, "node_modules/@tailwindcss/oxide": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.1.tgz", - "integrity": "sha512-yVPyo8RNkabVr3O2EhHEE0Rewu7YKzc1DhIqfL46LKveFrmu9XbDazNOJY7/GRuvw1h6u3utWnR29H/p5JPlgA==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.2.tgz", + "integrity": "sha512-z8ZgnzX8gdNoWLBLqBPoh/sjnxkwvf9ZuWjnO0l0yIzbLa5/9S+eC5QxGZKRobVHIC3/1BoMWjHblqWjcgFgag==", "dev": true, "license": "MIT", "engines": { "node": ">= 20" }, "optionalDependencies": { - "@tailwindcss/oxide-android-arm64": "4.3.1", - "@tailwindcss/oxide-darwin-arm64": "4.3.1", - "@tailwindcss/oxide-darwin-x64": "4.3.1", - "@tailwindcss/oxide-freebsd-x64": "4.3.1", - "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.1", - "@tailwindcss/oxide-linux-arm64-gnu": "4.3.1", - "@tailwindcss/oxide-linux-arm64-musl": "4.3.1", - "@tailwindcss/oxide-linux-x64-gnu": "4.3.1", - "@tailwindcss/oxide-linux-x64-musl": "4.3.1", - "@tailwindcss/oxide-wasm32-wasi": "4.3.1", - "@tailwindcss/oxide-win32-arm64-msvc": "4.3.1", - "@tailwindcss/oxide-win32-x64-msvc": "4.3.1" + "@tailwindcss/oxide-android-arm64": "4.3.2", + "@tailwindcss/oxide-darwin-arm64": "4.3.2", + "@tailwindcss/oxide-darwin-x64": "4.3.2", + "@tailwindcss/oxide-freebsd-x64": "4.3.2", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.2", + "@tailwindcss/oxide-linux-arm64-gnu": "4.3.2", + "@tailwindcss/oxide-linux-arm64-musl": "4.3.2", + "@tailwindcss/oxide-linux-x64-gnu": "4.3.2", + "@tailwindcss/oxide-linux-x64-musl": "4.3.2", + "@tailwindcss/oxide-wasm32-wasi": "4.3.2", + "@tailwindcss/oxide-win32-arm64-msvc": "4.3.2", + "@tailwindcss/oxide-win32-x64-msvc": "4.3.2" } }, "node_modules/@tailwindcss/oxide-android-arm64": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.1.tgz", - "integrity": "sha512-SVlyf61g374l5cHyg8x9kf5xmLcOaxvOTsbsqDnSsDJaKOEFZ7GCvi84VAVGpxojYOs1+3K6M0UjXfqPU8vmOQ==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.2.tgz", + "integrity": "sha512-WHxqIuHpvZ5VtdX6GTl1Ik/Vp2YuN42Et+0CdeaVd/frQ9jAvGmvR8vLT+jk3e8/Q3x8kECB9+R17pgpp2BulA==", "cpu": [ "arm64" ], @@ -517,9 +517,9 @@ } }, "node_modules/@tailwindcss/oxide-darwin-arm64": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.1.tgz", - "integrity": "sha512-hVnWLwv+e/l7c4WKyVtHVrIPvYdqWHjRB3MDIqARynzFtnQg85kmQEFCbV9Ja0VVx4xXTIiDWY60Y7iz/iNoDA==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.2.tgz", + "integrity": "sha512-GZypeUY/IDJW3877KeM+O67vbXr3MBnbtEL4aYhNErv/JWZhye2vGSWWG9tB6iiqR2MqRNkY8IOUy4NdSZV26w==", "cpu": [ "arm64" ], @@ -534,9 +534,9 @@ } }, "node_modules/@tailwindcss/oxide-darwin-x64": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.1.tgz", - "integrity": "sha512-Cf7abu0WVgbhU7ANgPUnSAvm7nCvMweusHb8FnaHlLfv/Caq4GYaEZg7ZImzzmjx4lIAfuS8q+eLIS7A7IzxIg==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.2.tgz", + "integrity": "sha512-UIIzmefR6KO1sDU7MzRqAxC8iBpft/VhkGjTjnhoS6k7Z3rQ9wEgA1ODSiyH/tcSYssulNm4Ci3hOeK1jH7ccQ==", "cpu": [ "x64" ], @@ -551,9 +551,9 @@ } }, "node_modules/@tailwindcss/oxide-freebsd-x64": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.1.tgz", - "integrity": "sha512-ZZqzX2Y+GXtXXfqSfpJhDm60OoZfvLHLCgm+J7NVqgHHJjG/m9ugZI77RwTsVd4fnBJuCFP6Ae6kTJb71UdS8g==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.2.tgz", + "integrity": "sha512-GN+uAmcI6DNspnCDwtOAZrTz6oukJnp337qZvxqCGLd3BHBzJpO0ZbTLRvJNdztOeAmTzewewGIMPb0tk2R4WA==", "cpu": [ "x64" ], @@ -568,9 +568,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.1.tgz", - "integrity": "sha512-/Ah/xik0LaMYfv9DZ0S/t4pBlBNYOcqtRwusjgovHkvT8ixueWCLyJjsaF5kQIckjb4IT8Q6K6p/iPmZMixYgg==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.2.tgz", + "integrity": "sha512-4ABn7qSbdHRwTiDiuWNegCyb5+2FJ4vKIKc3DmKrvAFw7MU1Lm11dIkTPwUaFdTzc7IsOpDbqBrlh0x6y36U/w==", "cpu": [ "arm" ], @@ -585,9 +585,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.1.tgz", - "integrity": "sha512-gqdFoVJlw444GvpnheZLHmvTzSxI/cOUUh2KSNejQjTcYkW062SVD+En0rUgD+QV91bz1XGIGtt1HJd48xUGbQ==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.2.tgz", + "integrity": "sha512-wDgEIGwoM8w8pufh9LVt1PahDgNdKXrLC2qfAnV3vAmococ9RWbxeAw4pxPttd/TsJfwjyLf90Dg1y9y8I6Emw==", "cpu": [ "arm64" ], @@ -605,9 +605,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm64-musl": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.1.tgz", - "integrity": "sha512-Bwv9KwOvE0VKa86xPFif9b9c3Y1NxOV1P0gLti/IYaWEsQYZXDlxfGEtA8mdDZ7SG3wyNXAWYT5SIn3giL57oA==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.2.tgz", + "integrity": "sha512-J5Nuk0uZQIiMTJj3LEx4sAA9tMFUoXQZFv1J6An+QGYe53HKRJuFDi0rpq/tuouCZeAbOBY3kQ6g8qeD4TUjtA==", "cpu": [ "arm64" ], @@ -625,9 +625,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-x64-gnu": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.1.tgz", - "integrity": "sha512-Ymi8O8T15HYQdOUWUtTI6ldN0neHP85FC+Qz32xTcZ7iJXtem/x8ITev0o1e9e5rkqj4lONZfTRLvkmin1+tKg==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.2.tgz", + "integrity": "sha512-kqCZpSKOBEJO4mz7OqWoofBZeXTAwaVGPj0ErAj7CojmhKpWVWVOnrt9dE8odoIraZq4oj3ausM37kXi+Tow8w==", "cpu": [ "x64" ], @@ -645,9 +645,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-x64-musl": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.1.tgz", - "integrity": "sha512-M+P/91qJ6uILLw4k2G93GMDRAXj61SMvFQYt39AqvUqYgExXpLL5aepfns7sj4HiAQeolirQF9E0lzRvdf4zPQ==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.2.tgz", + "integrity": "sha512-cixpqbh2toJDmkuCRI68nXA8ZxNmdK9Y+9v5h3MC3ZQKy/0BO8AWzlkWyRM7JAFSGBlfig4YVTPsK6MVgqz1uw==", "cpu": [ "x64" ], @@ -665,9 +665,9 @@ } }, "node_modules/@tailwindcss/oxide-wasm32-wasi": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.1.tgz", - "integrity": "sha512-zsM8uOeqvVGHsAXsJxsT28ttosFahLJKCLOTUBqRAtKnVgGSRitds9T432QiT8b77Yga7JIBkulIRRlJPtYhRA==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.2.tgz", + "integrity": "sha512-4ec2Z/LOmRsAgU23CS4xeJfcJlmRg94A/XrbGRCF1gyU/zdDfRLYDVsS+ynSZCmGNxQ1jQriQOKMQeQxBA3Isw==", "bundleDependencies": [ "@napi-rs/wasm-runtime", "@emnapi/core", @@ -683,9 +683,9 @@ "license": "MIT", "optional": true, "dependencies": { - "@emnapi/core": "^1.10.0", - "@emnapi/runtime": "^1.10.0", - "@emnapi/wasi-threads": "^1.2.1", + "@emnapi/core": "^1.11.1", + "@emnapi/runtime": "^1.11.1", + "@emnapi/wasi-threads": "^1.2.2", "@napi-rs/wasm-runtime": "^1.1.4", "@tybys/wasm-util": "^0.10.2", "tslib": "^2.8.1" @@ -695,9 +695,9 @@ } }, "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.1.tgz", - "integrity": "sha512-aiNvSq9BsVk8V513lDKlrCFAgf8qBMPZTpgEhInL+NwQqs97mYmupVMrPrgBBSL8Pv/0zXu9MrMF9rMun1ZeNg==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.2.tgz", + "integrity": "sha512-Zyr/M0+XcYZu3bZrUytc7TXvrk0ftWfl8gN2MwekNDzhqhKRUucMPSeOzM0o0wH5AWOU49BsKRrfKxI2atCPMQ==", "cpu": [ "arm64" ], @@ -712,9 +712,9 @@ } }, "node_modules/@tailwindcss/oxide-win32-x64-msvc": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.1.tgz", - "integrity": "sha512-xDEyu1rg290472FEGaKHnzyDyh5QH+AlWvsU5hMoMtPpzmKlRI0jaYKCgSHDYtaQWZOYbMaduSyCwFwY4n1HmA==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.2.tgz", + "integrity": "sha512-QI9BO7KlNZsp2GuO0jwAAj5jCDABOKXRkCk2XuKTSaNEFSdfzqswYVTtCHBNKHLsqyjFyFkqlDiwkNbTYSssMQ==", "cpu": [ "x64" ], @@ -729,15 +729,15 @@ } }, "node_modules/@tailwindcss/vite": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.3.1.tgz", - "integrity": "sha512-hItDHuIIlEV61R+faXu66s1K36aTurO/Qw0e45Vskz57gXl9pWOT6eg3zmcEui6CZXddbN7zd41bwmvag4JGwQ==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.3.2.tgz", + "integrity": "sha512-eHpMeX4JXfVNJDEcsouTeCBubJBTcTLigeaw/NTUW6PB5ATKKXdyonnXgTBX2VuRbjz1hjfz6C5XAhr52ImQXA==", "dev": true, "license": "MIT", "dependencies": { - "@tailwindcss/node": "4.3.1", - "@tailwindcss/oxide": "4.3.1", - "tailwindcss": "4.3.1" + "@tailwindcss/node": "4.3.2", + "@tailwindcss/oxide": "4.3.2", + "tailwindcss": "4.3.2" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7 || ^8" @@ -751,9 +751,9 @@ "license": "MIT" }, "node_modules/@tybys/wasm-util": { - "version": "0.10.2", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", - "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", + "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", "dev": true, "license": "MIT", "optional": true, @@ -779,13 +779,13 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "25.9.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.3.tgz", - "integrity": "sha512-603BddQMv3pUcr4U2dhujk83N2tTDVr/34wII2B6bJy6g+8WD6yUb11jszNs0gdi4PesVWl7ABt8nYMVpnLUcg==", + "version": "26.0.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.0.1.tgz", + "integrity": "sha512-fc3KiUoBt6kie0N9bIW3E47vZsuaMf0PM2AaUpLCLT0s/LvX1nxAim6Fc049cNxODPpGm6qRAuUOB86SkRuPQw==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": ">=7.24.0 <7.24.7" + "undici-types": "~8.3.0" } }, "node_modules/@types/react": { @@ -816,9 +816,9 @@ "license": "MIT" }, "node_modules/@types/three": { - "version": "0.184.1", - "resolved": "https://registry.npmjs.org/@types/three/-/three-0.184.1.tgz", - "integrity": "sha512-6q4VdiqVsrTRqmk62/BnlcAvIrnDM0zf2ZDVKI5kZiniWrSaOHaQzmbp+BNzoggc/8tgW412pL//wZIxu2PPTA==", + "version": "0.185.0", + "resolved": "https://registry.npmjs.org/@types/three/-/three-0.185.0.tgz", + "integrity": "sha512-O2Uy8Cj4Nonr8dWUUbifMdPe8B0Mq7EdOHb89S4+kjUw/KhbjTZrUuYlrQ1bpUKG+EP9QJnN7qNxbHGlGoLHMA==", "dev": true, "license": "MIT", "dependencies": { @@ -838,13 +838,13 @@ "license": "MIT" }, "node_modules/@vitejs/plugin-react": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.2.tgz", - "integrity": "sha512-DlSMqo4WhThw4vB8Mpn0Woe9J+Jfq1geJ61AKW0QEgLzGMNwtIMdxbDUzLxcun8W7NbJO0e2Jg/Nxm3cCSVzzg==", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.3.tgz", + "integrity": "sha512-vmFvco5/QuC2f9Oj+wTk0+9XeDFkHxSamwZKYc7MxYwKICfvUvlMhqKI0VuICPltGqh1neqBKDvO4kes1ya8vg==", "dev": true, "license": "MIT", "dependencies": { - "@rolldown/pluginutils": "^1.0.0" + "@rolldown/pluginutils": "^1.0.1" }, "engines": { "node": "^20.19.0 || >=22.12.0" @@ -1258,9 +1258,9 @@ } }, "node_modules/lucide-react": { - "version": "1.18.0", - "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.18.0.tgz", - "integrity": "sha512-LZDb7H/0YfM+RJncD0hDQRCAu+vSGODqpe35TuVI8EuXaRjkczbsx7p8dY4J87F/MUSj6bpYqeI8nw8qXaAdmA==", + "version": "1.22.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.22.0.tgz", + "integrity": "sha512-c9o3l0PiNcgOQDW4F31BEYHudE7kgxVt3o30qMl36ZPwTxXlGB4QnLilhERvVM4uh/pl5MDyY1/gzZSYcHDtBg==", "license": "ISC", "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" @@ -1284,9 +1284,9 @@ "license": "MIT" }, "node_modules/nanoid": { - "version": "3.3.12", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", - "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "version": "3.3.15", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz", + "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", "dev": true, "funding": [ { @@ -1329,13 +1329,13 @@ } }, "node_modules/playwright": { - "version": "1.61.0", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.61.0.tgz", - "integrity": "sha512-Z+7BeeqQPRRzklHsVFP4KTGIyMxKUmfeRA4WisM6G3/XW6nwGeX6fX9qYaDa+CiUqpOkb2f6X3nar05R3kSuJQ==", + "version": "1.61.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.61.1.tgz", + "integrity": "sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.61.0" + "playwright-core": "1.61.1" }, "bin": { "playwright": "cli.js" @@ -1348,9 +1348,9 @@ } }, "node_modules/playwright-core": { - "version": "1.61.0", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.61.0.tgz", - "integrity": "sha512-caX7TrY3Ml6egyDX0WUcTHDxodl/b51y5wJOdCEA36QviK/s2g081hvmGs8eaE3DWb6NYZQ6BjO/QkNRPenoPA==", + "version": "1.61.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.61.1.tgz", + "integrity": "sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==", "dev": true, "license": "Apache-2.0", "bin": { @@ -1361,9 +1361,9 @@ } }, "node_modules/postcss": { - "version": "8.5.15", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", - "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "version": "8.5.16", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz", + "integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==", "dev": true, "funding": [ { @@ -1411,13 +1411,13 @@ } }, "node_modules/rolldown": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz", - "integrity": "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.3.tgz", + "integrity": "sha512-1F1eEtUBtFvcGm1HQ9TiUIUHPQG7mSAODrhIzjxoUEFuo8OcbrGLiVLkevNgj84TE4lnHvnumwFjhJO5Eu135g==", "dev": true, "license": "MIT", "dependencies": { - "@oxc-project/types": "=0.133.0", + "@oxc-project/types": "=0.137.0", "@rolldown/pluginutils": "^1.0.0" }, "bin": { @@ -1427,21 +1427,21 @@ "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rolldown/binding-android-arm64": "1.0.3", - "@rolldown/binding-darwin-arm64": "1.0.3", - "@rolldown/binding-darwin-x64": "1.0.3", - "@rolldown/binding-freebsd-x64": "1.0.3", - "@rolldown/binding-linux-arm-gnueabihf": "1.0.3", - "@rolldown/binding-linux-arm64-gnu": "1.0.3", - "@rolldown/binding-linux-arm64-musl": "1.0.3", - "@rolldown/binding-linux-ppc64-gnu": "1.0.3", - "@rolldown/binding-linux-s390x-gnu": "1.0.3", - "@rolldown/binding-linux-x64-gnu": "1.0.3", - "@rolldown/binding-linux-x64-musl": "1.0.3", - "@rolldown/binding-openharmony-arm64": "1.0.3", - "@rolldown/binding-wasm32-wasi": "1.0.3", - "@rolldown/binding-win32-arm64-msvc": "1.0.3", - "@rolldown/binding-win32-x64-msvc": "1.0.3" + "@rolldown/binding-android-arm64": "1.1.3", + "@rolldown/binding-darwin-arm64": "1.1.3", + "@rolldown/binding-darwin-x64": "1.1.3", + "@rolldown/binding-freebsd-x64": "1.1.3", + "@rolldown/binding-linux-arm-gnueabihf": "1.1.3", + "@rolldown/binding-linux-arm64-gnu": "1.1.3", + "@rolldown/binding-linux-arm64-musl": "1.1.3", + "@rolldown/binding-linux-ppc64-gnu": "1.1.3", + "@rolldown/binding-linux-s390x-gnu": "1.1.3", + "@rolldown/binding-linux-x64-gnu": "1.1.3", + "@rolldown/binding-linux-x64-musl": "1.1.3", + "@rolldown/binding-openharmony-arm64": "1.1.3", + "@rolldown/binding-wasm32-wasi": "1.1.3", + "@rolldown/binding-win32-arm64-msvc": "1.1.3", + "@rolldown/binding-win32-x64-msvc": "1.1.3" } }, "node_modules/scheduler": { @@ -1461,9 +1461,9 @@ } }, "node_modules/tailwindcss": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.1.tgz", - "integrity": "sha512-hk+TB1m+K8CYNrP6rjQaq/Y+4Zylwpa87mLYBKCunwnnQ9p+fHb7kmSfGqyEJoxF/O6CDyABWVFEafNSYKll+Q==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.2.tgz", + "integrity": "sha512-WtctNNSH8A9jlMIqxzuYumOHU5uGZyRv0Q5svQl+oEPy5w84YpBxdb7MdqyiSPQge5jTJ6zFQLq0PFygdccSBA==", "dev": true, "license": "MIT" }, @@ -1482,9 +1482,9 @@ } }, "node_modules/three": { - "version": "0.184.0", - "resolved": "https://registry.npmjs.org/three/-/three-0.184.0.tgz", - "integrity": "sha512-wtTRjG92pM5eUg/KuUnHsqSAlPM296brTOcLgMRqEeylYTh/CdtvKUvCyyCQTzFuStieWxvZb8mVTMvdPyUpxg==", + "version": "0.185.0", + "resolved": "https://registry.npmjs.org/three/-/three-0.185.0.tgz", + "integrity": "sha512-+yRrcRO2iZa8uzvNNl0d7cL4huhgKgBvVJ0njcTe8xFqZ6DMAFZdCKDP91SEAuj25bNAj7k1QQdf+srZywVK6w==", "license": "MIT" }, "node_modules/tinyglobby": { @@ -1527,23 +1527,23 @@ } }, "node_modules/undici-types": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", - "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", + "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", "dev": true, "license": "MIT" }, "node_modules/vite": { - "version": "8.0.16", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.16.tgz", - "integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==", + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.2.tgz", + "integrity": "sha512-6YYPbRXTxx6bRXmOn7XdnQAy5DQNHhDgtjhDHI13oe4pY93kkcdGJWxpGwOm++/Wh0QpQhDrpIoVMrmrsI5AGQ==", "dev": true, "license": "MIT", "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", - "postcss": "^8.5.15", - "rolldown": "1.0.3", + "postcss": "^8.5.16", + "rolldown": "~1.1.3", "tinyglobby": "^0.2.17" }, "bin": { @@ -1560,7 +1560,7 @@ }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", - "@vitejs/devtools": "^0.1.18", + "@vitejs/devtools": "^0.3.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", diff --git a/package.json b/package.json index 33c9e50..f946787 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "voltviz", "private": true, - "version": "0.19.1", + "version": "0.20.0", "type": "module", "scripts": { "dev": "vite --port=3000 --host=0.0.0.0", @@ -15,22 +15,22 @@ "dependencies": { "@sendspin/sendspin-js": "^3.2.0", "d3-geo": "^3.1.1", - "lucide-react": "^1.18.0", + "lucide-react": "^1.22.0", "react": "^19.2.7", "react-dom": "^19.2.7", - "three": "^0.184.0" + "three": "^0.185.0" }, "devDependencies": { - "@playwright/test": "^1.61.0", - "@tailwindcss/vite": "^4.3.1", + "@playwright/test": "^1.61.1", + "@tailwindcss/vite": "^4.3.2", "@types/d3-geo": "^3.1.0", - "@types/node": "^25.9.3", + "@types/node": "^26.0.1", "@types/react": "^19.2.17", "@types/react-dom": "^19.2.3", - "@types/three": "^0.184.1", - "@vitejs/plugin-react": "^6.0.2", - "tailwindcss": "^4.3.1", + "@types/three": "^0.185.0", + "@vitejs/plugin-react": "^6.0.3", + "tailwindcss": "^4.3.2", "typescript": "~6.0.3", - "vite": "^8.0.16" + "vite": "^8.1.2" } } diff --git a/src/App.tsx b/src/App.tsx index 3a6379c..0ca0cc3 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -58,7 +58,8 @@ type VisualizerType = | 'trailsstream' | 'shambhala' | 'holoblinds' - | 'insidequantum'; + | 'insidequantum' + | 'particlesstream'; type VisualizerProps = { stream: MediaStream; @@ -137,6 +138,7 @@ const visualizerComponents: Record import('./components/visualizers/Shambhala')), holoblinds: lazy(() => import('./components/visualizers/HoloBlinds')), insidequantum: lazy(() => import('./components/visualizers/InsideQuantum')), + particlesstream: lazy(() => import('./components/visualizers/ParticlesStream')), }; export default function App() { @@ -445,6 +447,7 @@ export default function App() { + diff --git a/src/components/visualizers/ParticlesStream.tsx b/src/components/visualizers/ParticlesStream.tsx new file mode 100644 index 0000000..0b31c2b --- /dev/null +++ b/src/components/visualizers/ParticlesStream.tsx @@ -0,0 +1,517 @@ +import { useEffect, useRef, useState } from 'react'; +import * as THREE from 'three'; +import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer.js'; +import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass.js'; +import { AfterimagePass } from 'three/examples/jsm/postprocessing/AfterimagePass.js'; +import { VisualizerSettings } from '../../types'; +import type { ServerStateMetadata } from '@sendspin/sendspin-js'; +import { ImagePlus, Eye, EyeOff } from 'lucide-react'; +import dummyCover from '../../../images/dummycover.png'; + +interface Props { + stream: MediaStream; + settings: VisualizerSettings; + sendspinMetadata?: ServerStateMetadata | null; +} + +// Hardcoded constants (formerly the lil-gui `params` from the source demo). +const BLUR_AMOUNT = 0.86; // AfterimagePass damp +const NUM_LAYERS = 5; +const STATIC_DARK_LAYERS = 1; +const EDGE_SMOOTHING = 0.897; +const GROUP_DEPTH = 600; +const BASE_IMAGE_OPACITY = 0.40; +const BASE_SPEED = 2.0; +const PARTICLE_COUNT = 80000; // capped from the original 500000 for smooth embedded playback +const PARTICLE_SIZE = 1.5; +const PARTICLE_BRIGHTNESS = 1.0; +const FADE_START_DISTANCE = -902; +const BASS_SPEED_BOOST = 4.0; // K: how strongly bass accelerates the fly-through + +export default function ParticlesStream({ stream, settings, sendspinMetadata }: Props) { + const containerRef = useRef(null); + const mountRef = useRef(null); + const animationRef = useRef(null); + const audioCtxRef = useRef(null); + const analyserRef = useRef(null); + const sourceRef = useRef(null); + const settingsRef = useRef(settings); + + const [uploadedImage, setUploadedImage] = useState(null); + const [showUI, setShowUI] = useState(true); + + useEffect(() => { + settingsRef.current = settings; + }, [settings]); + + const imageSrc = uploadedImage ?? sendspinMetadata?.artwork_url ?? dummyCover; + + const handleUpload = (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) return; + const reader = new FileReader(); + reader.onload = (event) => setUploadedImage(event.target?.result as string); + reader.readAsDataURL(file); + }; + + useEffect(() => { + if (!containerRef.current || !mountRef.current) return; + const container = containerRef.current; + const mount = mountRef.current; + + // --- Audio setup --- + const audioCtx = new (window.AudioContext || (window as any).webkitAudioContext)(); + audioCtxRef.current = audioCtx; + const analyser = audioCtx.createAnalyser(); + analyser.fftSize = 512; + analyser.smoothingTimeConstant = 0.8; + analyserRef.current = analyser; + const source = audioCtx.createMediaStreamSource(stream); + source.connect(analyser); + sourceRef.current = source; + const dataArray = new Uint8Array(analyser.frequencyBinCount); + + // --- Three.js setup --- + const scene = new THREE.Scene(); + const getSize = () => ({ + w: container.clientWidth || 1, + h: container.clientHeight || 1, + }); + let { w: viewW, h: viewH } = getSize(); + + const camera = new THREE.PerspectiveCamera(75, viewW / viewH, 0.1, 3000); + camera.position.z = 0; + + const renderer = new THREE.WebGLRenderer({ antialias: false, powerPreference: 'high-performance' }); + renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); + renderer.setSize(viewW, viewH); + while (mount.firstChild) mount.removeChild(mount.firstChild); + mount.appendChild(renderer.domElement); + + const composer = new EffectComposer(renderer); + composer.addPass(new RenderPass(scene, camera)); + const afterimagePass = new AfterimagePass(); + afterimagePass.uniforms['damp'].value = BLUR_AMOUNT; + composer.addPass(afterimagePass); + + // --- Scene state --- + let movingMeshes: THREE.Mesh[] = []; + let baseMesh: THREE.Mesh | null = null; + let baseTexture: THREE.CanvasTexture | null = null; + let currentTextures: THREE.CanvasTexture[] = []; + let currentImageAspect = 1; + let particleSystem: THREE.Points | null = null; + let startupFade = 0.0; + + const tintColor = new THREE.Color(); + const tmpColor = new THREE.Color(); + + function removeParticles() { + if (particleSystem) { + scene.remove(particleSystem); + particleSystem.geometry.dispose(); + (particleSystem.material as THREE.Material).dispose(); + particleSystem = null; + } + } + + function createParticles(baseData: ImageData, w: number, h: number) { + removeParticles(); + + const geometry = new THREE.BufferGeometry(); + const positions = new Float32Array(PARTICLE_COUNT * 3); + const colors = new Float32Array(PARTICLE_COUNT * 4); // RGBA: alpha drives per-particle fade + + const loopDepth = GROUP_DEPTH * 3; + + for (let i = 0; i < PARTICLE_COUNT; i++) { + const x = Math.floor(Math.random() * w); + const y = Math.floor(Math.random() * h); + const index = (y * w + x) * 4; + + tmpColor.setRGB( + baseData.data[index] / 255, + baseData.data[index + 1] / 255, + baseData.data[index + 2] / 255 + ); + tmpColor.convertSRGBToLinear(); + tmpColor.multiplyScalar(PARTICLE_BRIGHTNESS); + + colors[i * 4] = tmpColor.r; + colors[i * 4 + 1] = tmpColor.g; + colors[i * 4 + 2] = tmpColor.b; + colors[i * 4 + 3] = 0.0; + + positions[i * 3] = (x / w) - 0.5; + positions[i * 3 + 1] = 0.5 - (y / h); + positions[i * 3 + 2] = Math.random() * -loopDepth; + } + + geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3)); + geometry.setAttribute('color', new THREE.BufferAttribute(colors, 4)); + + const material = new THREE.PointsMaterial({ + size: PARTICLE_SIZE, + vertexColors: true, + transparent: true, + opacity: 1.0, + blending: THREE.NormalBlending, + depthWrite: false, + sizeAttenuation: false, + }); + + particleSystem = new THREE.Points(geometry, material); + scene.add(particleSystem); + } + + // Split the image into luma-based cross-faded depth layers (artifact-free edges). + function processImageIntoLayers(img: HTMLImageElement) { + currentImageAspect = img.width / img.height; + + currentTextures.forEach((tex) => tex.dispose()); + currentTextures = []; + + const canvas = document.createElement('canvas'); + const maxSize = 1024; + let w = img.width; + let h = img.height; + if (w > maxSize || h > maxSize) { + const ratio = Math.min(maxSize / w, maxSize / h); + w = Math.floor(w * ratio); + h = Math.floor(h * ratio); + } + + canvas.width = w; + canvas.height = h; + const ctx = canvas.getContext('2d', { willReadFrequently: true })!; + ctx.drawImage(img, 0, 0, w, h); + + if (baseTexture) baseTexture.dispose(); + baseTexture = new THREE.CanvasTexture(canvas); + baseTexture.colorSpace = THREE.SRGBColorSpace; + baseTexture.matrixAutoUpdate = false; + + let baseData: ImageData; + try { + baseData = ctx.getImageData(0, 0, w, h); + } catch { + // Remote artwork tainted the canvas (CORS). Fall back to the bundled cover. + if (imageSrc !== dummyCover) setUploadedImage(null); + return; + } + const totalPixels = baseData.data.length; + + createParticles(baseData, w, h); + + // Pre-calculate smooth layer index per luma value to avoid banding artifacts. + const steppedXArray = new Float32Array(256); + const edgeWidth = EDGE_SMOOTHING; + const minEdge = 0.5 - (edgeWidth / 2); + const maxEdge = 0.5 + (edgeWidth / 2); + + for (let l = 0; l < 256; l++) { + const x = (l / 255) * (NUM_LAYERS - 1); + const floorX = Math.floor(x); + const f = x - floorX; + let smoothedF = f; + if (edgeWidth < 1.0) { + if (edgeWidth <= 0.001) { + smoothedF = f < 0.5 ? 0.0 : 1.0; + } else if (f <= minEdge) { + smoothedF = 0.0; + } else if (f >= maxEdge) { + smoothedF = 1.0; + } else { + const t = (f - minEdge) / (maxEdge - minEdge); + smoothedF = t * t * (3 - 2 * t); + } + } + steppedXArray[l] = floorX + smoothedF; + } + + for (let i = 0; i < NUM_LAYERS; i++) { + const lCanvas = document.createElement('canvas'); + lCanvas.width = w; + lCanvas.height = h; + const lCtx = lCanvas.getContext('2d')!; + const lData = lCtx.createImageData(w, h); + + for (let j = 0; j < totalPixels; j += 4) { + const r = baseData.data[j]; + const g = baseData.data[j + 1]; + const b = baseData.data[j + 2]; + const a = baseData.data[j + 3]; + + const luma = Math.round(0.299 * r + 0.587 * g + 0.114 * b); + const clampedLuma = Math.max(0, Math.min(255, luma)); + const steppedX = steppedXArray[clampedLuma]; + const weight = Math.max(0, 1.0 - Math.abs(steppedX - i)); + + if (weight > 0) { + lData.data[j] = r; + lData.data[j + 1] = g; + lData.data[j + 2] = b; + lData.data[j + 3] = a * weight; + } + } + + lCtx.putImageData(lData, 0, 0); + const tex = new THREE.CanvasTexture(lCanvas); + tex.colorSpace = THREE.SRGBColorSpace; + tex.matrixAutoUpdate = false; + currentTextures.push(tex); + } + + startupFade = 0.0; + buildScene(); + } + + function buildScene() { + movingMeshes.forEach((mesh) => { + scene.remove(mesh); + (mesh.material as THREE.Material).dispose(); + }); + movingMeshes = []; + if (baseMesh) { + scene.remove(baseMesh); + (baseMesh.material as THREE.Material).dispose(); + baseMesh = null; + } + + const geometry = new THREE.PlaneGeometry(1, 1); + + // Static full background image. + const baseMat = new THREE.MeshBasicMaterial({ + map: baseTexture, + depthWrite: false, + transparent: true, + opacity: 0.0, + }); + baseMesh = new THREE.Mesh(geometry, baseMat); + baseMesh.position.z = -GROUP_DEPTH * 3; + // Faint full-image backdrop behind the particle field (opacity set in the loop). + scene.add(baseMesh); + + // Partial luma layers (static + moving). Hidden by default since particles render. + currentTextures.forEach((tex, i) => { + const isStatic = i < STATIC_DARK_LAYERS; + const instanceCount = isStatic ? 1 : 3; + + for (let g = 0; g < instanceCount; g++) { + const material = new THREE.MeshBasicMaterial({ + map: tex, + transparent: true, + side: THREE.DoubleSide, + depthWrite: false, + opacity: 0.0, + }); + + const mesh = new THREE.Mesh(geometry, material); + + if (isStatic) { + mesh.position.z = -GROUP_DEPTH * 2.5 + (i * 10); + mesh.userData = { isStatic: true }; + } else { + const depthRatio = 1.0 - (i / currentTextures.length); + const localZ = -depthRatio * GROUP_DEPTH; + const groupZ = -g * GROUP_DEPTH; + mesh.position.z = localZ + groupZ; + mesh.userData = { isStatic: false }; + } + + mesh.visible = false; // particles enabled + scene.add(mesh); + movingMeshes.push(mesh); + } + }); + + updateCoverUVs(); + } + + function updateCoverUVs() { + const { w, h } = getSize(); + const screenAspect = w / h; + + const backgroundDepth = Math.abs(-GROUP_DEPTH * 3); + const vFOV = camera.fov * Math.PI / 180; + const visibleHeight = 2 * Math.tan(vFOV / 2) * backgroundDepth; + const visibleWidth = visibleHeight * screenAspect; + + if (baseMesh) baseMesh.scale.set(visibleWidth, visibleHeight, 1); + movingMeshes.forEach((mesh) => mesh.scale.set(visibleWidth, visibleHeight, 1)); + if (particleSystem) particleSystem.scale.set(visibleWidth, visibleHeight, 1); + + const texturesToUpdate: THREE.Texture[] = [...currentTextures]; + if (baseTexture) texturesToUpdate.push(baseTexture); + + texturesToUpdate.forEach((tex) => { + let scaleX = 1; + let scaleY = 1; + if (screenAspect > currentImageAspect) { + scaleY = currentImageAspect / screenAspect; + } else { + scaleX = screenAspect / currentImageAspect; + } + tex.matrix.setUvTransform(0.0, 0.0, scaleX, scaleY, 0, 0.5, 0.5); + }); + } + + // --- Load the image, then build the scene --- + const img = new Image(); + img.crossOrigin = 'Anonymous'; + img.onload = () => processImageIntoLayers(img); + img.onerror = () => { + if (imageSrc !== dummyCover) setUploadedImage(null); + }; + img.src = imageSrc; + + // --- Resize --- + const handleResize = () => { + const next = getSize(); + viewW = next.w; + viewH = next.h; + camera.aspect = viewW / viewH; + camera.updateProjectionMatrix(); + renderer.setSize(viewW, viewH); + composer.setSize(viewW, viewH); + updateCoverUVs(); + }; + window.addEventListener('resize', handleResize); + + // --- Animation loop --- + const animate = () => { + animationRef.current = requestAnimationFrame(animate); + const s = settingsRef.current; + + analyser.getByteFrequencyData(dataArray); + let bassSum = 0; + const bassBins = Math.min(16, dataArray.length); + for (let i = 0; i < bassBins; i++) bassSum += dataArray[i]; + const bass = bassSum / bassBins / 255; + const bassBoost = bass * s.sensitivity; + + if (startupFade < 1.0) { + startupFade = Math.min(1.0, startupFade + 0.01); + } + + const speed = BASE_SPEED * s.speed * (1 + bassBoost * BASS_SPEED_BOOST); + const loopDepth = GROUP_DEPTH * 3; + const maxVisibleZ = 100; + const spawnZ = maxVisibleZ - loopDepth; + const fadeInDistance = GROUP_DEPTH * 0.8; + const fadeSpan = maxVisibleZ - FADE_START_DISTANCE; + + if (baseMesh && baseMesh.visible) { + (baseMesh.material as THREE.MeshBasicMaterial).opacity = startupFade * BASE_IMAGE_OPACITY; + } + + // Brightness pulse + hue tint via material.color (multiplied by vertex colors). + const brightness = Math.min(2.5, PARTICLE_BRIGHTNESS * (1 + bassBoost)); + if (s.hueShift === 0) { + tintColor.setRGB(brightness, brightness, brightness); + } else { + // Tint the whole field toward the chosen hue, scaled by the brightness pulse. + tintColor.setHSL((s.hueShift % 360) / 360, 0.6, 0.5); + tintColor.multiplyScalar(brightness * 2); + } + + movingMeshes.forEach((mesh) => { + const mat = mesh.material as THREE.MeshBasicMaterial; + if (mesh.userData.isStatic) { + mat.opacity = startupFade; + return; + } + + mesh.position.z += speed; + + let targetOpacity = 1.0; + if (mesh.position.z > FADE_START_DISTANCE) { + targetOpacity = 1.0 - (mesh.position.z - FADE_START_DISTANCE) / fadeSpan; + } + if (mesh.position.z < spawnZ + fadeInDistance) { + const spawnOpacity = (mesh.position.z - spawnZ) / fadeInDistance; + targetOpacity = Math.min(targetOpacity, spawnOpacity); + } + mat.opacity = Math.max(0, Math.min(1, targetOpacity)) * startupFade; + + if (mesh.position.z > maxVisibleZ) { + mesh.position.z -= loopDepth; + } + }); + + if (particleSystem) { + const pMat = particleSystem.material as THREE.PointsMaterial; + pMat.size = PARTICLE_SIZE * s.scale; + pMat.color.copy(tintColor); + + const positions = particleSystem.geometry.attributes.position.array as Float32Array; + const colors = particleSystem.geometry.attributes.color.array as Float32Array; + + for (let i = 0; i < PARTICLE_COUNT; i++) { + let z = positions[i * 3 + 2] + speed; + if (z > maxVisibleZ) z -= loopDepth; + positions[i * 3 + 2] = z; + + let targetOpacity = 1.0; + if (z > FADE_START_DISTANCE) { + targetOpacity = 1.0 - (z - FADE_START_DISTANCE) / fadeSpan; + } + if (z < spawnZ + fadeInDistance) { + const spawnOpacity = (z - spawnZ) / fadeInDistance; + targetOpacity = Math.min(targetOpacity, spawnOpacity); + } + colors[i * 4 + 3] = Math.max(0, Math.min(1, targetOpacity)) * startupFade; + } + particleSystem.geometry.attributes.position.needsUpdate = true; + particleSystem.geometry.attributes.color.needsUpdate = true; + } + + composer.render(); + }; + animate(); + + // --- Cleanup --- + return () => { + window.removeEventListener('resize', handleResize); + if (animationRef.current) cancelAnimationFrame(animationRef.current); + img.onload = null; + img.onerror = null; + if (sourceRef.current) sourceRef.current.disconnect(); + if (audioCtxRef.current && audioCtxRef.current.state !== 'closed') { + audioCtxRef.current.close(); + } + removeParticles(); + movingMeshes.forEach((mesh) => (mesh.material as THREE.Material).dispose()); + if (baseMesh) (baseMesh.material as THREE.Material).dispose(); + currentTextures.forEach((tex) => tex.dispose()); + if (baseTexture) baseTexture.dispose(); + composer.dispose(); + renderer.dispose(); + if (renderer.domElement.parentNode === mount) { + mount.removeChild(renderer.domElement); + } + }; + }, [stream, imageSrc]); + + return ( +
+
+
+ {showUI && ( + + )} + +
+
+ ); +}