/** * 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;