Initial commit
This commit is contained in:
181
assets/js/logo/reactive-logo.js
Normal file
181
assets/js/logo/reactive-logo.js
Normal file
@@ -0,0 +1,181 @@
|
||||
/**
|
||||
* Reactive Logo
|
||||
* Small WebGL canvas logo that reacts to audio and mouse
|
||||
*/
|
||||
|
||||
import * as THREE from 'three';
|
||||
|
||||
export class ReactiveLogo {
|
||||
constructor(canvas, audioManager) {
|
||||
this.canvas = canvas;
|
||||
this.audioManager = audioManager;
|
||||
this.mouse = { x: 0, y: 0 };
|
||||
this.running = false;
|
||||
this.time = 0;
|
||||
|
||||
if (!canvas) {
|
||||
console.warn('ReactiveLogo: No canvas provided');
|
||||
return;
|
||||
}
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
const size = 32;
|
||||
|
||||
// Scene
|
||||
this.scene = new THREE.Scene();
|
||||
|
||||
// Camera
|
||||
this.camera = new THREE.PerspectiveCamera(50, 1, 0.1, 100);
|
||||
this.camera.position.z = 3;
|
||||
|
||||
// Renderer
|
||||
this.renderer = new THREE.WebGLRenderer({
|
||||
canvas: this.canvas,
|
||||
alpha: true,
|
||||
antialias: true
|
||||
});
|
||||
this.renderer.setSize(size, size);
|
||||
this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
||||
|
||||
// Create logo geometry (simple points in a circular pattern)
|
||||
this.createLogoParticles();
|
||||
|
||||
// Mouse tracking
|
||||
this.canvas.addEventListener('mouseenter', () => this.onMouseEnter());
|
||||
this.canvas.addEventListener('mouseleave', () => this.onMouseLeave());
|
||||
window.addEventListener('mousemove', (e) => this.onMouseMove(e));
|
||||
|
||||
this.start();
|
||||
}
|
||||
|
||||
createLogoParticles() {
|
||||
const count = 50;
|
||||
const geometry = new THREE.BufferGeometry();
|
||||
const positions = new Float32Array(count * 3);
|
||||
const randoms = new Float32Array(count);
|
||||
|
||||
// Create points in a circular pattern
|
||||
for (let i = 0; i < count; i++) {
|
||||
const angle = (i / count) * Math.PI * 2;
|
||||
const radius = 0.8 + Math.random() * 0.2;
|
||||
|
||||
positions[i * 3] = Math.cos(angle) * radius;
|
||||
positions[i * 3 + 1] = Math.sin(angle) * radius;
|
||||
positions[i * 3 + 2] = (Math.random() - 0.5) * 0.2;
|
||||
|
||||
randoms[i] = Math.random();
|
||||
}
|
||||
|
||||
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
|
||||
geometry.setAttribute('aRandom', new THREE.BufferAttribute(randoms, 1));
|
||||
|
||||
const material = new THREE.ShaderMaterial({
|
||||
vertexShader: `
|
||||
uniform float uTime;
|
||||
uniform float uAudio;
|
||||
attribute float aRandom;
|
||||
|
||||
void main() {
|
||||
vec3 pos = position;
|
||||
|
||||
// Bouncy pulsing
|
||||
float pulse = 1.0 + sin(uTime * 5.0 + aRandom * 6.28) * 0.15;
|
||||
pulse += sin(uTime * 8.0 + aRandom * 3.14) * 0.1;
|
||||
pos *= pulse;
|
||||
|
||||
// Audio reactivity - bouncy
|
||||
float audioBounce = uAudio * (1.0 + sin(uTime * 10.0) * 0.3);
|
||||
pos *= 1.0 + audioBounce * 0.6;
|
||||
|
||||
vec4 mvPos = modelViewMatrix * vec4(pos, 1.0);
|
||||
gl_PointSize = 3.0 * (1.0 + uAudio * 0.8);
|
||||
gl_Position = projectionMatrix * mvPos;
|
||||
}
|
||||
`,
|
||||
fragmentShader: `
|
||||
void main() {
|
||||
float dist = length(gl_PointCoord - 0.5);
|
||||
if (dist > 0.5) discard;
|
||||
float alpha = 1.0 - smoothstep(0.2, 0.5, dist);
|
||||
gl_FragColor = vec4(1.0, 1.0, 1.0, alpha);
|
||||
}
|
||||
`,
|
||||
uniforms: {
|
||||
uTime: { value: 0 },
|
||||
uAudio: { value: 0 }
|
||||
},
|
||||
transparent: true,
|
||||
blending: THREE.AdditiveBlending,
|
||||
depthWrite: false
|
||||
});
|
||||
|
||||
this.logoMesh = new THREE.Points(geometry, material);
|
||||
this.scene.add(this.logoMesh);
|
||||
}
|
||||
|
||||
onMouseEnter() {
|
||||
this.isHovered = true;
|
||||
}
|
||||
|
||||
onMouseLeave() {
|
||||
this.isHovered = false;
|
||||
}
|
||||
|
||||
onMouseMove(e) {
|
||||
if (!this.isHovered) return;
|
||||
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;
|
||||
}
|
||||
|
||||
start() {
|
||||
if (this.running) return;
|
||||
this.running = true;
|
||||
this.animate();
|
||||
}
|
||||
|
||||
pause() {
|
||||
this.running = false;
|
||||
}
|
||||
|
||||
animate() {
|
||||
if (!this.running) return;
|
||||
requestAnimationFrame(() => this.animate());
|
||||
|
||||
this.time += 0.016;
|
||||
|
||||
// Get audio level
|
||||
let audioLevel = 0;
|
||||
if (this.audioManager?.isInitialized) {
|
||||
const bands = this.audioManager.getFrequencyBands();
|
||||
audioLevel = (bands.low + bands.mid + bands.high) / 3;
|
||||
}
|
||||
|
||||
// Update uniforms
|
||||
if (this.logoMesh?.material?.uniforms) {
|
||||
this.logoMesh.material.uniforms.uTime.value = this.time;
|
||||
this.logoMesh.material.uniforms.uAudio.value = audioLevel;
|
||||
}
|
||||
|
||||
// Rotate based on mouse
|
||||
if (this.logoMesh) {
|
||||
this.logoMesh.rotation.x += (this.mouse.y * 0.5 - this.logoMesh.rotation.x) * 0.1;
|
||||
this.logoMesh.rotation.y += (this.mouse.x * 0.5 - this.logoMesh.rotation.y) * 0.1;
|
||||
this.logoMesh.rotation.z = this.time * 0.2;
|
||||
}
|
||||
|
||||
this.renderer.render(this.scene, this.camera);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.running = false;
|
||||
this.logoMesh?.geometry?.dispose();
|
||||
this.logoMesh?.material?.dispose();
|
||||
this.renderer?.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
export default ReactiveLogo;
|
||||
257
assets/js/main.js
Normal file
257
assets/js/main.js
Normal file
@@ -0,0 +1,257 @@
|
||||
/**
|
||||
* Pivoine.art - Main JavaScript
|
||||
* Audio blog with WebGL visualizers
|
||||
*/
|
||||
|
||||
import { Visualizer } from './visualizer/scene.js';
|
||||
import { ReactiveLogo } from './logo/reactive-logo.js';
|
||||
|
||||
// Audio Manager - Web Audio API wrapper
|
||||
class AudioManager {
|
||||
constructor() {
|
||||
this.audio = document.createElement('audio');
|
||||
this.audio.crossOrigin = 'anonymous';
|
||||
this.audioContext = null;
|
||||
this.analyser = null;
|
||||
this.source = null;
|
||||
this.frequencyData = null;
|
||||
this.isInitialized = false;
|
||||
}
|
||||
|
||||
async init() {
|
||||
if (this.isInitialized) return;
|
||||
|
||||
try {
|
||||
this.audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
||||
this.analyser = this.audioContext.createAnalyser();
|
||||
this.analyser.fftSize = 512;
|
||||
this.analyser.smoothingTimeConstant = 0.8;
|
||||
|
||||
this.source = this.audioContext.createMediaElementSource(this.audio);
|
||||
this.source.connect(this.analyser);
|
||||
this.analyser.connect(this.audioContext.destination);
|
||||
|
||||
this.frequencyData = new Uint8Array(this.analyser.frequencyBinCount);
|
||||
this.isInitialized = true;
|
||||
|
||||
// Update Alpine store on audio events
|
||||
this.audio.addEventListener('timeupdate', () => {
|
||||
if (window.Alpine) {
|
||||
Alpine.store('audio').progress = this.audio.currentTime;
|
||||
Alpine.store('audio').duration = this.audio.duration || 0;
|
||||
}
|
||||
});
|
||||
|
||||
this.audio.addEventListener('ended', () => {
|
||||
if (window.Alpine) {
|
||||
Alpine.store('audio').isPlaying = false;
|
||||
}
|
||||
});
|
||||
|
||||
this.audio.addEventListener('play', () => {
|
||||
if (window.Alpine) {
|
||||
Alpine.store('audio').isPlaying = true;
|
||||
}
|
||||
});
|
||||
|
||||
this.audio.addEventListener('pause', () => {
|
||||
if (window.Alpine) {
|
||||
Alpine.store('audio').isPlaying = false;
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('Failed to initialize audio context:', e);
|
||||
}
|
||||
}
|
||||
|
||||
async play(url) {
|
||||
await this.init();
|
||||
|
||||
if (this.audioContext?.state === 'suspended') {
|
||||
await this.audioContext.resume();
|
||||
}
|
||||
|
||||
if (url && url !== this.audio.src) {
|
||||
this.audio.src = url;
|
||||
}
|
||||
|
||||
await this.audio.play();
|
||||
}
|
||||
|
||||
pause() {
|
||||
this.audio.pause();
|
||||
}
|
||||
|
||||
toggle() {
|
||||
if (this.audio.paused) {
|
||||
this.audio.play();
|
||||
} else {
|
||||
this.audio.pause();
|
||||
}
|
||||
}
|
||||
|
||||
seek(time) {
|
||||
this.audio.currentTime = time;
|
||||
}
|
||||
|
||||
setVolume(v) {
|
||||
this.audio.volume = Math.max(0, Math.min(1, v));
|
||||
}
|
||||
|
||||
getFrequencyData() {
|
||||
if (this.analyser) {
|
||||
this.analyser.getByteFrequencyData(this.frequencyData);
|
||||
}
|
||||
return this.frequencyData;
|
||||
}
|
||||
|
||||
getFrequencyBands() {
|
||||
const data = this.getFrequencyData();
|
||||
if (!data) return { low: 0, mid: 0, high: 0 };
|
||||
|
||||
const len = data.length;
|
||||
const low = this._avg(data, 0, len * 0.1) / 255;
|
||||
const mid = this._avg(data, len * 0.1, len * 0.5) / 255;
|
||||
const high = this._avg(data, len * 0.5, len) / 255;
|
||||
return { low, mid, high };
|
||||
}
|
||||
|
||||
_avg(data, start, end) {
|
||||
let sum = 0;
|
||||
for (let i = Math.floor(start); i < Math.floor(end); i++) {
|
||||
sum += data[i];
|
||||
}
|
||||
return sum / (end - start);
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize global instances
|
||||
if (!window.__pivoine) {
|
||||
const audioManager = new AudioManager();
|
||||
|
||||
window.__pivoine = {
|
||||
audioManager,
|
||||
visualizer: null,
|
||||
logo: null
|
||||
};
|
||||
|
||||
// Initialize WebGL components after DOM is ready
|
||||
const initWebGL = () => {
|
||||
// Main visualizer (fullscreen background)
|
||||
const visualizerCanvas = document.getElementById('visualizer');
|
||||
if (visualizerCanvas && !window.__pivoine.visualizer) {
|
||||
window.__pivoine.visualizer = new Visualizer(visualizerCanvas, audioManager);
|
||||
}
|
||||
|
||||
// Logo in header
|
||||
const logoCanvas = document.getElementById('logo-canvas');
|
||||
if (logoCanvas && !window.__pivoine.logo) {
|
||||
window.__pivoine.logo = new ReactiveLogo(logoCanvas, audioManager);
|
||||
}
|
||||
};
|
||||
|
||||
// Initialize on DOM ready
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initWebGL);
|
||||
} else {
|
||||
initWebGL();
|
||||
}
|
||||
}
|
||||
|
||||
// Alpine.js initialization
|
||||
document.addEventListener('alpine:init', () => {
|
||||
// Global audio store
|
||||
Alpine.store('audio', {
|
||||
currentTrack: null,
|
||||
isPlaying: false,
|
||||
progress: 0,
|
||||
duration: 0,
|
||||
volume: 0.8
|
||||
});
|
||||
|
||||
// Main app component
|
||||
Alpine.data('app', () => ({
|
||||
init() {
|
||||
// Restore volume from localStorage
|
||||
const savedVolume = localStorage.getItem('pivoine-volume');
|
||||
if (savedVolume) {
|
||||
Alpine.store('audio').volume = parseFloat(savedVolume);
|
||||
window.__pivoine.audioManager.setVolume(parseFloat(savedVolume));
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
// Player UI component
|
||||
Alpine.data('playerUI', () => ({
|
||||
togglePlay() {
|
||||
window.__pivoine.audioManager.toggle();
|
||||
},
|
||||
|
||||
seek(time) {
|
||||
window.__pivoine.audioManager.seek(parseFloat(time));
|
||||
},
|
||||
|
||||
setVolume(v) {
|
||||
const volume = parseFloat(v);
|
||||
Alpine.store('audio').volume = volume;
|
||||
window.__pivoine.audioManager.setVolume(volume);
|
||||
localStorage.setItem('pivoine-volume', volume);
|
||||
},
|
||||
|
||||
toggleMute() {
|
||||
const store = Alpine.store('audio');
|
||||
if (store.volume > 0) {
|
||||
this._previousVolume = store.volume;
|
||||
this.setVolume(0);
|
||||
} else {
|
||||
this.setVolume(this._previousVolume || 0.8);
|
||||
}
|
||||
},
|
||||
|
||||
formatTime(seconds) {
|
||||
if (!seconds || isNaN(seconds)) return '0:00';
|
||||
const mins = Math.floor(seconds / 60);
|
||||
const secs = Math.floor(seconds % 60);
|
||||
return `${mins}:${secs.toString().padStart(2, '0')}`;
|
||||
},
|
||||
|
||||
_previousVolume: 0.8
|
||||
}));
|
||||
});
|
||||
|
||||
// htmx lifecycle hooks
|
||||
document.body.addEventListener('htmx:beforeSwap', () => {
|
||||
// Pause visualizer during page transition for performance
|
||||
window.__pivoine?.visualizer?.pause();
|
||||
});
|
||||
|
||||
document.body.addEventListener('htmx:afterSwap', () => {
|
||||
// Resume visualizer after swap
|
||||
window.__pivoine?.visualizer?.resume();
|
||||
// Re-initialize scroll animations
|
||||
window.dispatchEvent(new CustomEvent('page:loaded'));
|
||||
});
|
||||
|
||||
// Scroll animations with Intersection Observer
|
||||
function initScrollAnimations() {
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
entry.target.classList.add('is-visible');
|
||||
}
|
||||
});
|
||||
},
|
||||
{ threshold: 0.1, rootMargin: '0px 0px -50px 0px' }
|
||||
);
|
||||
|
||||
document.querySelectorAll('.fade-in-up').forEach((el) => {
|
||||
observer.observe(el);
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize on page load
|
||||
document.addEventListener('DOMContentLoaded', initScrollAnimations);
|
||||
window.addEventListener('page:loaded', initScrollAnimations);
|
||||
|
||||
export { AudioManager };
|
||||
145
assets/js/visualizer/particles.js
Normal file
145
assets/js/visualizer/particles.js
Normal file
@@ -0,0 +1,145 @@
|
||||
/**
|
||||
* Particle System
|
||||
* Audio-reactive 3D particle cloud
|
||||
*/
|
||||
|
||||
import * as THREE from 'three';
|
||||
|
||||
// Vertex Shader
|
||||
const vertexShader = `
|
||||
uniform float uTime;
|
||||
uniform float uLow;
|
||||
uniform float uMid;
|
||||
uniform float uHigh;
|
||||
uniform float uSize;
|
||||
|
||||
attribute float aRandom;
|
||||
attribute float aScale;
|
||||
|
||||
varying float vAlpha;
|
||||
|
||||
void main() {
|
||||
vec3 pos = position;
|
||||
|
||||
// Base rotation
|
||||
float angle = uTime * 0.2;
|
||||
mat3 rotation = mat3(
|
||||
cos(angle), 0.0, sin(angle),
|
||||
0.0, 1.0, 0.0,
|
||||
-sin(angle), 0.0, cos(angle)
|
||||
);
|
||||
pos = rotation * pos;
|
||||
|
||||
// Bass affects scale/expansion
|
||||
float bassScale = 1.0 + uLow * 0.4;
|
||||
pos *= bassScale;
|
||||
|
||||
// Mid frequencies create wave motion
|
||||
float wave = sin(pos.x * 0.1 + uTime * 2.0) * uMid * 8.0;
|
||||
pos.y += wave * aRandom;
|
||||
|
||||
// High frequencies add jitter/sparkle
|
||||
vec3 jitter = normalize(pos) * uHigh * aRandom * 5.0;
|
||||
pos += jitter;
|
||||
|
||||
// Calculate view position
|
||||
vec4 mvPosition = modelViewMatrix * vec4(pos, 1.0);
|
||||
|
||||
// Point size with perspective
|
||||
float size = uSize * aScale;
|
||||
size *= (1.0 + uLow * 0.5); // Pulse with bass
|
||||
gl_PointSize = size * (300.0 / -mvPosition.z);
|
||||
|
||||
// Alpha based on distance and audio
|
||||
vAlpha = 0.6 + uLow * 0.3;
|
||||
|
||||
gl_Position = projectionMatrix * mvPosition;
|
||||
}
|
||||
`;
|
||||
|
||||
// Fragment Shader
|
||||
const fragmentShader = `
|
||||
varying float vAlpha;
|
||||
|
||||
void main() {
|
||||
// Circular point with soft edge
|
||||
float dist = length(gl_PointCoord - vec2(0.5));
|
||||
if (dist > 0.5) discard;
|
||||
|
||||
float alpha = 1.0 - smoothstep(0.2, 0.5, dist);
|
||||
alpha *= vAlpha;
|
||||
|
||||
// Pure white color for minimal aesthetic
|
||||
gl_FragColor = vec4(1.0, 1.0, 1.0, alpha);
|
||||
}
|
||||
`;
|
||||
|
||||
export class ParticleSystem {
|
||||
constructor(count = 5000) {
|
||||
this.count = count;
|
||||
this.createGeometry();
|
||||
this.createMaterial();
|
||||
this.mesh = new THREE.Points(this.geometry, this.material);
|
||||
}
|
||||
|
||||
createGeometry() {
|
||||
this.geometry = new THREE.BufferGeometry();
|
||||
|
||||
const positions = new Float32Array(this.count * 3);
|
||||
const randoms = new Float32Array(this.count);
|
||||
const scales = new Float32Array(this.count);
|
||||
|
||||
for (let i = 0; i < this.count; i++) {
|
||||
const i3 = i * 3;
|
||||
|
||||
// Spherical distribution
|
||||
const radius = 20 + Math.random() * 30;
|
||||
const theta = Math.random() * Math.PI * 2;
|
||||
const phi = Math.acos(2 * Math.random() - 1);
|
||||
|
||||
positions[i3] = radius * Math.sin(phi) * Math.cos(theta);
|
||||
positions[i3 + 1] = radius * Math.sin(phi) * Math.sin(theta);
|
||||
positions[i3 + 2] = radius * Math.cos(phi);
|
||||
|
||||
randoms[i] = Math.random();
|
||||
scales[i] = 0.5 + Math.random() * 1.5;
|
||||
}
|
||||
|
||||
this.geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
|
||||
this.geometry.setAttribute('aRandom', new THREE.BufferAttribute(randoms, 1));
|
||||
this.geometry.setAttribute('aScale', new THREE.BufferAttribute(scales, 1));
|
||||
}
|
||||
|
||||
createMaterial() {
|
||||
this.material = new THREE.ShaderMaterial({
|
||||
vertexShader,
|
||||
fragmentShader,
|
||||
uniforms: {
|
||||
uTime: { value: 0 },
|
||||
uLow: { value: 0 },
|
||||
uMid: { value: 0 },
|
||||
uHigh: { value: 0 },
|
||||
uSize: { value: 2.0 }
|
||||
},
|
||||
transparent: true,
|
||||
blending: THREE.AdditiveBlending,
|
||||
depthWrite: false
|
||||
});
|
||||
}
|
||||
|
||||
update(bands, time) {
|
||||
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;
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.geometry?.dispose();
|
||||
this.material?.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
export default ParticleSystem;
|
||||
110
assets/js/visualizer/scene.js
Normal file
110
assets/js/visualizer/scene.js
Normal file
@@ -0,0 +1,110 @@
|
||||
/**
|
||||
* WebGL Visualizer Scene
|
||||
* Three.js particle system that reacts to audio frequency data
|
||||
*/
|
||||
|
||||
import * as THREE from 'three';
|
||||
import { ParticleSystem } from './particles.js';
|
||||
|
||||
export class Visualizer {
|
||||
constructor(canvas, audioManager) {
|
||||
this.canvas = canvas;
|
||||
this.audioManager = audioManager;
|
||||
this.running = false;
|
||||
this.time = 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));
|
||||
|
||||
// Particles
|
||||
this.particles = new ParticleSystem(5000);
|
||||
this.scene.add(this.particles.mesh);
|
||||
|
||||
// Event listeners
|
||||
window.addEventListener('resize', () => this.resize());
|
||||
|
||||
// Start animation
|
||||
this.start();
|
||||
}
|
||||
|
||||
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 particles with audio data
|
||||
this.particles.update(bands, this.time);
|
||||
|
||||
// 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.particles?.dispose();
|
||||
this.renderer?.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
export default Visualizer;
|
||||
Reference in New Issue
Block a user