/** * Vortex Visualizer * Spiral of particles being pulled into a center point */ 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 uMouseX; uniform float uMouseY; attribute float aAngle; attribute float aRadius; attribute float aSpeed; varying float vAlpha; varying float vRadius; void main() { // Spiral motion - particles orbit and get pulled inward float time = uTime * (0.5 + uMid * 2.0); float angle = aAngle + time * aSpeed; // Radius pulses with bass, particles get pulled in float radiusPull = 1.0 - uLow * 0.3; float radius = aRadius * radiusPull; // Add some vertical motion based on radius float z = sin(angle * 3.0 + uTime) * 5.0 * (1.0 - aRadius / 50.0); z += uLow * 10.0; // Bounce toward viewer // Calculate position float x = cos(angle) * radius; float y = sin(angle) * radius; // High frequencies add turbulence x += sin(aAngle * 10.0 + uTime * 5.0) * uHigh * 3.0; y += cos(aAngle * 10.0 + uTime * 5.0) * uHigh * 3.0; // Mouse offset x -= uMouseX * 5.0; y -= uMouseY * 4.0; vec3 pos = vec3(x, y, z); vec4 mvPosition = modelViewMatrix * vec4(pos, 1.0); // Size based on radius - larger in center float size = 3.0 * (1.0 - aRadius / 60.0) + uLow * 2.0; gl_PointSize = size * (300.0 / -mvPosition.z); // Alpha - brighter toward center vAlpha = 0.8 - aRadius / 80.0 + uLow * 0.2; vRadius = aRadius; gl_Position = projectionMatrix * mvPosition; } `; const fragmentShader = ` varying float vAlpha; varying float vRadius; void main() { float dist = length(gl_PointCoord - vec2(0.5)); if (dist > 0.5) discard; float alpha = 1.0 - smoothstep(0.1, 0.5, dist); alpha *= vAlpha; // Pure white vec3 color = vec3(1.0); gl_FragColor = vec4(color, alpha); } `; export class VortexVisualizer extends BaseVisualizer { static name = 'Vortex'; constructor(scene) { super(scene); this.particleCount = 3000; this.init(); } init() { const positions = new Float32Array(this.particleCount * 3); const angles = new Float32Array(this.particleCount); const radii = new Float32Array(this.particleCount); const speeds = new Float32Array(this.particleCount); for (let i = 0; i < this.particleCount; i++) { // Distribute particles in a disk const angle = Math.random() * Math.PI * 2; const radius = 5 + Math.random() * 45; // 5 to 50 positions[i * 3] = 0; positions[i * 3 + 1] = 0; positions[i * 3 + 2] = 0; angles[i] = angle; radii[i] = radius; // Faster rotation for particles closer to center speeds[i] = 0.5 + (1.0 - radius / 50.0) * 1.5; } const geometry = new THREE.BufferGeometry(); geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3)); geometry.setAttribute('aAngle', new THREE.BufferAttribute(angles, 1)); geometry.setAttribute('aRadius', new THREE.BufferAttribute(radii, 1)); geometry.setAttribute('aSpeed', new THREE.BufferAttribute(speeds, 1)); this.material = this.createShaderMaterial(vertexShader, fragmentShader, { 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; } } export default VortexVisualizer;