Files
pivoine.art/assets/js/logo/reactive-logo.js
2025-11-29 17:51:00 +01:00

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;