146 lines
3.5 KiB
JavaScript
146 lines
3.5 KiB
JavaScript
|
|
/**
|
||
|
|
* 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;
|