/* ========================================================================== 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; } }