/** * Waveform Circle Visualizer * Audio waveform wrapped in a circle - classic visualizer style */ 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 aRing; varying float vAlpha; varying float vRing; void main() { // Base radius for each ring float baseRadius = 15.0 + aRing * 12.0; // Waveform displacement based on angle and audio float waveFreq = 8.0 + aRing * 4.0; float wave = sin(aAngle * waveFreq + uTime * 3.0) * (uMid * 8.0 + 2.0); wave += cos(aAngle * waveFreq * 0.5 - uTime * 2.0) * (uLow * 6.0); wave += sin(aAngle * waveFreq * 2.0 + uTime * 5.0) * (uHigh * 4.0); float radius = baseRadius + wave; // Calculate position float x = cos(aAngle) * radius; float y = sin(aAngle) * radius; float z = sin(aAngle * 3.0 + uTime) * 3.0 + uLow * 8.0; // Slow rotation float rotAngle = uTime * 0.1; float cosR = cos(rotAngle); float sinR = sin(rotAngle); float newX = x * cosR - y * sinR; float newY = x * sinR + y * cosR; x = newX; y = newY; // 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 audio intensity float size = 2.5 + uLow * 1.5 + abs(wave) * 0.1; gl_PointSize = size * (300.0 / -mvPosition.z); // Alpha - outer rings slightly dimmer vAlpha = 0.8 - aRing * 0.15 + uLow * 0.2; vRing = aRing; gl_Position = projectionMatrix * mvPosition; } `; const fragmentShader = ` varying float vAlpha; varying float vRing; 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 WaveformVisualizer extends BaseVisualizer { static name = 'Waveform'; constructor(scene) { super(scene); this.rings = 4; this.pointsPerRing = 200; this.init(); } init() { const count = this.rings * this.pointsPerRing; const positions = new Float32Array(count * 3); const angles = new Float32Array(count); const rings = new Float32Array(count); let idx = 0; for (let ring = 0; ring < this.rings; ring++) { for (let i = 0; i < this.pointsPerRing; i++) { const angle = (i / this.pointsPerRing) * Math.PI * 2; positions[idx * 3] = 0; positions[idx * 3 + 1] = 0; positions[idx * 3 + 2] = 0; angles[idx] = angle; rings[idx] = ring; idx++; } } const geometry = new THREE.BufferGeometry(); geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3)); geometry.setAttribute('aAngle', new THREE.BufferAttribute(angles, 1)); geometry.setAttribute('aRing', new THREE.BufferAttribute(rings, 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 WaveformVisualizer;