- 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>
220 lines
5.3 KiB
JavaScript
220 lines
5.3 KiB
JavaScript
/**
|
|
* WebGL Visualizer Scene
|
|
* Three.js scene manager with multiple selectable visualizers
|
|
*/
|
|
|
|
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,
|
|
VortexVisualizer,
|
|
StarfieldVisualizer,
|
|
GridVisualizer,
|
|
GalaxyVisualizer,
|
|
WaveformVisualizer,
|
|
KaleidoscopeVisualizer
|
|
];
|
|
|
|
export class Visualizer {
|
|
constructor(canvas, audioManager) {
|
|
this.canvas = canvas;
|
|
this.audioManager = audioManager;
|
|
this.running = false;
|
|
this.time = 0;
|
|
|
|
// Mouse tracking (normalized -1 to 1)
|
|
this.mouse = { x: 0, y: 0 };
|
|
this.mouseActive = false;
|
|
|
|
// Visualizer management
|
|
this.visualizers = [];
|
|
this.currentIndex = 0;
|
|
|
|
if (!canvas) {
|
|
console.warn('Visualizer: No canvas provided');
|
|
return;
|
|
}
|
|
|
|
this.init();
|
|
}
|
|
|
|
init() {
|
|
// Scene setup
|
|
this.scene = new THREE.Scene();
|
|
|
|
// Camera
|
|
this.camera = new THREE.PerspectiveCamera(
|
|
75,
|
|
window.innerWidth / window.innerHeight,
|
|
0.1,
|
|
1000
|
|
);
|
|
this.camera.position.z = 50;
|
|
|
|
// Renderer
|
|
this.renderer = new THREE.WebGLRenderer({
|
|
canvas: this.canvas,
|
|
alpha: true,
|
|
antialias: true,
|
|
powerPreference: 'high-performance'
|
|
});
|
|
this.renderer.setSize(window.innerWidth, window.innerHeight);
|
|
this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
|
|
|
// Initialize all visualizers
|
|
this.visualizers = VISUALIZERS.map(VisualizerClass => {
|
|
return new VisualizerClass(this.scene);
|
|
});
|
|
|
|
// Load saved visualizer preference
|
|
const savedIndex = localStorage.getItem('pivoine-visualizer');
|
|
if (savedIndex !== null) {
|
|
this.currentIndex = parseInt(savedIndex, 10) % this.visualizers.length;
|
|
}
|
|
|
|
// Show the current visualizer
|
|
this.visualizers[this.currentIndex].show();
|
|
|
|
// Event listeners
|
|
window.addEventListener('resize', () => this.resize());
|
|
window.addEventListener('mousemove', (e) => this.onMouseMove(e));
|
|
|
|
// Start animation
|
|
this.start();
|
|
}
|
|
|
|
/**
|
|
* Get list of visualizer names
|
|
*/
|
|
getVisualizerNames() {
|
|
return VISUALIZERS.map(V => V.name);
|
|
}
|
|
|
|
/**
|
|
* Get current visualizer name
|
|
*/
|
|
getCurrentName() {
|
|
return VISUALIZERS[this.currentIndex].name;
|
|
}
|
|
|
|
/**
|
|
* Get current visualizer index
|
|
*/
|
|
getCurrentIndex() {
|
|
return this.currentIndex;
|
|
}
|
|
|
|
/**
|
|
* Set active visualizer by index
|
|
*/
|
|
setVisualizer(index) {
|
|
if (index < 0 || index >= this.visualizers.length) return;
|
|
if (index === this.currentIndex) return;
|
|
|
|
// Hide current
|
|
this.visualizers[this.currentIndex].hide();
|
|
|
|
// Show new
|
|
this.currentIndex = index;
|
|
this.visualizers[this.currentIndex].show();
|
|
|
|
// Save preference
|
|
localStorage.setItem('pivoine-visualizer', index.toString());
|
|
}
|
|
|
|
/**
|
|
* Cycle to next visualizer
|
|
*/
|
|
nextVisualizer() {
|
|
const nextIndex = (this.currentIndex + 1) % this.visualizers.length;
|
|
this.setVisualizer(nextIndex);
|
|
return this.getCurrentName();
|
|
}
|
|
|
|
/**
|
|
* Cycle to previous visualizer
|
|
*/
|
|
prevVisualizer() {
|
|
const prevIndex = (this.currentIndex - 1 + this.visualizers.length) % this.visualizers.length;
|
|
this.setVisualizer(prevIndex);
|
|
return this.getCurrentName();
|
|
}
|
|
|
|
onMouseMove(e) {
|
|
// Normalize to -1 to 1 relative to canvas
|
|
const rect = this.canvas.getBoundingClientRect();
|
|
this.mouse.x = ((e.clientX - rect.left) / rect.width) * 2 - 1;
|
|
this.mouse.y = -((e.clientY - rect.top) / rect.height) * 2 + 1;
|
|
this.mouseActive = true;
|
|
}
|
|
|
|
resize() {
|
|
if (!this.camera || !this.renderer) return;
|
|
|
|
this.camera.aspect = window.innerWidth / window.innerHeight;
|
|
this.camera.updateProjectionMatrix();
|
|
this.renderer.setSize(window.innerWidth, window.innerHeight);
|
|
}
|
|
|
|
start() {
|
|
if (this.running) return;
|
|
this.running = true;
|
|
this.animate();
|
|
}
|
|
|
|
pause() {
|
|
this.running = false;
|
|
}
|
|
|
|
resume() {
|
|
this.start();
|
|
}
|
|
|
|
animate() {
|
|
if (!this.running) return;
|
|
requestAnimationFrame(() => this.animate());
|
|
|
|
this.time += 0.01;
|
|
|
|
// Get audio frequency bands
|
|
let bands = { low: 0, mid: 0, high: 0 };
|
|
if (this.audioManager?.isInitialized) {
|
|
bands = this.audioManager.getFrequencyBands();
|
|
}
|
|
|
|
// Update current visualizer with audio data and mouse position
|
|
const currentViz = this.visualizers[this.currentIndex];
|
|
if (currentViz) {
|
|
const mouse = this.mouseActive ? this.mouse : { x: 0, y: 0 };
|
|
currentViz.update(bands, this.time, mouse);
|
|
}
|
|
|
|
// Subtle camera movement
|
|
this.camera.position.x = Math.sin(this.time * 0.1) * 3;
|
|
this.camera.position.y = Math.cos(this.time * 0.15) * 2;
|
|
this.camera.lookAt(0, 0, 0);
|
|
|
|
this.renderer.render(this.scene, this.camera);
|
|
}
|
|
|
|
destroy() {
|
|
this.running = false;
|
|
this.visualizers.forEach(v => v.dispose());
|
|
this.renderer?.dispose();
|
|
}
|
|
}
|
|
|
|
export default Visualizer;
|