Files
pivoine.art/assets/js/visualizer/scene.js
Sebastian Krüger 8d9d47cea7 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>
2025-11-30 10:10:54 +01:00

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;