feat: add 6 new audio visualizers

- Vortex: spiral particles pulled toward center, speed reacts to beat
- Starfield: flying through stars with depth parallax and streak effects
- Grid: 3D wave plane with ripple effects from mouse and audio
- Galaxy: 3-arm spiral galaxy with tilted perspective
- Waveform: circular audio waveform in concentric rings
- Kaleidoscope: 8-segment mirrored geometric patterns

All visualizers include:
- GLSL shaders with audio reactivity (low/mid/high frequencies)
- Mouse tracking for interactive parallax
- Beat-synchronized animations

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-30 10:10:54 +01:00
parent 25008e3385
commit 8d9d47cea7
7 changed files with 869 additions and 1 deletions

View File

@@ -0,0 +1,158 @@
/**
* 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;

View File

@@ -0,0 +1,134 @@
/**
* 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;

View File

@@ -0,0 +1,152 @@
/**
* Kaleidoscope Visualizer
* Mirrored geometric patterns that morph with audio
*/
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 aSegment;
attribute float aLayer;
attribute float aProgress;
varying float vAlpha;
varying float vSegment;
void main() {
// Kaleidoscope parameters
float segments = 8.0;
float segmentAngle = 6.28318 / segments;
// Base angle for this segment
float baseAngle = aSegment * segmentAngle;
// Create pattern within segment
float patternRadius = 10.0 + aLayer * 15.0 + uLow * 10.0;
float patternAngle = aProgress * segmentAngle * 0.8; // Stay within segment
// Mirror effect - alternate segments are flipped
float mirror = mod(aSegment, 2.0) < 1.0 ? 1.0 : -1.0;
patternAngle *= mirror;
// Add time-based morphing
float morph = sin(uTime * 2.0 + aLayer) * (5.0 + uMid * 10.0);
patternRadius += morph;
// Final angle
float angle = baseAngle + patternAngle + uTime * 0.2;
// High frequencies add shimmer
float shimmer = sin(aProgress * 20.0 + uTime * 8.0) * uHigh * 3.0;
patternRadius += shimmer;
// Calculate position
float x = cos(angle) * patternRadius;
float y = sin(angle) * patternRadius;
float z = sin(aProgress * 6.28 + uTime * 2.0) * (3.0 + uLow * 5.0);
// Mouse offset
x -= uMouseX * 5.0;
y -= uMouseY * 4.0;
vec3 pos = vec3(x, y, z);
vec4 mvPosition = modelViewMatrix * vec4(pos, 1.0);
// Size varies with layer
float size = 2.0 + aLayer * 0.5 + uLow * 1.0;
gl_PointSize = size * (300.0 / -mvPosition.z);
// Alpha - inner layers brighter
vAlpha = 0.8 - aLayer * 0.1 + uLow * 0.2;
vSegment = aSegment;
gl_Position = projectionMatrix * mvPosition;
}
`;
const fragmentShader = `
varying float vAlpha;
varying float vSegment;
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 KaleidoscopeVisualizer extends BaseVisualizer {
static name = 'Kaleidoscope';
constructor(scene) {
super(scene);
this.segments = 8;
this.layers = 5;
this.pointsPerSegmentLayer = 30;
this.init();
}
init() {
const count = this.segments * this.layers * this.pointsPerSegmentLayer;
const positions = new Float32Array(count * 3);
const segmentIndices = new Float32Array(count);
const layerIndices = new Float32Array(count);
const progress = new Float32Array(count);
let idx = 0;
for (let seg = 0; seg < this.segments; seg++) {
for (let layer = 0; layer < this.layers; layer++) {
for (let p = 0; p < this.pointsPerSegmentLayer; p++) {
positions[idx * 3] = 0;
positions[idx * 3 + 1] = 0;
positions[idx * 3 + 2] = 0;
segmentIndices[idx] = seg;
layerIndices[idx] = layer;
progress[idx] = p / this.pointsPerSegmentLayer;
idx++;
}
}
}
const geometry = new THREE.BufferGeometry();
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
geometry.setAttribute('aSegment', new THREE.BufferAttribute(segmentIndices, 1));
geometry.setAttribute('aLayer', new THREE.BufferAttribute(layerIndices, 1));
geometry.setAttribute('aProgress', new THREE.BufferAttribute(progress, 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 KaleidoscopeVisualizer;

View File

@@ -7,12 +7,24 @@ import * as THREE from 'three';
import { SphereVisualizer } from './particles.js';
import { TunnelVisualizer } from './tunnel.js';
import { HelixVisualizer } from './helix.js';
import { VortexVisualizer } from './vortex.js';
import { StarfieldVisualizer } from './starfield.js';
import { GridVisualizer } from './grid.js';
import { GalaxyVisualizer } from './galaxy.js';
import { WaveformVisualizer } from './waveform.js';
import { KaleidoscopeVisualizer } from './kaleidoscope.js';
// Available visualizer classes
const VISUALIZERS = [
SphereVisualizer,
TunnelVisualizer,
HelixVisualizer
HelixVisualizer,
VortexVisualizer,
StarfieldVisualizer,
GridVisualizer,
GalaxyVisualizer,
WaveformVisualizer,
KaleidoscopeVisualizer
];
export class Visualizer {

View File

@@ -0,0 +1,133 @@
/**
* 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;

View File

@@ -0,0 +1,138 @@
/**
* Vortex Visualizer
* Spiral of particles being pulled into a center point
*/
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 aRadius;
attribute float aSpeed;
varying float vAlpha;
varying float vRadius;
void main() {
// Spiral motion - particles orbit and get pulled inward
float time = uTime * (0.5 + uMid * 2.0);
float angle = aAngle + time * aSpeed;
// Radius pulses with bass, particles get pulled in
float radiusPull = 1.0 - uLow * 0.3;
float radius = aRadius * radiusPull;
// Add some vertical motion based on radius
float z = sin(angle * 3.0 + uTime) * 5.0 * (1.0 - aRadius / 50.0);
z += uLow * 10.0; // Bounce toward viewer
// Calculate position
float x = cos(angle) * radius;
float y = sin(angle) * radius;
// High frequencies add turbulence
x += sin(aAngle * 10.0 + uTime * 5.0) * uHigh * 3.0;
y += cos(aAngle * 10.0 + uTime * 5.0) * uHigh * 3.0;
// 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 radius - larger in center
float size = 3.0 * (1.0 - aRadius / 60.0) + uLow * 2.0;
gl_PointSize = size * (300.0 / -mvPosition.z);
// Alpha - brighter toward center
vAlpha = 0.8 - aRadius / 80.0 + uLow * 0.2;
vRadius = aRadius;
gl_Position = projectionMatrix * mvPosition;
}
`;
const fragmentShader = `
varying float vAlpha;
varying float vRadius;
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;
// Pure white
vec3 color = vec3(1.0);
gl_FragColor = vec4(color, alpha);
}
`;
export class VortexVisualizer extends BaseVisualizer {
static name = 'Vortex';
constructor(scene) {
super(scene);
this.particleCount = 3000;
this.init();
}
init() {
const positions = new Float32Array(this.particleCount * 3);
const angles = new Float32Array(this.particleCount);
const radii = new Float32Array(this.particleCount);
const speeds = new Float32Array(this.particleCount);
for (let i = 0; i < this.particleCount; i++) {
// Distribute particles in a disk
const angle = Math.random() * Math.PI * 2;
const radius = 5 + Math.random() * 45; // 5 to 50
positions[i * 3] = 0;
positions[i * 3 + 1] = 0;
positions[i * 3 + 2] = 0;
angles[i] = angle;
radii[i] = radius;
// Faster rotation for particles closer to center
speeds[i] = 0.5 + (1.0 - radius / 50.0) * 1.5;
}
const geometry = new THREE.BufferGeometry();
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
geometry.setAttribute('aAngle', new THREE.BufferAttribute(angles, 1));
geometry.setAttribute('aRadius', new THREE.BufferAttribute(radii, 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 VortexVisualizer;

View File

@@ -0,0 +1,141 @@
/**
* 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;