/** * Starfield Visualizer * Flying through stars that streak and pulse with the beat */ 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 aSize; attribute float aSpeed; varying float vAlpha; varying float vStreak; void main() { vec3 pos = position; // Stars fly toward camera (positive Z) float speed = (10.0 + uLow * 30.0) * aSpeed; float z = mod(pos.z + uTime * speed, 200.0) - 100.0; pos.z = z; // Depth factor - stars closer are brighter and larger float depthFactor = (z + 100.0) / 200.0; // Slight drift based on position pos.x += sin(uTime * 0.5 + position.x * 0.1) * 2.0; pos.y += cos(uTime * 0.5 + position.y * 0.1) * 2.0; // Mouse parallax - closer stars move more pos.x -= uMouseX * 10.0 * (1.0 - depthFactor); pos.y -= uMouseY * 8.0 * (1.0 - depthFactor); vec4 mvPosition = modelViewMatrix * vec4(pos, 1.0); // Size increases as stars approach float size = aSize * (1.0 - depthFactor) * 3.0; size *= (1.0 + uLow * 0.5); gl_PointSize = size * (300.0 / -mvPosition.z); // Alpha and streak based on depth and speed vAlpha = (1.0 - depthFactor) * 0.9 + uHigh * 0.2; vStreak = speed * 0.02; // For potential streak effect gl_Position = projectionMatrix * mvPosition; } `; const fragmentShader = ` varying float vAlpha; varying float vStreak; void main() { vec2 uv = gl_PointCoord - vec2(0.5); float dist = length(uv); if (dist > 0.5) discard; // Core glow float alpha = 1.0 - smoothstep(0.0, 0.5, dist); alpha *= vAlpha; // Add slight elongation effect for speed float streak = 1.0 - smoothstep(0.0, 0.3, abs(uv.y)); alpha *= mix(1.0, streak, min(vStreak, 0.5)); vec3 color = vec3(1.0); gl_FragColor = vec4(color, alpha); } `; export class StarfieldVisualizer extends BaseVisualizer { static name = 'Starfield'; constructor(scene) { super(scene); this.starCount = 2000; this.init(); } init() { const positions = new Float32Array(this.starCount * 3); const sizes = new Float32Array(this.starCount); const speeds = new Float32Array(this.starCount); for (let i = 0; i < this.starCount; i++) { // Distribute stars in a cylinder around the camera const angle = Math.random() * Math.PI * 2; const radius = 10 + Math.random() * 60; positions[i * 3] = Math.cos(angle) * radius; positions[i * 3 + 1] = Math.sin(angle) * radius; positions[i * 3 + 2] = Math.random() * 200 - 100; // -100 to 100 sizes[i] = 1.0 + Math.random() * 3.0; speeds[i] = 0.5 + Math.random() * 1.0; } const geometry = new THREE.BufferGeometry(); geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3)); geometry.setAttribute('aSize', new THREE.BufferAttribute(sizes, 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 StarfieldVisualizer;