LLM-test/test/code/1/deepseel-cc/js/particleSystem.js

422 lines
14 KiB
JavaScript
Raw Normal View History

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