/** * Galaxy Visualizer * Spiral arm formation with rotating particles */ 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 aArm; attribute float aOffset; attribute float aRadius; varying float vAlpha; varying float vArm; void main() { // Spiral arm parameters float arms = 3.0; float armAngle = aArm * (6.28318 / arms); // Radius with some variation float radius = aRadius * (1.0 + uLow * 0.3); // Spiral: angle increases with radius float spiralTightness = 0.15; float angle = armAngle + aRadius * spiralTightness + uTime * (0.2 + uMid * 0.5); // Add offset for thickness angle += aOffset * 0.3; float radiusOffset = aOffset * 3.0; // Calculate position float x = cos(angle) * (radius + radiusOffset); float y = sin(angle) * (radius + radiusOffset); // Height variation - thicker disk toward center float z = sin(aOffset * 3.0 + aRadius * 0.1) * (3.0 - aRadius * 0.05); z += uLow * 5.0; // Bounce with bass // High frequencies add sparkle x += sin(aOffset * 20.0 + uTime * 5.0) * uHigh * 2.0; y += cos(aOffset * 20.0 + uTime * 5.0) * uHigh * 2.0; // Tilt the galaxy float tiltX = 0.3; float cosX = cos(tiltX); float sinX = sin(tiltX); float newY = y * cosX - z * sinX; float newZ = y * sinX + z * cosX; y = newY; z = newZ; // Mouse offset x -= uMouseX * 5.0; y -= uMouseY * 4.0; vec3 pos = vec3(x, y, z); vec4 mvPosition = modelViewMatrix * vec4(pos, 1.0); // Size - larger toward center, pulses with bass float size = 2.5 * (1.0 - aRadius / 60.0) + uLow * 1.0; gl_PointSize = size * (300.0 / -mvPosition.z); // Alpha - brighter toward center and in arm cores vAlpha = (0.7 - aRadius / 80.0) * (1.0 - abs(aOffset) * 0.3) + uLow * 0.2; vArm = aArm; gl_Position = projectionMatrix * mvPosition; } `; const fragmentShader = ` varying float vAlpha; varying float vArm; void main() { float dist = length(gl_PointCoord - vec2(0.5)); if (dist > 0.5) discard; float alpha = 1.0 - smoothstep(0.0, 0.5, dist); alpha *= vAlpha; vec3 color = vec3(1.0); gl_FragColor = vec4(color, alpha); } `; export class GalaxyVisualizer extends BaseVisualizer { static name = 'Galaxy'; constructor(scene) { super(scene); this.particleCount = 4000; this.arms = 3; this.init(); } init() { const positions = new Float32Array(this.particleCount * 3); const armIndices = new Float32Array(this.particleCount); const offsets = new Float32Array(this.particleCount); const radii = new Float32Array(this.particleCount); for (let i = 0; i < this.particleCount; i++) { // Assign to an arm const arm = Math.floor(Math.random() * this.arms); // Radius - more particles toward center const radius = Math.pow(Math.random(), 0.5) * 50; // Offset from arm center const offset = (Math.random() - 0.5) * 2; positions[i * 3] = 0; positions[i * 3 + 1] = 0; positions[i * 3 + 2] = 0; armIndices[i] = arm; offsets[i] = offset; radii[i] = radius; } const geometry = new THREE.BufferGeometry(); geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3)); geometry.setAttribute('aArm', new THREE.BufferAttribute(armIndices, 1)); geometry.setAttribute('aOffset', new THREE.BufferAttribute(offsets, 1)); geometry.setAttribute('aRadius', new THREE.BufferAttribute(radii, 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 GalaxyVisualizer;