/** * Waveform Tunnel Visualizer * Infinite tunnel of pulsing rings */ 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 aRingIndex; attribute float aAngle; varying float vAlpha; varying float vRingIndex; void main() { vec3 pos = position; // Ring depth position (z movement for tunnel effect) float depth = mod(pos.z + uTime * 12.0, 200.0) - 100.0; pos.z = depth; // Distance from camera affects ring behavior float depthFactor = 1.0 - (depth + 100.0) / 200.0; // Bass expands rings - stronger pulse float bassExpand = 1.0 + uLow * 0.8 * depthFactor; pos.x *= bassExpand; pos.y *= bassExpand; // Bounce toward viewer on bass pos.z += uLow * 15.0; // Mid creates wave distortion on ring float wave = sin(aAngle * 4.0 + uTime * 3.0) * uMid * 2.0 * depthFactor; float waveRadius = 1.0 + wave * 0.3; pos.x *= waveRadius; pos.y *= waveRadius; // High adds subtle jitter pos.x += sin(aAngle * 8.0 + uTime * 5.0) * uHigh * 0.5; pos.y += cos(aAngle * 8.0 + uTime * 5.0) * uHigh * 0.5; // Mouse offset - stronger for closer rings pos.x -= uMouseX * 5.0 * depthFactor; pos.y -= uMouseY * 4.0 * depthFactor; vec4 mvPosition = modelViewMatrix * vec4(pos, 1.0); // Point size based on depth - larger for more coverage float size = 4.5 * (1.0 + uLow * 0.4); gl_PointSize = size * (250.0 / -mvPosition.z); // Alpha fades with depth vAlpha = 0.8 * depthFactor + uHigh * 0.2; vRingIndex = aRingIndex; gl_Position = projectionMatrix * mvPosition; } `; const fragmentShader = ` varying float vAlpha; varying float vRingIndex; 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; // Slight color variation based on ring vec3 color = vec3(1.0, 1.0, 1.0); gl_FragColor = vec4(color, alpha); } `; export class TunnelVisualizer extends BaseVisualizer { static name = 'Tunnel'; constructor(scene) { super(scene); this.ringCount = 100; this.pointsPerRing = 128; this.init(); } init() { const totalPoints = this.ringCount * this.pointsPerRing; const positions = new Float32Array(totalPoints * 3); const ringIndices = new Float32Array(totalPoints); const angles = new Float32Array(totalPoints); let idx = 0; for (let ring = 0; ring < this.ringCount; ring++) { const z = (ring / this.ringCount) * 200 - 100; // -100 to 100 const radius = 28 + Math.sin(ring * 0.25) * 5; // Larger varying radius for (let p = 0; p < this.pointsPerRing; p++) { const angle = (p / this.pointsPerRing) * Math.PI * 2; positions[idx * 3] = Math.cos(angle) * radius; positions[idx * 3 + 1] = Math.sin(angle) * radius; positions[idx * 3 + 2] = z; ringIndices[idx] = ring / this.ringCount; angles[idx] = angle; idx++; } } const geometry = new THREE.BufferGeometry(); geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3)); geometry.setAttribute('aRingIndex', new THREE.BufferAttribute(ringIndices, 1)); geometry.setAttribute('aAngle', new THREE.BufferAttribute(angles, 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 TunnelVisualizer;