Initial commit

This commit is contained in:
2025-11-29 17:51:00 +01:00
commit 694a7047a4
66 changed files with 5105 additions and 0 deletions

View 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;

View 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;