- Add BaseVisualizer class for shared visualizer logic - Add Helix visualizer (DNA helix with rungs, beat compression/bounce) - Add Tunnel visualizer (ring tunnel with depth parallax) - Refactor SphereVisualizer to extend BaseVisualizer - Add mouse tracking to all visualizers (canvas-relative, centered) - Add visualizer switching via logo click (persisted to localStorage) - Add beat-responsive bouncing and compression effects 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
147 lines
4.0 KiB
JavaScript
147 lines
4.0 KiB
JavaScript
/**
|
|
* 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;
|