/** * Wave Grid Visualizer * 3D plane of dots that ripple like water */ 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 aIndex; varying float vAlpha; varying float vHeight; void main() { vec3 pos = position; // Distance from center for wave calculations float dist = length(pos.xy); // Multiple wave sources float wave1 = sin(dist * 0.3 - uTime * 2.0) * (5.0 + uLow * 10.0); float wave2 = sin(dist * 0.5 - uTime * 3.0 + 1.0) * (3.0 + uMid * 5.0); float wave3 = cos(pos.x * 0.2 + uTime) * cos(pos.y * 0.2 + uTime) * (2.0 + uHigh * 4.0); // Combine waves for height float height = wave1 + wave2 + wave3; pos.z = height; // Mouse creates ripple effect float mouseDist = length(pos.xy - vec2(uMouseX * 30.0, uMouseY * 30.0)); pos.z += sin(mouseDist * 0.5 - uTime * 4.0) * 3.0 * (1.0 / (1.0 + mouseDist * 0.1)); // Rotate grid for better viewing angle float tiltAngle = 0.6; float cosT = cos(tiltAngle); float sinT = sin(tiltAngle); float newY = pos.y * cosT - pos.z * sinT; float newZ = pos.y * sinT + pos.z * cosT; pos.y = newY; pos.z = newZ - 20.0; // Push back vec4 mvPosition = modelViewMatrix * vec4(pos, 1.0); // Size pulses with bass float size = 2.5 + uLow * 1.5; gl_PointSize = size * (300.0 / -mvPosition.z); // Alpha based on height vAlpha = 0.6 + abs(height) * 0.02; vHeight = height; gl_Position = projectionMatrix * mvPosition; } `; const fragmentShader = ` varying float vAlpha; varying float vHeight; 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; vec3 color = vec3(1.0); gl_FragColor = vec4(color, alpha); } `; export class GridVisualizer extends BaseVisualizer { static name = 'Grid'; constructor(scene) { super(scene); this.gridSize = 40; // 40x40 grid this.spacing = 3; this.init(); } init() { const count = this.gridSize * this.gridSize; const positions = new Float32Array(count * 3); const indices = new Float32Array(count); let idx = 0; const offset = (this.gridSize - 1) * this.spacing / 2; for (let x = 0; x < this.gridSize; x++) { for (let y = 0; y < this.gridSize; y++) { positions[idx * 3] = x * this.spacing - offset; positions[idx * 3 + 1] = y * this.spacing - offset; positions[idx * 3 + 2] = 0; indices[idx] = idx; idx++; } } const geometry = new THREE.BufferGeometry(); geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3)); geometry.setAttribute('aIndex', new THREE.BufferAttribute(indices, 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 GridVisualizer;