/** * Particle System (Sphere) Visualizer * Audio-reactive 3D particle cloud */ import * as THREE from 'three'; import { BaseVisualizer } from './base-visualizer.js'; 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; varying float vAlpha; void main() { vec3 pos = position; // Base rotation float angle = uTime * 0.2; mat3 rotation = mat3( cos(angle), 0.0, sin(angle), 0.0, 1.0, 0.0, -sin(angle), 0.0, cos(angle) ); pos = rotation * pos; // Bass affects scale/expansion float bassScale = 1.0 + uLow * 0.4; pos *= bassScale; // Mid frequencies create wave motion float wave = sin(pos.x * 0.1 + uTime * 2.0) * uMid * 8.0; pos.y += wave * aRandom; // High frequencies add jitter/sparkle 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); // Point size with perspective float size = uSize * aScale; size *= (1.0 + uLow * 0.5); // Pulse with bass gl_PointSize = size * (300.0 / -mvPosition.z); // Alpha based on distance and audio vAlpha = 0.6 + uLow * 0.3; gl_Position = projectionMatrix * mvPosition; } `; const fragmentShader = ` varying float vAlpha; void main() { // Circular point with soft edge float dist = length(gl_PointCoord - vec2(0.5)); if (dist > 0.5) discard; float alpha = 1.0 - smoothstep(0.2, 0.5, dist); alpha *= vAlpha; // Pure white color for minimal aesthetic gl_FragColor = vec4(1.0, 1.0, 1.0, alpha); } `; export class SphereVisualizer extends BaseVisualizer { static name = 'Sphere'; constructor(scene, count = 5000) { super(scene); this.count = count; this.init(); } init() { const positions = new Float32Array(this.count * 3); const randoms = new Float32Array(this.count); const scales = new Float32Array(this.count); for (let i = 0; i < this.count; i++) { const i3 = i * 3; // Spherical distribution const radius = 20 + Math.random() * 30; const theta = Math.random() * Math.PI * 2; const phi = Math.acos(2 * Math.random() - 1); positions[i3] = radius * Math.sin(phi) * Math.cos(theta); positions[i3 + 1] = radius * Math.sin(phi) * Math.sin(theta); positions[i3 + 2] = radius * Math.cos(phi); randoms[i] = Math.random(); scales[i] = 0.5 + Math.random() * 1.5; } 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)); 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, 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; this.material.uniforms.uMouseX.value = mouse.x; this.material.uniforms.uMouseY.value = mouse.y; } } // Legacy export for backwards compatibility export class ParticleSystem extends SphereVisualizer { constructor(count = 5000) { super(null, count); } } export default SphereVisualizer;