From 5d44bea631d6cae645544382efe96cd09c56be06 Mon Sep 17 00:00:00 2001 From: sanderdw Date: Mon, 15 Jun 2026 22:07:43 +0200 Subject: [PATCH] chore: update dependencies and version to 0.19.0; add test scripts feat: add new visualizers 'HoloBlinds' and 'InsideQuantum' with audio-reactive features docs: create audio-reactivity patterns guide for visualizers refactor: enhance visualizer components with improved audio modulation techniques --- .github/docs/audio-reactivity-patterns.md | 91 +++++ CHANGELOG.md | 12 + Dockerfile | 4 +- README.md | 20 + package-lock.json | 380 +++++++++---------- package.json | 28 +- src/App.tsx | 8 +- src/components/visualizers/HoloBlinds.tsx | 316 +++++++++++++++ src/components/visualizers/InsideQuantum.tsx | 344 +++++++++++++++++ 9 files changed, 997 insertions(+), 206 deletions(-) create mode 100644 .github/docs/audio-reactivity-patterns.md create mode 100644 src/components/visualizers/HoloBlinds.tsx create mode 100644 src/components/visualizers/InsideQuantum.tsx diff --git a/.github/docs/audio-reactivity-patterns.md b/.github/docs/audio-reactivity-patterns.md new file mode 100644 index 0000000..85f97d3 --- /dev/null +++ b/.github/docs/audio-reactivity-patterns.md @@ -0,0 +1,91 @@ +# Audio Reactivity Patterns for Visualizers + +Lessons learned from fixing time-dependent audio drift in shader-based visualizers. + +## The Core Problem: Audio × Time Drift + +When audio-modulated values are **multiplied by elapsed time**, the visual impact of the same audio input grows unboundedly as the session continues. + +```glsl +// BAD: audio-modulated speed × growing time = drift +float t = uTime * uSpeed; // uTime grows forever +rotation = rot(t * uTwistSpeed); // same audio fluctuation has bigger effect at t=600s vs t=10s +``` + +## The Fix: Phase Accumulators + +Accumulate phase on the JS side. Audio modulates the **rate of change per frame**, not a multiplier on total elapsed time. + +```typescript +// GOOD: accumulate phase incrementally +globalPhase += delta * currentSpeed; // only this frame's contribution +twistPhase += delta * currentSpeed * twistRate * audioBoost; + +uniforms.uPhase.value = globalPhase; +uniforms.uTwistPhase.value = twistPhase; +``` + +```glsl +// Shader uses pre-accumulated phase directly +rotation = rot(uPhase * rotationFactor); +twist = rot(vertexY * twistAmount + uTwistPhase); +``` + +**Why this works:** A bass hit at t=10s and t=600s both add the same `delta * boost` increment — the visual effect is identical regardless of session duration. + +## Direct Audio-Reactive Offsets (Time-Independent) + +For instant "punch" response to audio, add a **direct offset** uniform that maps audio energy to a visual parameter without any time multiplication: + +```typescript +// Direct offset: audio -> visual, no time involved +uniforms.uTwistReact.value = smoothedMids * 3.0 + smoothedBass * 1.5; +``` + +```glsl +// Added directly to rotation angles +q.xz *= rot(q.y * uTwist + uTwistPhase + uTwistReact); +``` + +This provides immediate, bounded reactivity that stays constant over time. + +## Safe vs Unsafe Audio Modulation Targets + +| Target | Safe Pattern | Unsafe Pattern | +|--------|-------------|----------------| +| Rotation/twist angles | Phase accumulator (`+= delta * rate`) | `elapsedTime * audioSpeed` | +| Object size/radius | Additive offset (`base + audio * scale`) | — | +| Thickness/detail | Additive offset (`base + audio * scale`) | — | +| Particle spawn rate | Direct threshold check | — | +| Color shift | Direct mapping | — | +| Animation speed | Drives phase accumulation rate | Multiplied by growing time | + +**Rule of thumb:** If a parameter is multiplied by something that grows monotonically (time, frame count), it must NOT be audio-modulated directly. Instead, audio should modulate the *rate of growth* via a phase accumulator. + +## PolySphere as Reference Implementation + +`PolySphere.tsx` naturally avoids this bug because: +- `time += 0.01 * speed` — speed is user-controlled, not audio-modulated +- Audio only affects additive properties: radius pulse, face detach offset, particle spawn +- None of these are multiplied by the growing `time` variable + +## Smoothing Best Practices + +```typescript +// Exponential smoothing (EMA) — good defaults +smoothedBass += (bass - smoothedBass) * 0.15; // fast response +smoothedMids += (mids - smoothedMids) * 0.12; // medium +smoothedHighs += (highs - smoothedHighs) * 0.10; // slower (less jitter) +``` + +- Higher alpha = faster response, more jitter +- Lower alpha = smoother, more latency +- `analyser.smoothingTimeConstant = 0.8` provides additional FFT-level smoothing + +## Checklist for New Visualizers + +1. **Never multiply audio-modulated values by elapsed time** — use phase accumulators +2. **Add direct audio offsets** for instant reactivity (twist, scale, glow) +3. **Keep offsets bounded** — `smoothedValue` is already 0–1 range (with sensitivity=1) +4. **Test at 5+ minutes** — compare visual intensity to the first 10 seconds +5. **Sensitivity slider** scales raw band energy before smoothing — works naturally with both patterns diff --git a/CHANGELOG.md b/CHANGELOG.md index b387b00..fb17c24 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # Changelog +## [0.19.0] - 2026-06-15 + +### Added +- Holo Blinds visualizer – raymarched twisting gyroid confined to a squashed sphere core with audio-reactive brightness, twist speed, and detail (Three.js/WebGL). +- Inside Quantum visualizer – full-screen KIFS fractal with volumetric raymarching, asymmetric folding, domain warping, and accumulated glow (Three.js/WebGL). Audio-reactive warp amplitude, rotation speed, and ray thickness. + +### Changed +- Dependency bumps + +### Acknowledgments +- Shoutout to [@sabosugi](https://x.com/sabosugi) for the nice visuals. + ## [0.18.0] - 2026-05-23 ### Added diff --git a/Dockerfile b/Dockerfile index 4ec9751..01b03dd 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM node:25-alpine AS builder +FROM node:26-alpine AS builder WORKDIR /app COPY package*.json ./ @@ -7,7 +7,7 @@ RUN npm ci COPY . . RUN npm run build -FROM nginx:1.29-alpine +FROM nginx:1.31.1-alpine COPY nginx/default.conf /etc/nginx/conf.d/default.conf COPY --from=builder /app/dist /usr/share/nginx/html diff --git a/README.md b/README.md index 3f90143..036d3b6 100644 --- a/README.md +++ b/README.md @@ -82,6 +82,8 @@ http://localhost:8080 | `npm run preview` | Preview production build locally | | `npm run clean` | Remove build artifacts | | `npm run lint` | Check TypeScript for errors | +| `npm run test` | Run the Playwright smoke tests | +| `npm run test:install` | Download the Playwright Chromium browser | --- @@ -199,6 +201,18 @@ npm run build npm run preview # Test production build locally ``` +### Run Smoke Tests +```bash +npm run test:install +npm run test +``` + +On Linux, Playwright may also need system browser libraries: + +```bash +npx playwright install-deps chromium +``` + ### Environment Setup The app requires **microphone or display-capture permissions** to function properly. When you first load VoltViz, you'll be prompted to grant these permissions. @@ -258,6 +272,12 @@ Contributions are welcome! Whether you want to add new visualizations, improve p --- +## 🎨 Credits + +Special shoutout to [@sabosugi](https://x.com/sabosugi) for the nice visuals. + +--- + ## 💡 Tips - **Best Experience**: Use with headphones and a full-screen window diff --git a/package-lock.json b/package-lock.json index 707fd70..1287250 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,25 +8,25 @@ "name": "voltviz", "version": "0.18.0", "dependencies": { - "@sendspin/sendspin-js": "^3.1.0", + "@sendspin/sendspin-js": "^3.2.0", "d3-geo": "^3.1.1", - "lucide-react": "^1.9.0", - "react": "^19.2.6", - "react-dom": "^19.2.6", + "lucide-react": "^1.18.0", + "react": "^19.2.7", + "react-dom": "^19.2.7", "three": "^0.184.0" }, "devDependencies": { - "@playwright/test": "^1.60.0", - "@tailwindcss/vite": "^4.3.0", + "@playwright/test": "^1.61.0", + "@tailwindcss/vite": "^4.3.1", "@types/d3-geo": "^3.1.0", - "@types/node": "^25.7.0", - "@types/react": "^19.2.14", + "@types/node": "^25.9.3", + "@types/react": "^19.2.17", "@types/react-dom": "^19.2.3", "@types/three": "^0.184.1", - "@vitejs/plugin-react": "^6.0.1", - "tailwindcss": "^4.3.0", + "@vitejs/plugin-react": "^6.0.2", + "tailwindcss": "^4.3.1", "typescript": "~6.0.3", - "vite": "^8.0.12" + "vite": "^8.0.16" } }, "node_modules/@dimforge/rapier3d-compat": { @@ -121,14 +121,14 @@ } }, "node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", - "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", + "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==", "dev": true, "license": "MIT", "optional": true, "dependencies": { - "@tybys/wasm-util": "^0.10.1" + "@tybys/wasm-util": "^0.10.2" }, "funding": { "type": "github", @@ -140,9 +140,9 @@ } }, "node_modules/@oxc-project/types": { - "version": "0.132.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.132.0.tgz", - "integrity": "sha512-FESMOxil5Se014ui/Eq8fT5uHJo6nIRwH0PfJrZJXs6Gek3ZVFOrpUv3YIZT20m+extU98Hg1Ym72U58rlsxUQ==", + "version": "0.133.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.133.0.tgz", + "integrity": "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==", "dev": true, "license": "MIT", "funding": { @@ -150,13 +150,13 @@ } }, "node_modules/@playwright/test": { - "version": "1.60.0", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.60.0.tgz", - "integrity": "sha512-O71yZIbAh/PxDMNGns37GHBIfrVkEVyn+AXyIa5dOTfb4/xNvRWV+Vv/NMbNCtODB/pO7vLlF2OTmMVLhmr7Ag==", + "version": "1.61.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.61.0.tgz", + "integrity": "sha512-cKA5B6lpFEMyMGjxF54QihfYpB4FkEGH+qZhtArDEG+wezQAJY8Pq6C7T1SjWz+FFzt3TbyoXBQYk/0292TdJA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "playwright": "1.60.0" + "playwright": "1.61.0" }, "bin": { "playwright": "cli.js" @@ -166,9 +166,9 @@ } }, "node_modules/@rolldown/binding-android-arm64": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.2.tgz", - "integrity": "sha512-ZS4D1JPGn/MYQN/SYDWftIE/nVsM8j/AFOYEzAoOE2O3NktQOZru+/vYXGbR/qtdLdIfGCP0lcoJiYVzsEz+iQ==", + "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==", "cpu": [ "arm64" ], @@ -183,9 +183,9 @@ } }, "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.2.tgz", - "integrity": "sha512-vdFA9+C/rekyGce7WqHs/xoT0ioZEWaOFyZLIV1mEeNFaFDUQrPIo8Vs2GvJ6eetb3rzDUtUBgzto3ExpXJB3w==", + "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==", "cpu": [ "arm64" ], @@ -200,9 +200,9 @@ } }, "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.2.tgz", - "integrity": "sha512-BewSOwTHazv77DTYiAZXSqqKZ4KP/KonFisDMVU7PImxoWfB2aepnPhd2E4SWz3zDzYgDNbs6jBmTdgNnF02GA==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.3.tgz", + "integrity": "sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==", "cpu": [ "x64" ], @@ -217,9 +217,9 @@ } }, "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.2.tgz", - "integrity": "sha512-m41o7M0YWtUdqk61Tb+jnKb2rN++iRdIASlExkUoKfIAH30DOHCB8fVLzSUpbWHHU8esmEioY62PxzexE8MBuA==", + "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==", "cpu": [ "x64" ], @@ -234,9 +234,9 @@ } }, "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.2.tgz", - "integrity": "sha512-jcojB9H7W/jS29pMKWAK1N+fU99vXodHDTatS3b3y/XSOCiHo0kkA74pL3jJmkoQtYpOCxDvaKs1fo2Ij/1X5w==", + "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==", "cpu": [ "arm" ], @@ -251,9 +251,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.2.tgz", - "integrity": "sha512-1jn6qDU5iiOgFgygDzKUuKP0maTi0/f1+sBLgvij/76C77Nm3ts6ufz9Bjg5q5dduxiUIxtq86JIoBvo1xQ4Ig==", + "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==", "cpu": [ "arm64" ], @@ -271,9 +271,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.2.tgz", - "integrity": "sha512-QVLO/czFMdoMFSqlX3bcswcJNm/23r+qoa/jgtmFc/qEp6/jXmIkDjF/XIo8dPfGaiwy1xfQn8o77L79GeXFgw==", + "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==", "cpu": [ "arm64" ], @@ -291,9 +291,9 @@ } }, "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.2.tgz", - "integrity": "sha512-hgO5Abm0w5UL6FEa2iFnZqo2KlK7TQ5QhV5x09hujBf7t5KzHQ1VmfPuTpqRy/rNlSxua3eWH374xxiVrP+lcA==", + "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==", "cpu": [ "ppc64" ], @@ -311,9 +311,9 @@ } }, "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.2.tgz", - "integrity": "sha512-fy8rXxuYEu602abC8MUNaPjYLIFzReOaEIEMKMUa0rFEUxNpVXhs15KSSQ4qlqSaM7B6rcj9rDZgADh/IGDzLQ==", + "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==", "cpu": [ "s390x" ], @@ -331,9 +331,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.2.tgz", - "integrity": "sha512-0+bOkiQ779+r1WpoHOWHqncvyySci0vKph+myNDYb+im6meJAzHQXay6oEgnkHuUGouM1LKTZwqKpBow6Kj7CQ==", + "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==", "cpu": [ "x64" ], @@ -351,9 +351,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.2.tgz", - "integrity": "sha512-mjSkrzZK5Qsl0a9d1JgILOiuZOSDTVdKENcSXBoqbzSrspLR/4/IRVDo5wd2GgZjNss/viBFJdeq+j7qH2nypw==", + "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==", "cpu": [ "x64" ], @@ -371,9 +371,9 @@ } }, "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.2.tgz", - "integrity": "sha512-1v5vHasdfQAZoEHakBV72LIFAC9JjnymsiKxp+GEr/ma3+NJCPSaYK+qavInOovJkgwFrs7GccX2d6IgDA3Z5w==", + "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==", "cpu": [ "arm64" ], @@ -388,9 +388,9 @@ } }, "node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.2.tgz", - "integrity": "sha512-mb1VobWn6NheziTk5/WEaR6AKVbrwT5sOi6C7zk3gy/pD1qtJfU1j4PgTo2NJnOtbL9Dl3Aeei8w9jJ7qC2jZQ==", + "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==", "cpu": [ "wasm32" ], @@ -407,9 +407,9 @@ } }, "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.2.tgz", - "integrity": "sha512-SqKonF56vA/L2yHwHYcEp2P34URpOZ7d1fS635cTkpDnUtEGdUbhI6NzsPdqeSWvAAeGDrxjWjNmibDIdFf9/A==", + "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==", "cpu": [ "arm64" ], @@ -424,9 +424,9 @@ } }, "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.2.tgz", - "integrity": "sha512-v7qRI7gXLRINcOGXt+7YmAZ6iFuyZVMIoXAxhd8oP+DR9dLfL9GfNIx7PLMxmhZdvq8waUJBQiWN9EKNy+TRBQ==", + "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==", "cpu": [ "x64" ], @@ -448,9 +448,9 @@ "license": "MIT" }, "node_modules/@sendspin/sendspin-js": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@sendspin/sendspin-js/-/sendspin-js-3.1.0.tgz", - "integrity": "sha512-tljpoE7JwGIvAMk9M+UBGKkur4ME1RyZRxIV/FFJ0trp6fJOC5bWjewaO8bgm9ybEKO64su5PMbn/u63YzlaMQ==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@sendspin/sendspin-js/-/sendspin-js-3.2.0.tgz", + "integrity": "sha512-sxMibcr9eiue5yT0viXxCruq8LTRSEzaz9epmhSp3Qe4gHcksE+JwFOTEJfgkY5kJBmPq22CkYlyvJXcHacK2g==", "license": "Apache-2.0", "dependencies": { "opus-encdec": "^0.1.1" @@ -460,49 +460,49 @@ } }, "node_modules/@tailwindcss/node": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.0.tgz", - "integrity": "sha512-aFb4gUhFOgdh9AXo4IzBEOzBkkAxm9VigwDJnMIYv3lcfXCJVesNfbEaBl4BNgVRyid92AmdviqwBUBRKSeY3g==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.1.tgz", + "integrity": "sha512-6NDaqRoAMSXD1mr/RXu0HBvNE9a2n5tHPsxu9XHLws8o4Twes5rBM2205SUUiJ9goAtadrN6xTGX0UDEwp/N4A==", "dev": true, "license": "MIT", "dependencies": { "@jridgewell/remapping": "^2.3.5", - "enhanced-resolve": "^5.21.0", - "jiti": "^2.6.1", + "enhanced-resolve": "5.21.6", + "jiti": "^2.7.0", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", - "tailwindcss": "4.3.0" + "tailwindcss": "4.3.1" } }, "node_modules/@tailwindcss/oxide": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.0.tgz", - "integrity": "sha512-F7HZGBeN9I0/AuuJS5PwcD8xayx5ri5GhjYUDBEVYUkexyA/giwbDNjRVrxSezE3T250OU2K/wp/ltWx3UOefg==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.1.tgz", + "integrity": "sha512-yVPyo8RNkabVr3O2EhHEE0Rewu7YKzc1DhIqfL46LKveFrmu9XbDazNOJY7/GRuvw1h6u3utWnR29H/p5JPlgA==", "dev": true, "license": "MIT", "engines": { "node": ">= 20" }, "optionalDependencies": { - "@tailwindcss/oxide-android-arm64": "4.3.0", - "@tailwindcss/oxide-darwin-arm64": "4.3.0", - "@tailwindcss/oxide-darwin-x64": "4.3.0", - "@tailwindcss/oxide-freebsd-x64": "4.3.0", - "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.0", - "@tailwindcss/oxide-linux-arm64-gnu": "4.3.0", - "@tailwindcss/oxide-linux-arm64-musl": "4.3.0", - "@tailwindcss/oxide-linux-x64-gnu": "4.3.0", - "@tailwindcss/oxide-linux-x64-musl": "4.3.0", - "@tailwindcss/oxide-wasm32-wasi": "4.3.0", - "@tailwindcss/oxide-win32-arm64-msvc": "4.3.0", - "@tailwindcss/oxide-win32-x64-msvc": "4.3.0" + "@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" } }, "node_modules/@tailwindcss/oxide-android-arm64": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.0.tgz", - "integrity": "sha512-TJPiq67tKlLuObP6RkwvVGDoxCMBVtDgKkLfa/uyj7/FyxvQwHS+UOnVrXXgbEsfUaMgiVvC4KbJnRr26ho4Ng==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.1.tgz", + "integrity": "sha512-SVlyf61g374l5cHyg8x9kf5xmLcOaxvOTsbsqDnSsDJaKOEFZ7GCvi84VAVGpxojYOs1+3K6M0UjXfqPU8vmOQ==", "cpu": [ "arm64" ], @@ -517,9 +517,9 @@ } }, "node_modules/@tailwindcss/oxide-darwin-arm64": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.0.tgz", - "integrity": "sha512-oMN/WZRb+SO37BmUElEgeEWuU8E/HXRkiODxJxLe1UTHVXLrdVSgfaJV7pSlhRGMSOiXLuxTIjfsF3wYvz8cgQ==", + "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==", "cpu": [ "arm64" ], @@ -534,9 +534,9 @@ } }, "node_modules/@tailwindcss/oxide-darwin-x64": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.0.tgz", - "integrity": "sha512-N6CUmu4a6bKVADfw77p+iw6Yd9Q3OBhe0veaDX+QazfuVYlQsHfDgxBrsjQ/IW+zywL8mTrNd0SdJT/zgtvMdA==", + "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==", "cpu": [ "x64" ], @@ -551,9 +551,9 @@ } }, "node_modules/@tailwindcss/oxide-freebsd-x64": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.0.tgz", - "integrity": "sha512-zDL5hBkQdH5C6MpqbK3gQAgP80tsMwSI26vjOzjJtNCMUo0lFgOItzHKBIupOZNQxt3ouPH7RPhvNhiTfCe5CQ==", + "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==", "cpu": [ "x64" ], @@ -568,9 +568,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.0.tgz", - "integrity": "sha512-R06HdNi7A7OEoMsf6d4tjZ71RCWnZQPHj2mnotSFURjNLdBC+cIgXQ7l81CqeoiQftjf6OOblxXMInMgN2VzMA==", + "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==", "cpu": [ "arm" ], @@ -585,9 +585,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.0.tgz", - "integrity": "sha512-qTJHELX8jetjhRQHCLilkVLmybpzNQAtaI/gaoVoidn/ufbNDbAo8KlK2J+yPoc8wQxvDxCmh/5lr8nC1+lTbg==", + "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==", "cpu": [ "arm64" ], @@ -605,9 +605,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm64-musl": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.0.tgz", - "integrity": "sha512-Z6sukiQsngnWO+l39X4pPbiWT81IC+PLKF+PHxIlyZbGNb9MODfYlXEVlFvej5BOZInWX01kVyzeLvHsXhfczQ==", + "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==", "cpu": [ "arm64" ], @@ -625,9 +625,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-x64-gnu": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.0.tgz", - "integrity": "sha512-DRNdQRpSGzRGfARVuVkxvM8Q12nh19l4BF/G7zGA1oe+9wcC6saFBHTISrpIcKzhiXtSrlSrluCfvMuledoCTQ==", + "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==", "cpu": [ "x64" ], @@ -645,9 +645,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-x64-musl": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.0.tgz", - "integrity": "sha512-Z0IADbDo8bh6I7h2IQMx601AdXBLfFpEdUotft86evd/8ZPflZe9COPO8Q1vw+pfLWIUo9zN/JGZvwuAJqduqg==", + "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==", "cpu": [ "x64" ], @@ -665,9 +665,9 @@ } }, "node_modules/@tailwindcss/oxide-wasm32-wasi": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.0.tgz", - "integrity": "sha512-HNZGOUxEmElksYR7S6sC5jTeNGpobAsy9u7Gu0AskJ8/20FR9GqebUyB+HBcU/ax6BHuiuJi+Oda4B+YX6H1yA==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.1.tgz", + "integrity": "sha512-zsM8uOeqvVGHsAXsJxsT28ttosFahLJKCLOTUBqRAtKnVgGSRitds9T432QiT8b77Yga7JIBkulIRRlJPtYhRA==", "bundleDependencies": [ "@napi-rs/wasm-runtime", "@emnapi/core", @@ -687,7 +687,7 @@ "@emnapi/runtime": "^1.10.0", "@emnapi/wasi-threads": "^1.2.1", "@napi-rs/wasm-runtime": "^1.1.4", - "@tybys/wasm-util": "^0.10.1", + "@tybys/wasm-util": "^0.10.2", "tslib": "^2.8.1" }, "engines": { @@ -695,9 +695,9 @@ } }, "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.0.tgz", - "integrity": "sha512-Pe+RPVTi1T+qymuuRpcdvwSVZjnll/f7n8gBxMMh3xLTctMDKqpdfGimbMyioqtLhUYZxdJ9wGNhV7MKHvgZsQ==", + "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==", "cpu": [ "arm64" ], @@ -712,9 +712,9 @@ } }, "node_modules/@tailwindcss/oxide-win32-x64-msvc": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.0.tgz", - "integrity": "sha512-Mvrf2kXW/yeW/OTezZlCGOirXRcUuLIBx/5Y12BaPM7wJoryG6dfS/NJL8aBPqtTEx/Vm4T4vKzFUcKDT+TKUA==", + "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==", "cpu": [ "x64" ], @@ -729,15 +729,15 @@ } }, "node_modules/@tailwindcss/vite": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.3.0.tgz", - "integrity": "sha512-t6J3OrB5Fc0ExuhohouH0fWUGMYL6PTLhW+E7zIk/pdbnJARZDCwjBznFnkh5ynRnIRSI4YjtTH0t6USjJISrw==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.3.1.tgz", + "integrity": "sha512-hItDHuIIlEV61R+faXu66s1K36aTurO/Qw0e45Vskz57gXl9pWOT6eg3zmcEui6CZXddbN7zd41bwmvag4JGwQ==", "dev": true, "license": "MIT", "dependencies": { - "@tailwindcss/node": "4.3.0", - "@tailwindcss/oxide": "4.3.0", - "tailwindcss": "4.3.0" + "@tailwindcss/node": "4.3.1", + "@tailwindcss/oxide": "4.3.1", + "tailwindcss": "4.3.1" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7 || ^8" @@ -779,9 +779,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "25.9.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.1.tgz", - "integrity": "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==", + "version": "25.9.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.3.tgz", + "integrity": "sha512-603BddQMv3pUcr4U2dhujk83N2tTDVr/34wII2B6bJy6g+8WD6yUb11jszNs0gdi4PesVWl7ABt8nYMVpnLUcg==", "dev": true, "license": "MIT", "dependencies": { @@ -789,9 +789,9 @@ } }, "node_modules/@types/react": { - "version": "19.2.15", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.15.tgz", - "integrity": "sha512-eRwcGNHve+E8qtEQSSRl6urh+rFop4v8gm6O8rGv25CodbvFdLjA1vVQ1KkiFE0w0UPOnb8tDiFKL5lp0rtY5Q==", + "version": "19.2.17", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz", + "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==", "dev": true, "license": "MIT", "dependencies": { @@ -905,9 +905,9 @@ } }, "node_modules/enhanced-resolve": { - "version": "5.22.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.22.0.tgz", - "integrity": "sha512-xYcDWrpELkFzz9SpZ3PlI6Eu6eD93Yf0WLDRxikGhWJ3MAir2SNZTIVCVZqZ/NUyx8AdMc2gT9C0gPiw18kG+A==", + "version": "5.21.6", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.6.tgz", + "integrity": "sha512-aNnGCvbJ/RIyWo1IuhNdVjnNF+EjH9wpzpNHt+ci/m9He9LJvUN8wrCcXjp9cWsGNAuvSpVFTx/vraAFQ8qGjQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1258,9 +1258,9 @@ } }, "node_modules/lucide-react": { - "version": "1.16.0", - "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.16.0.tgz", - "integrity": "sha512-dYwyPzb4MEKpGUmNYk3WKWPnMrHs3FKM+q94kAnJrcDIqqn1hq2xY8scaS2ovsOCM5D51ey2gaRG3PBb1vgoYQ==", + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.18.0.tgz", + "integrity": "sha512-LZDb7H/0YfM+RJncD0hDQRCAu+vSGODqpe35TuVI8EuXaRjkczbsx7p8dY4J87F/MUSj6bpYqeI8nw8qXaAdmA==", "license": "ISC", "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" @@ -1329,13 +1329,13 @@ } }, "node_modules/playwright": { - "version": "1.60.0", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.60.0.tgz", - "integrity": "sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA==", + "version": "1.61.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.61.0.tgz", + "integrity": "sha512-Z+7BeeqQPRRzklHsVFP4KTGIyMxKUmfeRA4WisM6G3/XW6nwGeX6fX9qYaDa+CiUqpOkb2f6X3nar05R3kSuJQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.60.0" + "playwright-core": "1.61.0" }, "bin": { "playwright": "cli.js" @@ -1348,9 +1348,9 @@ } }, "node_modules/playwright-core": { - "version": "1.60.0", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.60.0.tgz", - "integrity": "sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==", + "version": "1.61.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.61.0.tgz", + "integrity": "sha512-caX7TrY3Ml6egyDX0WUcTHDxodl/b51y5wJOdCEA36QviK/s2g081hvmGs8eaE3DWb6NYZQ6BjO/QkNRPenoPA==", "dev": true, "license": "Apache-2.0", "bin": { @@ -1390,34 +1390,34 @@ } }, "node_modules/react": { - "version": "19.2.6", - "resolved": "https://registry.npmjs.org/react/-/react-19.2.6.tgz", - "integrity": "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==", + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz", + "integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==", "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/react-dom": { - "version": "19.2.6", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.6.tgz", - "integrity": "sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g==", + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz", + "integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==", "license": "MIT", "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { - "react": "^19.2.6" + "react": "^19.2.7" } }, "node_modules/rolldown": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.2.tgz", - "integrity": "sha512-oZx5zVDtVB44AW3eaifgDml1gWRDZGvjcfdxonE4swNPG98PrrXjaO/KrnUjzlMnztCCRVlUueA1kCXhARGk6g==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz", + "integrity": "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==", "dev": true, "license": "MIT", "dependencies": { - "@oxc-project/types": "=0.132.0", + "@oxc-project/types": "=0.133.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.2", - "@rolldown/binding-darwin-arm64": "1.0.2", - "@rolldown/binding-darwin-x64": "1.0.2", - "@rolldown/binding-freebsd-x64": "1.0.2", - "@rolldown/binding-linux-arm-gnueabihf": "1.0.2", - "@rolldown/binding-linux-arm64-gnu": "1.0.2", - "@rolldown/binding-linux-arm64-musl": "1.0.2", - "@rolldown/binding-linux-ppc64-gnu": "1.0.2", - "@rolldown/binding-linux-s390x-gnu": "1.0.2", - "@rolldown/binding-linux-x64-gnu": "1.0.2", - "@rolldown/binding-linux-x64-musl": "1.0.2", - "@rolldown/binding-openharmony-arm64": "1.0.2", - "@rolldown/binding-wasm32-wasi": "1.0.2", - "@rolldown/binding-win32-arm64-msvc": "1.0.2", - "@rolldown/binding-win32-x64-msvc": "1.0.2" + "@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" } }, "node_modules/scheduler": { @@ -1461,9 +1461,9 @@ } }, "node_modules/tailwindcss": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.0.tgz", - "integrity": "sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q==", + "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==", "dev": true, "license": "MIT" }, @@ -1488,9 +1488,9 @@ "license": "MIT" }, "node_modules/tinyglobby": { - "version": "0.2.16", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", - "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", "dev": true, "license": "MIT", "dependencies": { @@ -1534,17 +1534,17 @@ "license": "MIT" }, "node_modules/vite": { - "version": "8.0.14", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.14.tgz", - "integrity": "sha512-s4BJJ+5y1pYL6Otw51FHhVJQhPnuRinKig64g/1+EUNaJsd3gCKdD31IPFvswUgW9/60QT9oFHbZHbQK5imcxw==", + "version": "8.0.16", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.16.tgz", + "integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==", "dev": true, "license": "MIT", "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", "postcss": "^8.5.15", - "rolldown": "1.0.2", - "tinyglobby": "^0.2.16" + "rolldown": "1.0.3", + "tinyglobby": "^0.2.17" }, "bin": { "vite": "bin/vite.js" diff --git a/package.json b/package.json index 0d86de8..711a7bd 100644 --- a/package.json +++ b/package.json @@ -1,34 +1,36 @@ { "name": "voltviz", "private": true, - "version": "0.18.0", + "version": "0.19.0", "type": "module", "scripts": { "dev": "vite --port=3000 --host=0.0.0.0", "build": "vite build", "preview": "vite preview", "clean": "rm -rf dist", - "lint": "tsc --noEmit" + "lint": "tsc --noEmit", + "test": "playwright test", + "test:install": "playwright install chromium" }, "dependencies": { - "@sendspin/sendspin-js": "^3.1.0", + "@sendspin/sendspin-js": "^3.2.0", "d3-geo": "^3.1.1", - "lucide-react": "^1.9.0", - "react": "^19.2.6", - "react-dom": "^19.2.6", + "lucide-react": "^1.18.0", + "react": "^19.2.7", + "react-dom": "^19.2.7", "three": "^0.184.0" }, "devDependencies": { - "@playwright/test": "^1.60.0", - "@tailwindcss/vite": "^4.3.0", + "@playwright/test": "^1.61.0", + "@tailwindcss/vite": "^4.3.1", "@types/d3-geo": "^3.1.0", - "@types/node": "^25.7.0", - "@types/react": "^19.2.14", + "@types/node": "^25.9.3", + "@types/react": "^19.2.17", "@types/react-dom": "^19.2.3", "@types/three": "^0.184.1", - "@vitejs/plugin-react": "^6.0.1", - "tailwindcss": "^4.3.0", + "@vitejs/plugin-react": "^6.0.2", + "tailwindcss": "^4.3.1", "typescript": "~6.0.3", - "vite": "^8.0.12" + "vite": "^8.0.16" } } diff --git a/src/App.tsx b/src/App.tsx index fad78c7..3a6379c 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -56,7 +56,9 @@ type VisualizerType = | 'aurumleaf' | 'anunakisphere' | 'trailsstream' - | 'shambhala'; + | 'shambhala' + | 'holoblinds' + | 'insidequantum'; type VisualizerProps = { stream: MediaStream; @@ -133,6 +135,8 @@ const visualizerComponents: Record import('./components/visualizers/AnunakiSphere')), trailsstream: lazy(() => import('./components/visualizers/TrailsStream')), shambhala: lazy(() => import('./components/visualizers/Shambhala')), + holoblinds: lazy(() => import('./components/visualizers/HoloBlinds')), + insidequantum: lazy(() => import('./components/visualizers/InsideQuantum')), }; export default function App() { @@ -476,6 +480,8 @@ export default function App() { + + diff --git a/src/components/visualizers/HoloBlinds.tsx b/src/components/visualizers/HoloBlinds.tsx new file mode 100644 index 0000000..18544bc --- /dev/null +++ b/src/components/visualizers/HoloBlinds.tsx @@ -0,0 +1,316 @@ +import { useEffect, useRef } from 'react'; +import * as THREE from 'three'; +import { VisualizerSettings } from '../../types'; + +interface Props { + stream: MediaStream; + settings: VisualizerSettings; +} + +const vertexShader = ` + void main() { + gl_Position = vec4(position, 1.0); + } +`; + +const fragmentShader = ` + uniform float uPhase; + uniform float uTwist1Phase; + uniform float uTwist2Phase; + uniform float uTwistReact; + uniform vec2 uResolution; + + uniform vec3 uColor1; + uniform vec3 uColor2; + uniform vec3 uColor3; + + uniform float uGlobalRotX; + uniform float uGlobalRotY; + uniform float uCoreSquash; + uniform float uCoreRadius; + uniform float uRayCorrection; + + uniform float uL1Twist; + uniform float uL1Scale; + uniform float uL1ScaleY; + uniform float uL1Thickness; + + uniform float uL2Twist; + uniform float uL2Scale; + uniform float uL2ScaleY; + uniform float uL2Thickness; + + uniform float uStepMult; + uniform float uStepMin; + + #define MAX_STEPS 42 + #define MAX_DIST 12.8 + + mat2 rot(float a) { + float s = sin(a), c = cos(a); + return mat2(c, -s, s, c); + } + + float map(vec3 p) { + p.xz *= rot(uPhase * uGlobalRotX); + p.yz *= rot(uPhase * uGlobalRotY); + + float core = length(vec3(p.x, p.y * uCoreSquash, p.z)) - uCoreRadius; + + vec3 q = p; + q.xz *= rot(q.y * uL1Twist + uTwist1Phase + uTwistReact); + q *= uL1Scale; + q.y *= uL1ScaleY; + + float g1 = dot(sin(q), cos(q.yzx)); + float r1 = abs(g1) - uL1Thickness; + + vec3 q2 = p; + q2.xz *= rot(q2.y * uL2Twist + uTwist2Phase + uTwistReact * 0.7); + q2 *= uL2Scale; + q2.y *= uL2ScaleY; + + float g2 = dot(sin(q2), cos(q2.yzx)); + float r2 = abs(g2) - uL2Thickness; + + float rays = max(r1, r2); + rays *= uRayCorrection; + + return max(core, rays); + } + + void main() { + vec2 uv = (gl_FragCoord.xy - 0.5 * uResolution) / uResolution.y; + + vec3 ro = vec3(0.0, 0.0, -4.5); + vec3 rd = normalize(vec3(uv, 1.0)); + + float t = uPhase; + float rayT = 0.0; + vec3 glow = vec3(0.0); + + for (int i = 0; i < MAX_STEPS; i++) { + vec3 p = ro + rd * rayT; + float d = map(p); + + float mixFactor1 = sin(p.y * 2.0 + p.x * 1.5 - t) * 0.5 + 0.5; + vec3 rayColor = mix(uColor1, uColor2, mixFactor1); + + float mixFactor2 = sin(p.z * 3.0 + t * 2.0) * 0.5 + 0.5; + rayColor = mix(rayColor, uColor3, mixFactor2); + + float intensity = 0.0018 / (abs(d) + 0.005); + glow += rayColor * intensity; + + rayT += max(abs(d) * uStepMult, uStepMin); + + if (rayT > MAX_DIST) break; + } + + vec3 finalColor = glow * 0.7; + finalColor = smoothstep(0.0, 1.1, finalColor); + finalColor = pow(finalColor, vec3(1.05)); + finalColor *= 1.0 - length(uv) * 0.5; + + gl_FragColor = vec4(finalColor, 1.0); + } +`; + +// Base constants matching the original params +const BASE_SPEED = 0.7; +const BASE_CORE_RADIUS = 2.8322; +const BASE_L1_TWIST_SPEED = 0.2; +const BASE_L2_TWIST_SPEED = -0.4; +const BASE_L1_THICKNESS = 0.1; +const BASE_L2_THICKNESS = 0.06; + +export default function HoloBlinds({ stream, settings }: Props) { + const containerRef = useRef(null); + const animationRef = useRef(null); + const audioCtxRef = useRef(null); + const sourceRef = useRef(null); + const settingsRef = useRef(settings); + + useEffect(() => { + settingsRef.current = settings; + }, [settings]); + + useEffect(() => { + if (!containerRef.current) return; + + const container = containerRef.current; + const w = container.clientWidth; + const h = container.clientHeight; + + // Audio setup + const audioCtx = new (window.AudioContext || (window as any).webkitAudioContext)(); + audioCtxRef.current = audioCtx; + + const analyser = audioCtx.createAnalyser(); + analyser.fftSize = 2048; + analyser.smoothingTimeConstant = 0.8; + + const source = audioCtx.createMediaStreamSource(stream); + source.connect(analyser); + sourceRef.current = source; + + const freqBins = analyser.frequencyBinCount; + const freqData = new Uint8Array(freqBins); + + // Three.js setup + const DPR = Math.min(window.devicePixelRatio, 1.0) * 0.8; + + const renderer = new THREE.WebGLRenderer({ antialias: false }); + renderer.setPixelRatio(DPR); + renderer.setSize(w, h); + while (container.firstChild) container.removeChild(container.firstChild); + container.appendChild(renderer.domElement); + + const scene = new THREE.Scene(); + const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1); + + // Pre-compute base HSL values for hueShift support + const baseColor1 = new THREE.Color(0.349, 0.000, 1.000); + const baseColor2 = new THREE.Color(1.000, 0.251, 0.000); + const baseColor3 = new THREE.Color(0.000, 0.482, 1.000); + const hsl1 = { h: 0, s: 0, l: 0 }; + const hsl2 = { h: 0, s: 0, l: 0 }; + const hsl3 = { h: 0, s: 0, l: 0 }; + baseColor1.getHSL(hsl1); + baseColor2.getHSL(hsl2); + baseColor3.getHSL(hsl3); + + const uniforms = { + uPhase: { value: 0.0 }, + uTwist1Phase: { value: 0.0 }, + uTwist2Phase: { value: 0.0 }, + uTwistReact: { value: 0.0 }, + uResolution: { value: new THREE.Vector2(w * DPR, h * DPR) }, + uColor1: { value: new THREE.Vector3(0.349, 0.000, 1.000) }, + uColor2: { value: new THREE.Vector3(1.000, 0.251, 0.000) }, + uColor3: { value: new THREE.Vector3(0.000, 0.482, 1.000) }, + uGlobalRotX: { value: 0.1 }, + uGlobalRotY: { value: 0.03 }, + uCoreSquash: { value: -0.2 }, + uCoreRadius: { value: BASE_CORE_RADIUS }, + uRayCorrection: { value: 0.35 }, + uL1Twist: { value: 43.1 }, + uL1Scale: { value: -0.2 }, + uL1ScaleY: { value: -0.21 }, + uL1Thickness: { value: BASE_L1_THICKNESS }, + uL2Twist: { value: 0.3 }, + uL2Scale: { value: 1.5 }, + uL2ScaleY: { value: 0.0 }, + uL2Thickness: { value: BASE_L2_THICKNESS }, + uStepMult: { value: 0.48 }, + uStepMin: { value: 0.017 }, + }; + + const material = new THREE.ShaderMaterial({ + vertexShader, + fragmentShader, + uniforms, + depthWrite: false, + depthTest: false, + }); + + const quad = new THREE.Mesh(new THREE.PlaneGeometry(2, 2), material); + scene.add(quad); + + const clock = new THREE.Clock(); + let smoothedBass = 0; + let smoothedMids = 0; + let smoothedHighs = 0; + let globalPhase = 0; + let twist1Phase = 0; + let twist2Phase = 0; + const tmpColor = new THREE.Color(); + + const bandEnergy = (start: number, end: number): number => { + let sum = 0; + for (let i = start; i < end; i++) sum += freqData[i]; + return end > start ? sum / ((end - start) * 255) : 0; + }; + + const draw = () => { + animationRef.current = requestAnimationFrame(draw); + const s = settingsRef.current; + + analyser.getByteFrequencyData(freqData); + const sampleRate = audioCtx.sampleRate; + const binHz = sampleRate / analyser.fftSize; + + const bassEnd = Math.min(Math.floor(250 / binHz), freqBins); + const midEnd = Math.min(Math.floor(2000 / binHz), freqBins); + + const bass = bandEnergy(0, bassEnd) * s.sensitivity; + const mids = bandEnergy(bassEnd, midEnd) * s.sensitivity; + const highs = bandEnergy(midEnd, freqBins) * s.sensitivity; + + smoothedBass += (bass - smoothedBass) * 0.15; + smoothedMids += (mids - smoothedMids) * 0.12; + smoothedHighs += (highs - smoothedHighs) * 0.10; + + const delta = clock.getDelta(); + + // Accumulate phases: audio modulates the rate, not a multiplier on elapsed time + const currentSpeed = BASE_SPEED * s.speed * (1.0 + smoothedBass * 0.25); + const midBoost = 1.0 + smoothedMids * 0.6; + globalPhase += delta * currentSpeed; + twist1Phase += delta * currentSpeed * BASE_L1_TWIST_SPEED * midBoost; + twist2Phase += delta * currentSpeed * BASE_L2_TWIST_SPEED * midBoost; + + uniforms.uPhase.value = globalPhase; + uniforms.uTwist1Phase.value = twist1Phase; + uniforms.uTwist2Phase.value = twist2Phase; + + // Direct audio-reactive twist offset (time-independent) + uniforms.uTwistReact.value = smoothedMids * 3.0 + smoothedBass * 1.5; + + // scale: map to core radius around the base value + const scaleClamped = Math.max(0.5, Math.min(3.0, s.scale)); + uniforms.uCoreRadius.value = BASE_CORE_RADIUS * scaleClamped; + + // bass -> L1 thickness pulse + uniforms.uL1Thickness.value = BASE_L1_THICKNESS + smoothedBass * 0.08; + + // highs -> L2 detail/thickness + uniforms.uL2Thickness.value = BASE_L2_THICKNESS + smoothedHighs * 0.06; + + // hueShift: offset hue of all three base colors + const hueDelta = s.hueShift / 360; + tmpColor.setHSL((hsl1.h + hueDelta + 1) % 1, hsl1.s, hsl1.l); + uniforms.uColor1.value.set(tmpColor.r, tmpColor.g, tmpColor.b); + tmpColor.setHSL((hsl2.h + hueDelta + 1) % 1, hsl2.s, hsl2.l); + uniforms.uColor2.value.set(tmpColor.r, tmpColor.g, tmpColor.b); + tmpColor.setHSL((hsl3.h + hueDelta + 1) % 1, hsl3.s, hsl3.l); + uniforms.uColor3.value.set(tmpColor.r, tmpColor.g, tmpColor.b); + + renderer.render(scene, camera); + }; + + draw(); + + const handleResize = () => { + const width = container.clientWidth; + const height = container.clientHeight; + renderer.setSize(width, height); + uniforms.uResolution.value.set(width * DPR, height * DPR); + }; + window.addEventListener('resize', handleResize); + + return () => { + window.removeEventListener('resize', handleResize); + if (animationRef.current) cancelAnimationFrame(animationRef.current); + if (sourceRef.current) sourceRef.current.disconnect(); + if (audioCtxRef.current && audioCtxRef.current.state !== 'closed') { + audioCtxRef.current.close(); + } + quad.geometry.dispose(); + material.dispose(); + renderer.dispose(); + }; + }, [stream]); + + return
; +} diff --git a/src/components/visualizers/InsideQuantum.tsx b/src/components/visualizers/InsideQuantum.tsx new file mode 100644 index 0000000..b41ff10 --- /dev/null +++ b/src/components/visualizers/InsideQuantum.tsx @@ -0,0 +1,344 @@ +import { useEffect, useRef } from 'react'; +import * as THREE from 'three'; +import { VisualizerSettings } from '../../types'; + +interface Props { + stream: MediaStream; + settings: VisualizerSettings; +} + +const vertexShader = ` + void main() { + gl_Position = vec4(position, 1.0); + } +`; + +const fragmentShader = ` + uniform float u_time; + uniform float u_rotPhaseX; + uniform float u_rotPhaseY; + uniform float u_warpReact; + uniform vec2 u_resolution; + + uniform vec3 u_color1; + uniform vec3 u_color2; + uniform vec3 u_color3; + + uniform float u_warpFreq; + uniform float u_warpAmp; + uniform vec3 u_fold; + uniform vec3 u_rot; + uniform float u_scaleMult; + uniform float u_scaleAccum; + uniform vec3 u_rays; + + vec3 orbitTrap; + + float hash(vec2 p) { + vec3 p3 = fract(vec3(p.xyx) * 0.1031); + p3 += dot(p3, p3.yzx + 33.33); + return fract((p3.x + p3.y) * p3.z); + } + + mat2 rot(float a) { + float s = sin(a), c = cos(a); + return mat2(c, -s, s, c); + } + + float map(vec3 p) { + p.xz *= rot(u_rotPhaseX); + p.xy *= rot(u_rotPhaseY); + + vec3 q = p; + float scale = 0.26; + orbitTrap = vec3(1000.0); + + for (int i = 1; i < 2; i++) { + q += sin(q.zxy * u_warpFreq) * (u_warpAmp + u_warpReact); + q = abs(q) - u_fold; + q.xy *= rot(u_rot.x); + q.xz *= rot(u_rot.y); + q.yz *= rot(u_rot.z); + q *= u_scaleMult; + scale *= u_scaleAccum; + orbitTrap = min(orbitTrap, abs(q)); + } + + float raysX = length(q.yz) - u_rays.x; + float raysY = length(q.xz) - u_rays.y; + float raysZ = length(q.xy) - u_rays.z; + + float k = 0.1; + + float h1 = clamp(6.7 + 0.1 * (raysX - raysY) / k, 0.1, 0.4); + float mergedRays = mix(raysX, raysY, h1) - k * h1 * (1.0 - h1); + + float h2 = clamp(0.1 + 0.1 * (mergedRays - raysZ) / k, 0.0, 0.8); + mergedRays = mix(mergedRays, raysZ, h2) - k * h2 * (-7.3 - h2); + + float core = length(p) - 0.44; + + mergedRays /= scale; + + float h3 = clamp(0.5 + 0.5 * (core - mergedRays) / 0.3, 0.0, 1.0); + float d = mix(core, mergedRays, h3) - 0.3 * h3 * (1.0 - h3); + + return d * 0.3; + } + + void main() { + vec2 uv = (gl_FragCoord.xy - 0.5 * u_resolution.xy) / u_resolution.y; + + vec3 ro = vec3(0.0, 0.0, -4.5); + vec3 rd = normalize(vec3(uv, 1.0)); + + float t = hash(gl_FragCoord.xy + mod(u_time, 100.0) * 10.0) * 0.05; + + vec3 finalGlow = vec3(0.0); + + for (int i = 0; i < 90; i++) { + vec3 p = ro + rd * t; + float d = map(p); + + vec3 stepColor = mix(u_color1, u_color2, smoothstep(0.0, 1.0, orbitTrap.x)); + stepColor = mix(stepColor, u_color3, smoothstep(0.0, 1.0, orbitTrap.y)); + + float glowIntensity = 0.0035 / (abs(d) + 0.005); + finalGlow += stepColor * glowIntensity; + + t += abs(d) * 0.45 + 0.015; + if (t > 10.0) break; + } + + float vignette = 1.0 - dot(uv, uv) * 0.4; + finalGlow *= vignette; + + finalGlow = (finalGlow * (2.38 * finalGlow + -0.04)) / (finalGlow * (2.35 * finalGlow + 1.52) + 0.14); + + float grain = hash(gl_FragCoord.xy + mod(u_time, 1000.0) * 15.0); + finalGlow += (grain - 0.5) * 0.10; + + float colorDither = (hash(gl_FragCoord.xy + 123.456) * 2.0 - 1.0) / 255.0; + finalGlow += colorDither; + + gl_FragColor = vec4(finalGlow, 1.0); + } +`; + +// Convert RGB float array [r,g,b] (0-1) to HSL {h,s,l} +function rgbToHsl(r: number, g: number, b: number): { h: number; s: number; l: number } { + const max = Math.max(r, g, b), min = Math.min(r, g, b); + const l = (max + min) / 2; + if (max === min) return { h: 0, s: 0, l }; + const d = max - min; + const s = l > 0.5 ? d / (2 - max - min) : d / (max + min); + let h = 0; + if (max === r) h = ((g - b) / d + (g < b ? 6 : 0)) / 6; + else if (max === g) h = ((b - r) / d + 2) / 6; + else h = ((r - g) / d + 4) / 6; + return { h, s, l }; +} + +// Convert HSL to RGB float array [r,g,b] (0-1) +function hslToRgb(h: number, s: number, l: number): [number, number, number] { + if (s === 0) return [l, l, l]; + const hue2rgb = (p: number, q: number, t: number) => { + if (t < 0) t += 1; + if (t > 1) t -= 1; + if (t < 1 / 6) return p + (q - p) * 6 * t; + if (t < 1 / 2) return q; + if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6; + return p; + }; + const q = l < 0.5 ? l * (1 + s) : l + s - l * s; + const p = 2 * l - q; + return [hue2rgb(p, q, h + 1 / 3), hue2rgb(p, q, h), hue2rgb(p, q, h - 1 / 3)]; +} + +export default function InsideQuantum({ stream, settings }: Props) { + const containerRef = useRef(null); + const animationRef = useRef(null); + const audioCtxRef = useRef(null); + const sourceRef = useRef(null); + const settingsRef = useRef(settings); + + useEffect(() => { + settingsRef.current = settings; + }, [settings]); + + useEffect(() => { + if (!containerRef.current) return; + + const container = containerRef.current; + const w = container.clientWidth; + const h = container.clientHeight; + + // Audio setup + const audioCtx = new (window.AudioContext || (window as unknown as { webkitAudioContext: typeof AudioContext }).webkitAudioContext)(); + audioCtxRef.current = audioCtx; + + const analyser = audioCtx.createAnalyser(); + analyser.fftSize = 2048; + analyser.smoothingTimeConstant = 0.7; + + const source = audioCtx.createMediaStreamSource(stream); + source.connect(analyser); + sourceRef.current = source; + + const bufferLength = analyser.frequencyBinCount; + const dataArray = new Uint8Array(bufferLength); + + // Renderer + const renderer = new THREE.WebGLRenderer({ antialias: false, powerPreference: 'high-performance' }); + renderer.setSize(w, h); + renderer.setPixelRatio(Math.min(window.devicePixelRatio, 1.0)); + while (container.firstChild) container.removeChild(container.firstChild); + container.appendChild(renderer.domElement); + + const scene = new THREE.Scene(); + const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0.1, 10); + camera.position.z = 1; + + // Base colors (green palette from the original) + const baseColor1: [number, number, number] = [0.016, 1.000, 0.000]; + const baseColor2: [number, number, number] = [0.784, 1.000, 0.000]; + const baseColor3: [number, number, number] = [0.000, 0.620, 0.239]; + + const BASE_WARP_AMP = 0.29; + const BASE_CAM_ROT_X = 0.15; + const BASE_CAM_ROT_Y = 0.10; + const BASE_RAY_X = 0.30; + const BASE_RAY_Y = 0.25; + const BASE_RAY_Z = 0.55; + const BASE_SCALE_MULT = 2.65; + + const uniforms = { + u_time: { value: 0.0 }, + u_rotPhaseX: { value: 0.0 }, + u_rotPhaseY: { value: 0.0 }, + u_warpReact: { value: 0.0 }, + u_resolution: { value: new THREE.Vector2(w, h) }, + u_color1: { value: new THREE.Vector3(...baseColor1) }, + u_color2: { value: new THREE.Vector3(...baseColor2) }, + u_color3: { value: new THREE.Vector3(...baseColor3) }, + u_warpFreq: { value: 56.3 }, + u_warpAmp: { value: BASE_WARP_AMP }, + u_fold: { value: new THREE.Vector3(0.5, 0.4, 0.4) }, + u_rot: { value: new THREE.Vector3(0.2, 1.6, -2.9) }, + u_scaleMult: { value: BASE_SCALE_MULT }, + u_scaleAccum: { value: 7.48 }, + u_rays: { value: new THREE.Vector3(BASE_RAY_X, BASE_RAY_Y, BASE_RAY_Z) }, + }; + + const material = new THREE.ShaderMaterial({ + vertexShader, + fragmentShader, + uniforms, + depthWrite: false, + depthTest: false, + }); + + const geometry = new THREE.PlaneGeometry(2, 2); + const plane = new THREE.Mesh(geometry, material); + scene.add(plane); + + const clock = new THREE.Clock(); + let smoothedBass = 0; + let smoothedMids = 0; + let smoothedHighs = 0; + let rotPhaseX = 0; + let rotPhaseY = 0; + + const draw = () => { + animationRef.current = requestAnimationFrame(draw); + const s = settingsRef.current; + + // Audio analysis + analyser.getByteFrequencyData(dataArray); + const sampleRate = audioCtx.sampleRate; + const binHz = sampleRate / analyser.fftSize; + + const bassEnd = Math.min(Math.floor(250 / binHz), bufferLength); + const midEnd = Math.min(Math.floor(2000 / binHz), bufferLength); + + const bandEnergy = (start: number, end: number) => { + let sum = 0; + for (let i = start; i < end; i++) sum += dataArray[i]; + return end > start ? (sum / ((end - start) * 255)) : 0; + }; + + const bass = bandEnergy(0, bassEnd) * s.sensitivity; + const mids = bandEnergy(bassEnd, midEnd) * s.sensitivity; + const highs = bandEnergy(midEnd, bufferLength) * s.sensitivity; + + smoothedBass += (bass - smoothedBass) * 0.2; + smoothedMids += (mids - smoothedMids) * 0.15; + smoothedHighs += (highs - smoothedHighs) * 0.15; + + // Update time and accumulate rotation phases + const delta = clock.getDelta(); + uniforms.u_time.value += delta * s.speed; + + const midBoost = 1.0 + smoothedMids * 2.0; + rotPhaseX += delta * s.speed * BASE_CAM_ROT_X * midBoost; + rotPhaseY += delta * s.speed * BASE_CAM_ROT_Y * midBoost; + uniforms.u_rotPhaseX.value = rotPhaseX; + uniforms.u_rotPhaseY.value = rotPhaseY; + + // Audio-reactive: bass drives warp amplitude + uniforms.u_warpAmp.value = BASE_WARP_AMP * (1.0 + smoothedBass * 1.5); + + // Direct audio-reactive warp offset (time-independent) + uniforms.u_warpReact.value = smoothedMids * 0.15 + smoothedBass * 0.1; + + // Audio-reactive: highs drive ray thickness + uniforms.u_rays.value.set( + BASE_RAY_X * (1.0 + smoothedHighs * 1.0), + BASE_RAY_Y * (1.0 + smoothedHighs * 1.0), + BASE_RAY_Z * (1.0 + smoothedHighs * 0.8), + ); + + // Scale controls fractal zoom/intensity + uniforms.u_scaleMult.value = BASE_SCALE_MULT * s.scale; + + // Hue shift: rotate all three base colors + const hueDelta = s.hueShift / 360; + const applyHueShift = (base: [number, number, number]): [number, number, number] => { + const hsl = rgbToHsl(base[0], base[1], base[2]); + return hslToRgb((hsl.h + hueDelta) % 1, hsl.s, hsl.l); + }; + const c1 = applyHueShift(baseColor1); + const c2 = applyHueShift(baseColor2); + const c3 = applyHueShift(baseColor3); + uniforms.u_color1.value.set(c1[0], c1[1], c1[2]); + uniforms.u_color2.value.set(c2[0], c2[1], c2[2]); + uniforms.u_color3.value.set(c3[0], c3[1], c3[2]); + + renderer.render(scene, camera); + }; + + draw(); + + const handleResize = () => { + const width = container.clientWidth; + const height = container.clientHeight; + renderer.setSize(width, height); + uniforms.u_resolution.value.set(width, height); + }; + window.addEventListener('resize', handleResize); + + return () => { + window.removeEventListener('resize', handleResize); + if (animationRef.current) cancelAnimationFrame(animationRef.current); + if (sourceRef.current) sourceRef.current.disconnect(); + if (audioCtxRef.current && audioCtxRef.current.state !== 'closed') { + audioCtxRef.current.close(); + } + geometry.dispose(); + material.dispose(); + renderer.dispose(); + }; + }, [stream]); + + return
; +}