182 lines
4.7 KiB
JavaScript
182 lines
4.7 KiB
JavaScript
/**
|
|
* 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;
|