422 lines
14 KiB
JavaScript
422 lines
14 KiB
JavaScript
/* ==========================================================================
|
|
SYSTEM AWAKENING - Particle System Module
|
|
Three.js based particle system with blinking effects and connections
|
|
========================================================================== */
|
|
|
|
import {
|
|
PARTICLE_CONFIG,
|
|
COLORS,
|
|
getParticleCount,
|
|
getRandomValue
|
|
} from './config.js';
|
|
|
|
/**
|
|
* Particle System Class
|
|
* Manages the Three.js particle system for the background
|
|
*/
|
|
export class ParticleSystem {
|
|
/**
|
|
* Create a new particle system
|
|
* @param {HTMLCanvasElement} canvas - The canvas element to render to
|
|
*/
|
|
constructor(canvas) {
|
|
this.canvas = canvas;
|
|
this.scene = null;
|
|
this.camera = null;
|
|
this.renderer = null;
|
|
this.particles = null;
|
|
this.clock = new THREE.Clock();
|
|
this.time = 0;
|
|
this.isInitialized = false;
|
|
|
|
// Particle properties
|
|
this.particleCount = getParticleCount();
|
|
this.positions = new Float32Array(this.particleCount * 3);
|
|
this.velocities = new Float32Array(this.particleCount * 3);
|
|
this.sizes = new Float32Array(this.particleCount);
|
|
this.opacities = new Float32Array(this.particleCount);
|
|
this.blinkPhases = new Float32Array(this.particleCount);
|
|
|
|
// Initialize the system
|
|
this.init();
|
|
}
|
|
|
|
/**
|
|
* Initialize the Three.js scene, camera, and renderer
|
|
*/
|
|
init() {
|
|
try {
|
|
// Create scene
|
|
this.scene = new THREE.Scene();
|
|
this.scene.fog = new THREE.Fog(COLORS.BG, 50, 500);
|
|
|
|
// Create camera
|
|
this.camera = new THREE.PerspectiveCamera(
|
|
75,
|
|
window.innerWidth / window.innerHeight,
|
|
0.1,
|
|
1000
|
|
);
|
|
this.camera.position.z = 100;
|
|
|
|
// Create renderer
|
|
this.renderer = new THREE.WebGLRenderer({
|
|
canvas: this.canvas,
|
|
alpha: true,
|
|
antialias: true
|
|
});
|
|
this.renderer.setSize(window.innerWidth, window.innerHeight);
|
|
this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
|
|
|
// Create particles
|
|
this.createParticles();
|
|
|
|
// Handle window resize
|
|
this.bindResize();
|
|
|
|
this.isInitialized = true;
|
|
console.log('Particle system initialized');
|
|
} catch (error) {
|
|
console.error('Failed to initialize particle system:', error);
|
|
this.isInitialized = false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create particle geometry and material
|
|
*/
|
|
createParticles() {
|
|
// Generate random particle properties
|
|
for (let i = 0; i < this.particleCount; i++) {
|
|
// Position (spherical distribution)
|
|
const radius = getRandomValue(20, 300);
|
|
const theta = Math.random() * Math.PI * 2;
|
|
const phi = Math.random() * Math.PI;
|
|
|
|
const x = radius * Math.sin(phi) * Math.cos(theta);
|
|
const y = radius * Math.sin(phi) * Math.sin(theta);
|
|
const z = radius * Math.cos(phi);
|
|
|
|
this.positions[i * 3] = x;
|
|
this.positions[i * 3 + 1] = y;
|
|
this.positions[i * 3 + 2] = z;
|
|
|
|
// Velocity (random direction)
|
|
this.velocities[i * 3] = (Math.random() - 0.5) * PARTICLE_CONFIG.SPEED_MAX;
|
|
this.velocities[i * 3 + 1] = (Math.random() - 0.5) * PARTICLE_CONFIG.SPEED_MAX;
|
|
this.velocities[i * 3 + 2] = (Math.random() - 0.5) * PARTICLE_CONFIG.SPEED_MAX;
|
|
|
|
// Size
|
|
this.sizes[i] = getRandomValue(
|
|
PARTICLE_CONFIG.SIZE_MIN * 10,
|
|
PARTICLE_CONFIG.SIZE_MAX * 10
|
|
) / 10;
|
|
|
|
// Opacity
|
|
this.opacities[i] = getRandomValue(
|
|
PARTICLE_CONFIG.OPACITY_MIN * 100,
|
|
PARTICLE_CONFIG.OPACITY_MAX * 100
|
|
) / 100;
|
|
|
|
// Blink phase
|
|
this.blinkPhases[i] = Math.random() * Math.PI * 2;
|
|
}
|
|
|
|
// Create geometry
|
|
const geometry = new THREE.BufferGeometry();
|
|
geometry.setAttribute('position', new THREE.BufferAttribute(this.positions, 3));
|
|
geometry.setAttribute('size', new THREE.BufferAttribute(this.sizes, 1));
|
|
geometry.setAttribute('opacity', new THREE.BufferAttribute(this.opacities, 1));
|
|
geometry.setAttribute('blinkPhase', new THREE.BufferAttribute(this.blinkPhases, 1));
|
|
|
|
// Create material
|
|
const material = new THREE.PointsMaterial({
|
|
size: 2,
|
|
transparent: true,
|
|
opacity: 0.8,
|
|
color: new THREE.Color(COLORS.PRIMARY),
|
|
blending: THREE.AdditiveBlending,
|
|
depthWrite: false
|
|
});
|
|
|
|
// Custom shader for blinking effect
|
|
material.onBeforeCompile = (shader) => {
|
|
shader.uniforms.time = { value: 0 };
|
|
shader.uniforms.blinkSpeed = { value: PARTICLE_CONFIG.BLINK_SPEED };
|
|
|
|
// Vertex shader modifications
|
|
shader.vertexShader = shader.vertexShader.replace(
|
|
'void main() {',
|
|
`
|
|
uniform float time;
|
|
uniform float blinkSpeed;
|
|
attribute float opacity;
|
|
attribute float blinkPhase;
|
|
|
|
varying float vOpacity;
|
|
|
|
void main() {
|
|
// Blinking effect
|
|
float blink = 0.7 + 0.3 * sin(time * blinkSpeed + blinkPhase);
|
|
vOpacity = opacity * blink;
|
|
`
|
|
);
|
|
|
|
// Fragment shader modifications
|
|
shader.fragmentShader = shader.fragmentShader.replace(
|
|
'void main() {',
|
|
`
|
|
varying float vOpacity;
|
|
|
|
void main() {
|
|
`
|
|
);
|
|
|
|
shader.fragmentShader = shader.fragmentShader.replace(
|
|
'gl_FragColor = vec4( diffuse, opacity );',
|
|
`
|
|
// Apply varying opacity
|
|
gl_FragColor = vec4( diffuse, vOpacity );
|
|
|
|
// Add glow effect
|
|
gl_FragColor.rgb *= 1.5;
|
|
`
|
|
);
|
|
|
|
// Store reference to uniforms for later updates
|
|
material.uniforms = shader.uniforms;
|
|
material.userData.shader = shader;
|
|
};
|
|
|
|
// Create points (particles)
|
|
this.particles = new THREE.Points(geometry, material);
|
|
this.scene.add(this.particles);
|
|
|
|
// Add connection lines
|
|
this.createConnections();
|
|
}
|
|
|
|
/**
|
|
* Create connection lines between nearby particles
|
|
*/
|
|
createConnections() {
|
|
// We'll update connections in the render loop
|
|
// Geometry for connection lines will be created dynamically
|
|
this.connections = null;
|
|
this.connectionGeometry = new THREE.BufferGeometry();
|
|
this.connectionMaterial = new THREE.LineBasicMaterial({
|
|
color: new THREE.Color(COLORS.PRIMARY),
|
|
transparent: true,
|
|
opacity: PARTICLE_CONFIG.CONNECTION_OPACITY,
|
|
blending: THREE.AdditiveBlending,
|
|
linewidth: 1
|
|
});
|
|
|
|
// Create empty line segments for now
|
|
this.connections = new THREE.LineSegments(this.connectionGeometry, this.connectionMaterial);
|
|
this.scene.add(this.connections);
|
|
}
|
|
|
|
/**
|
|
* Update connection lines between particles
|
|
*/
|
|
updateConnections() {
|
|
const positions = this.positions;
|
|
const particleCount = this.particleCount;
|
|
const maxDistance = PARTICLE_CONFIG.CONNECTION_DISTANCE;
|
|
|
|
// Temporary arrays for connection vertices
|
|
const connectionVertices = [];
|
|
const connectionColors = [];
|
|
|
|
// Check distances between particles (simple N^2 algorithm, optimized by limiting checks)
|
|
for (let i = 0; i < particleCount; i++) {
|
|
const x1 = positions[i * 3];
|
|
const y1 = positions[i * 3 + 1];
|
|
const z1 = positions[i * 3 + 2];
|
|
|
|
// Only check a subset of particles for performance
|
|
for (let j = i + 1; j < Math.min(i + 20, particleCount); j++) {
|
|
const x2 = positions[j * 3];
|
|
const y2 = positions[j * 3 + 1];
|
|
const z2 = positions[j * 3 + 2];
|
|
|
|
const dx = x2 - x1;
|
|
const dy = y2 - y1;
|
|
const dz = z2 - z1;
|
|
const distance = Math.sqrt(dx * dx + dy * dy + dz * dz);
|
|
|
|
if (distance < maxDistance) {
|
|
// Calculate opacity based on distance (closer = more opaque)
|
|
const opacity = PARTICLE_CONFIG.CONNECTION_OPACITY *
|
|
(1 - distance / maxDistance);
|
|
|
|
// Add vertices for line segment
|
|
connectionVertices.push(x1, y1, z1);
|
|
connectionVertices.push(x2, y2, z2);
|
|
|
|
// Add colors with varying opacity
|
|
connectionColors.push(
|
|
COLORS.PRIMARY_LIGHT, COLORS.PRIMARY_LIGHT, COLORS.PRIMARY_LIGHT, opacity
|
|
);
|
|
connectionColors.push(
|
|
COLORS.PRIMARY_LIGHT, COLORS.PRIMARY_LIGHT, COLORS.PRIMARY_LIGHT, opacity
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Update connection geometry
|
|
if (connectionVertices.length > 0) {
|
|
this.connectionGeometry.setAttribute(
|
|
'position',
|
|
new THREE.Float32BufferAttribute(connectionVertices, 3)
|
|
);
|
|
|
|
// Note: Three.js doesn't easily support per-vertex colors for LineSegments
|
|
// We'll use a uniform opacity instead
|
|
this.connections.material.opacity = PARTICLE_CONFIG.CONNECTION_OPACITY;
|
|
this.connectionGeometry.attributes.position.needsUpdate = true;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update particle positions and properties
|
|
* @param {number} deltaTime - Time since last update in seconds
|
|
*/
|
|
update(deltaTime) {
|
|
if (!this.isInitialized) return;
|
|
|
|
this.time += deltaTime;
|
|
|
|
// Update particle positions
|
|
const positions = this.positions;
|
|
const velocities = this.velocities;
|
|
|
|
for (let i = 0; i < this.particleCount; i++) {
|
|
// Apply velocity
|
|
positions[i * 3] += velocities[i * 3] * deltaTime * 60;
|
|
positions[i * 3 + 1] += velocities[i * 3 + 1] * deltaTime * 60;
|
|
positions[i * 3 + 2] += velocities[i * 3 + 2] * deltaTime * 60;
|
|
|
|
// Boundary check - wrap around
|
|
const radius = Math.sqrt(
|
|
positions[i * 3] * positions[i * 3] +
|
|
positions[i * 3 + 1] * positions[i * 3 + 1] +
|
|
positions[i * 3 + 2] * positions[i * 3 + 2]
|
|
);
|
|
|
|
if (radius > 300) {
|
|
// Normalize and scale to inner boundary
|
|
const scale = 20 / radius;
|
|
positions[i * 3] *= scale;
|
|
positions[i * 3 + 1] *= scale;
|
|
positions[i * 3 + 2] *= scale;
|
|
|
|
// Randomize velocity direction
|
|
velocities[i * 3] = (Math.random() - 0.5) * PARTICLE_CONFIG.SPEED_MAX;
|
|
velocities[i * 3 + 1] = (Math.random() - 0.5) * PARTICLE_CONFIG.SPEED_MAX;
|
|
velocities[i * 3 + 2] = (Math.random() - 5) * PARTICLE_CONFIG.SPEED_MAX;
|
|
}
|
|
}
|
|
|
|
// Update geometry attributes
|
|
if (this.particles) {
|
|
this.particles.geometry.attributes.position.needsUpdate = true;
|
|
this.particles.geometry.attributes.position.array = positions;
|
|
|
|
// Update shader uniforms for blinking effect
|
|
if (this.particles.material.uniforms) {
|
|
this.particles.material.uniforms.time.value = this.time;
|
|
}
|
|
}
|
|
|
|
// Update connection lines
|
|
this.updateConnections();
|
|
|
|
// Rotate camera slowly for dynamic effect
|
|
this.camera.position.x = Math.sin(this.time * 0.1) * 50;
|
|
this.camera.position.y = Math.cos(this.time * 0.15) * 30;
|
|
this.camera.lookAt(0, 0, 0);
|
|
}
|
|
|
|
/**
|
|
* Render the particle system
|
|
*/
|
|
render() {
|
|
if (!this.isInitialized) return;
|
|
|
|
this.renderer.render(this.scene, this.camera);
|
|
}
|
|
|
|
/**
|
|
* Handle window resize
|
|
*/
|
|
onResize() {
|
|
if (!this.isInitialized) return;
|
|
|
|
// Update camera aspect ratio
|
|
this.camera.aspect = window.innerWidth / window.innerHeight;
|
|
this.camera.updateProjectionMatrix();
|
|
|
|
// Update renderer size
|
|
this.renderer.setSize(window.innerWidth, window.innerHeight);
|
|
|
|
// Adjust particle count based on screen size
|
|
const newCount = getParticleCount();
|
|
if (newCount !== this.particleCount) {
|
|
this.particleCount = newCount;
|
|
this.scene.remove(this.particles);
|
|
this.scene.remove(this.connections);
|
|
this.createParticles();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Bind resize event listener
|
|
*/
|
|
bindResize() {
|
|
// Debounce resize events for performance
|
|
let resizeTimeout;
|
|
const handleResize = () => {
|
|
clearTimeout(resizeTimeout);
|
|
resizeTimeout = setTimeout(() => {
|
|
this.onResize();
|
|
}, 250);
|
|
};
|
|
|
|
window.addEventListener('resize', handleResize);
|
|
}
|
|
|
|
/**
|
|
* Clean up resources
|
|
*/
|
|
dispose() {
|
|
if (!this.isInitialized) return;
|
|
|
|
// Dispose geometries and materials
|
|
if (this.particles) {
|
|
this.particles.geometry.dispose();
|
|
this.particles.material.dispose();
|
|
}
|
|
|
|
if (this.connections) {
|
|
this.connectionGeometry.dispose();
|
|
this.connectionMaterial.dispose();
|
|
}
|
|
|
|
// Dispose renderer
|
|
this.renderer.dispose();
|
|
|
|
this.isInitialized = false;
|
|
console.log('Particle system disposed');
|
|
}
|
|
|
|
/**
|
|
* Get the current time for animation synchronization
|
|
* @returns {number} Current time in seconds
|
|
*/
|
|
getTime() {
|
|
return this.time;
|
|
}
|
|
} |