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