feat(visualizer): add multiple visualizers with mouse tracking and beat response

- Add BaseVisualizer class for shared visualizer logic
- Add Helix visualizer (DNA helix with rungs, beat compression/bounce)
- Add Tunnel visualizer (ring tunnel with depth parallax)
- Refactor SphereVisualizer to extend BaseVisualizer
- Add mouse tracking to all visualizers (canvas-relative, centered)
- Add visualizer switching via logo click (persisted to localStorage)
- Add beat-responsive bouncing and compression effects

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-29 21:11:38 +01:00
parent d46e54b592
commit e50c8a2503
9 changed files with 609 additions and 55 deletions

View File

@@ -1,17 +1,19 @@
/**
* Particle System
* Particle System (Sphere) Visualizer
* Audio-reactive 3D particle cloud
*/
import * as THREE from 'three';
import { BaseVisualizer } from './base-visualizer.js';
// Vertex Shader
const vertexShader = `
uniform float uTime;
uniform float uLow;
uniform float uMid;
uniform float uHigh;
uniform float uSize;
uniform float uMouseX;
uniform float uMouseY;
attribute float aRandom;
attribute float aScale;
@@ -42,6 +44,10 @@ void main() {
vec3 jitter = normalize(pos) * uHigh * aRandom * 5.0;
pos += jitter;
// Mouse offset
pos.x -= uMouseX * 3.0;
pos.y -= uMouseY * 2.0;
// Calculate view position
vec4 mvPosition = modelViewMatrix * vec4(pos, 1.0);
@@ -57,7 +63,6 @@ void main() {
}
`;
// Fragment Shader
const fragmentShader = `
varying float vAlpha;
@@ -74,17 +79,16 @@ void main() {
}
`;
export class ParticleSystem {
constructor(count = 5000) {
export class SphereVisualizer extends BaseVisualizer {
static name = 'Sphere';
constructor(scene, count = 5000) {
super(scene);
this.count = count;
this.createGeometry();
this.createMaterial();
this.mesh = new THREE.Points(this.geometry, this.material);
this.init();
}
createGeometry() {
this.geometry = new THREE.BufferGeometry();
init() {
const positions = new Float32Array(this.count * 3);
const randoms = new Float32Array(this.count);
const scales = new Float32Array(this.count);
@@ -105,41 +109,37 @@ export class ParticleSystem {
scales[i] = 0.5 + Math.random() * 1.5;
}
this.geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
this.geometry.setAttribute('aRandom', new THREE.BufferAttribute(randoms, 1));
this.geometry.setAttribute('aScale', new THREE.BufferAttribute(scales, 1));
}
const geometry = new THREE.BufferGeometry();
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
geometry.setAttribute('aRandom', new THREE.BufferAttribute(randoms, 1));
geometry.setAttribute('aScale', new THREE.BufferAttribute(scales, 1));
createMaterial() {
this.material = new THREE.ShaderMaterial({
vertexShader,
fragmentShader,
uniforms: {
uTime: { value: 0 },
uLow: { value: 0 },
uMid: { value: 0 },
uHigh: { value: 0 },
uSize: { value: 2.0 }
},
transparent: true,
blending: THREE.AdditiveBlending,
depthWrite: false
this.material = this.createShaderMaterial(vertexShader, fragmentShader, {
uSize: { value: 2.0 },
uMouseX: { value: 0 },
uMouseY: { value: 0 }
});
this.mesh = new THREE.Points(geometry, this.material);
}
update(bands, time) {
update(bands, time, mouse = { x: 0, y: 0 }) {
if (!this.material) return;
this.material.uniforms.uTime.value = time;
this.material.uniforms.uLow.value = bands.low || 0;
this.material.uniforms.uMid.value = bands.mid || 0;
this.material.uniforms.uHigh.value = bands.high || 0;
}
dispose() {
this.geometry?.dispose();
this.material?.dispose();
this.material.uniforms.uMouseX.value = mouse.x;
this.material.uniforms.uMouseY.value = mouse.y;
}
}
export default ParticleSystem;
// Legacy export for backwards compatibility
export class ParticleSystem extends SphereVisualizer {
constructor(count = 5000) {
super(null, count);
}
}
export default SphereVisualizer;