/** * Particle System * Audio-reactive 3D particle cloud */ import * as THREE from 'three'; // Vertex Shader const vertexShader = ` uniform float uTime; uniform float uLow; uniform float uMid; uniform float uHigh; uniform float uSize; 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; // 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; } `; // Fragment Shader 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 ParticleSystem { constructor(count = 5000) { this.count = count; this.createGeometry(); this.createMaterial(); this.mesh = new THREE.Points(this.geometry, this.material); } createGeometry() { this.geometry = new THREE.BufferGeometry(); 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; } 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)); } 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 }); } update(bands, time) { 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(); } } export default ParticleSystem;