Cosmic Particles
Beautiful particle system simulating a cosmic environment
Created: November 5, 2023
View Source Code
import * as THREE from 'three';
// Particle configuration
const PARTICLE_COUNT = 2500;
const PARTICLE_SIZE_MIN = 0.05;
const PARTICLE_SIZE_MAX = 0.25;
const SPREAD_RADIUS = 10;
const REPULSION_RADIUS = 2;
const ATTRACTION_RADIUS = 10;
const REPULSION_STRENGTH = 0.05;
const ATTRACTION_STRENGTH = 0.03;
const NOISE_STRENGTH = 0.01;
const CURL_STRENGTH = 0.2;
// Color palette (using beautiful complementary colors)
const COLOR_PALETTE = [
new THREE.Color(0x00b4ff), // Bright blue
new THREE.Color(0x7d4dff), // Purple
new THREE.Color(0xff3366), // Pink
new THREE.Color(0x00e5ff), // Cyan
new THREE.Color(0xffac41) // Orange
];
// Spatial grid for particle-particle interactions
const GRID_CELL_SIZE = 1.0; // Cell size for spatial partitioning
const REPEL_RADIUS = 1.0; // Radius for particle repulsion
const REPEL_STRENGTH = 0.025; // Strength of repulsion
// Spatial hash grid for faster neighbor lookups
class SpatialGrid {
private cells: Map<string, number[]> = new Map();
private cellSize: number;
constructor(cellSize: number) {
this.cellSize = cellSize;
}
// Clear the grid
clear(): void {
this.cells.clear();
}
// Get cell key from position
getCellKey(x: number, y: number, z: number): string {
const cellX = Math.floor(x / this.cellSize);
const cellY = Math.floor(y / this.cellSize);
const cellZ = Math.floor(z / this.cellSize);
return `${cellX},${cellY},${cellZ}`;
}
// Add a particle to the grid
addParticle(index: number, x: number, y: number, z: number): void {
const key = this.getCellKey(x, y, z);
if (!this.cells.has(key)) {
this.cells.set(key, []);
}
this.cells.get(key)!.push(index);
}
// Get potential neighbors from surrounding cells
getNeighbors(x: number, y: number, z: number): number[] {
const neighbors: number[] = [];
// Check current cell and surrounding cells (27 cells)
const cellX = Math.floor(x / this.cellSize);
const cellY = Math.floor(y / this.cellSize);
const cellZ = Math.floor(z / this.cellSize);
// Only check immediate neighbors to limit performance impact
for (let i = -1; i <= 1; i++) {
for (let j = -1; j <= 1; j++) {
for (let k = -1; k <= 1; k++) {
const key = `${cellX + i},${cellY + j},${cellZ + k}`;
const cell = this.cells.get(key);
if (cell) {
neighbors.push(...cell);
}
}
}
}
return neighbors;
}
}
/**
* CosmicParticles - A ThreeJS demo with interactive particle system
*/
class CosmicParticles {
private scene: THREE.Scene;
private camera: THREE.PerspectiveCamera;
private renderer: THREE.WebGLRenderer;
private particleSystem: THREE.Points;
private particlePositions: Float32Array;
private particleColors: Float32Array;
private particleSizes: Float32Array;
private raycaster: THREE.Raycaster;
private mouse: THREE.Vector2;
private mousePosition: THREE.Vector3;
private mouseCursor: THREE.Mesh; // Visual feedback for mouse position
private rayLine: THREE.Line; // Line showing the ray cast
private clock: THREE.Clock;
private time: number = 0;
private mousePressed: boolean = false;
private gravityEnabled: boolean = false; // Toggle for gravity effect
private repulsionEnabled: boolean = true; // Toggle for particle repulsion
private spatialGrid: SpatialGrid; // Spatial grid for optimized repulsion
private mouseForce: THREE.Vector3 = new THREE.Vector3();
private animationFrameId: number | null = null;
private isInitialized: boolean = false;
private container: HTMLElement | null = null;
constructor() {
this.scene = new THREE.Scene();
this.camera = new THREE.PerspectiveCamera();
this.renderer = new THREE.WebGLRenderer();
this.particleSystem = new THREE.Points();
this.particlePositions = new Float32Array();
this.particleColors = new Float32Array();
this.particleSizes = new Float32Array();
this.raycaster = new THREE.Raycaster();
this.mouse = new THREE.Vector2();
this.mousePosition = new THREE.Vector3();
this.mouseCursor = new THREE.Mesh();
this.rayLine = new THREE.Line();
this.clock = new THREE.Clock();
this.spatialGrid = new SpatialGrid(GRID_CELL_SIZE);
}
// Create a simplex noise-like function for movement
private simplex3D(x: number, y: number, z: number): number {
// Create a simple yet interesting noise function
const n1 = Math.sin(x * 0.3 + y * 0.7 + z * 0.1);
const n2 = Math.sin(x * 0.1 + y * 0.3 - z * 0.5);
const n3 = Math.sin(-x * 0.5 + y * 0.2 + z * 0.3);
return (n1 + n2 + n3) / 3;
}
// Curl noise for more natural flowing motion
private curlNoise(x: number, y: number, z: number): THREE.Vector3 {
const eps = 0.0001;
// Calculate gradient for each axis
const dx1 = this.simplex3D(x + eps, y, z) - this.simplex3D(x - eps, y, z);
const dy1 = this.simplex3D(x, y + eps, z) - this.simplex3D(x, y - eps, z);
const dz1 = this.simplex3D(x, y, z + eps) - this.simplex3D(x, y, z - eps);
const dx2 = this.simplex3D(x, y + eps, z) - this.simplex3D(x, y - eps, z);
const dy2 = this.simplex3D(x, y, z + eps) - this.simplex3D(x, y, z - eps);
const dz2 = this.simplex3D(x + eps, y, z) - this.simplex3D(x - eps, y, z);
const dx3 = this.simplex3D(x, y, z + eps) - this.simplex3D(x, y, z - eps);
const dy3 = this.simplex3D(x + eps, y, z) - this.simplex3D(x - eps, y, z);
const dz3 = this.simplex3D(x, y + eps, z) - this.simplex3D(x, y - eps, z);
// Curl formula
const curlX = (dy3 - dz2) / (2 * eps);
const curlY = (dz1 - dx3) / (2 * eps);
const curlZ = (dx2 - dy1) / (2 * eps);
return new THREE.Vector3(curlX, curlY, curlZ);
}
public initialize(container: HTMLElement | null): void {
if (!container || this.isInitialized) return;
this.container = container;
// Initialize THREE.js scene
this.scene = new THREE.Scene();
this.scene.background = new THREE.Color(0x050515); // Dark blue background
// Setup camera
const width = container.clientWidth;
const height = container.clientHeight;
const aspectRatio = width / height;
this.camera = new THREE.PerspectiveCamera(60, aspectRatio, 0.1, 1000);
this.camera.position.z = 27;
// Setup renderer with nice effects
this.renderer = new THREE.WebGLRenderer({
antialias: true,
alpha: true
});
this.renderer.setSize(width, height);
this.renderer.setPixelRatio(window.devicePixelRatio);
container.appendChild(this.renderer.domElement);
// Initialize mouse and raycaster for interaction
this.mouse = new THREE.Vector2();
this.mousePosition = new THREE.Vector3();
this.raycaster = new THREE.Raycaster();
// Create visual cursor for mouse position
const cursorGeometry = new THREE.SphereGeometry(0.3, 16, 16);
const cursorMaterial = new THREE.MeshBasicMaterial({
color: 0xffffff,
transparent: true,
opacity: 0.7,
depthWrite: false
});
this.mouseCursor = new THREE.Mesh(cursorGeometry, cursorMaterial);
this.mouseCursor.visible = false; // Start hidden
this.scene.add(this.mouseCursor);
// Create ray visualization
const rayGeometry = new THREE.BufferGeometry().setFromPoints([
new THREE.Vector3(0, 0, 0),
new THREE.Vector3(0, 0, -1000)
]);
const rayMaterial = new THREE.LineBasicMaterial({
color: 0xffffff,
transparent: true,
opacity: 0.3,
blending: THREE.AdditiveBlending
});
this.rayLine = new THREE.Line(rayGeometry, rayMaterial);
this.rayLine.visible = false; // Start hidden
this.scene.add(this.rayLine);
// Initialize clock and spatial grid
this.clock = new THREE.Clock();
this.spatialGrid = new SpatialGrid(GRID_CELL_SIZE);
// Create particle buffers
this.particlePositions = new Float32Array(PARTICLE_COUNT * 3);
this.particleColors = new Float32Array(PARTICLE_COUNT * 3);
this.particleSizes = new Float32Array(PARTICLE_COUNT);
// Initialize all particles
for (let i = 0; i < PARTICLE_COUNT; i++) {
const i3 = i * 3;
// Position particles in 3D space - use spherical distribution
const radius = Math.random() * SPREAD_RADIUS;
const theta = Math.random() * Math.PI * 2;
const phi = Math.acos(2 * Math.random() - 1);
this.particlePositions[i3] = radius * Math.sin(phi) * Math.cos(theta);
this.particlePositions[i3 + 1] = radius * Math.sin(phi) * Math.sin(theta);
this.particlePositions[i3 + 2] = radius * Math.cos(phi);
// Assign random size
this.particleSizes[i] = PARTICLE_SIZE_MIN + Math.random() * (PARTICLE_SIZE_MAX - PARTICLE_SIZE_MIN);
// Assign color from palette
const colorIndex = Math.floor(Math.random() * COLOR_PALETTE.length);
const color = COLOR_PALETTE[colorIndex];
// Add some variation to colors
const hsl = { h: 0, s: 0, l: 0 };
color.getHSL(hsl);
// Vary hue slightly
hsl.h += (Math.random() * 0.1) - 0.05;
hsl.s += (Math.random() * 0.2) - 0.1;
hsl.l += (Math.random() * 0.2) - 0.1;
const variedColor = new THREE.Color().setHSL(hsl.h, hsl.s, hsl.l);
this.particleColors[i3] = variedColor.r;
this.particleColors[i3 + 1] = variedColor.g;
this.particleColors[i3 + 2] = variedColor.b;
}
// Create particle geometry
const particleGeometry = new THREE.BufferGeometry();
particleGeometry.setAttribute('position', new THREE.BufferAttribute(this.particlePositions, 3));
particleGeometry.setAttribute('aColor', new THREE.BufferAttribute(this.particleColors, 3)); // Changed from 'color' to 'aColor'
particleGeometry.setAttribute('size', new THREE.BufferAttribute(this.particleSizes, 1));
// Create shader material for particles
const particleMaterial = new THREE.ShaderMaterial({
uniforms: {
time: { value: 0 },
pixelRatio: { value: window.devicePixelRatio }
},
vertexShader: /*glsl*/`
attribute float size;
attribute vec3 aColor;
uniform float time;
uniform float pixelRatio;
varying vec3 vColor;
void main() {
vColor = aColor;
vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
gl_PointSize = size * pixelRatio * (300.0 / -mvPosition.z);
gl_Position = projectionMatrix * mvPosition;
}
`,
fragmentShader: /*glsl*/`
varying vec3 vColor;
void main() {
// Calculate normalized distance from center of point
vec2 uv = gl_PointCoord.xy - 0.5;
float r = length(uv) * 2.0;
// Smooth circle with soft edges
float circle = smoothstep(1.0, 0.8, r);
// Add glow effect
float glow = exp(-r * 3.5) * 0.35;
// Combine for final color
gl_FragColor = vec4(vColor, circle + glow);
}
`,
transparent: true,
depthWrite: false,
blending: THREE.AdditiveBlending,
vertexColors: true
});
// Create the particle system
this.particleSystem = new THREE.Points(particleGeometry, particleMaterial);
this.scene.add(this.particleSystem);
// Add lighting
const ambientLight = new THREE.AmbientLight(0x222233);
this.scene.add(ambientLight);
// Add multiple colored point lights for dramatic effect
const light1 = new THREE.PointLight(0x3677ff, 1, 30);
light1.position.set(8, 5, 7);
this.scene.add(light1);
const light2 = new THREE.PointLight(0xff3366, 1, 30);
light2.position.set(-8, -5, 7);
this.scene.add(light2);
const light3 = new THREE.PointLight(0x23a6d5, 1, 30);
light3.position.set(0, 7, -10);
this.scene.add(light3);
// Add event listeners
this.renderer.domElement.addEventListener('mousemove', this.onMouseMove);
this.renderer.domElement.addEventListener('mousedown', () => {
this.gravityEnabled = true;
});
this.renderer.domElement.addEventListener('mouseup', () => {
this.gravityEnabled = false;
});
this.renderer.domElement.addEventListener('touchstart', this.onTouchStart);
this.renderer.domElement.addEventListener('touchmove', this.onTouchMove);
this.renderer.domElement.addEventListener('touchend', () => {
this.mousePressed = false;
});
window.addEventListener('resize', this.onWindowResize);
// Add keyboard event listener for repulsion toggle (press 'r')
window.addEventListener('keydown', this.onKeyDown);
// Start animation loop
this.animate();
this.isInitialized = true;
}
private onKeyDown = (event: KeyboardEvent): void => {
if (event.key === 'r' || event.key === 'R') {
this.repulsionEnabled = !this.repulsionEnabled;
// Visual feedback
if (this.repulsionEnabled) {
console.log('Particle repulsion enabled');
} else {
console.log('Particle repulsion disabled');
}
}
};
private onMouseMove = (event: MouseEvent): void => {
if (!this.renderer) return;
const rect = this.renderer.domElement.getBoundingClientRect();
// Convert mouse position to normalized device coordinates (-1 to +1)
this.mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
this.mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
this.updateMousePosition();
};
private onTouchStart = (event: TouchEvent): void => {
this.mousePressed = true;
// Toggle gravity effect on touch
this.gravityEnabled = !this.gravityEnabled;
// Update cursor color based on gravity state
if (this.gravityEnabled) {
(this.mouseCursor.material as THREE.MeshBasicMaterial).color.set(0x00ff88);
} else {
(this.mouseCursor.material as THREE.MeshBasicMaterial).color.set(0xff5500);
}
this.updateTouchPosition(event);
};
private onTouchMove = (event: TouchEvent): void => {
this.updateTouchPosition(event);
};
private updateTouchPosition(event: TouchEvent): void {
if (event.touches.length > 0 && this.renderer) {
const rect = this.renderer.domElement.getBoundingClientRect();
const touch = event.touches[0];
this.mouse.x = ((touch.clientX - rect.left) / rect.width) * 2 - 1;
this.mouse.y = -((touch.clientY - rect.top) / rect.height) * 2 + 1;
this.updateMousePosition();
}
}
private updateMousePosition(): void {
// Cast ray into the scene
this.raycaster.setFromCamera(this.mouse, this.camera);
// Get the camera's view direction
const cameraDirection = new THREE.Vector3();
this.camera.getWorldDirection(cameraDirection);
// Create a plane perpendicular to the camera's view direction
// Position it at a fixed distance from camera (in the middle of the scene)
const planeNormal = cameraDirection.clone();
const planeDistance = -10; // Negative because the plane normal points toward camera
const plane = new THREE.Plane(planeNormal, planeDistance);
// Calculate the intersection point in 3D space
if (!this.raycaster.ray.intersectPlane(plane, this.mousePosition)) {
// Fallback if no intersection (shouldn't happen)
this.mousePosition.set(this.mouse.x * 10, this.mouse.y * 10, 0);
}
// Update the cursor position
this.mouseCursor.position.copy(this.mousePosition);
this.mouseCursor.visible = true;
// Don't visualize the ray, but keep track of it for particle interactions
this.rayLine.visible = false;
// Pulse the cursor size
const scale = 1 + Math.sin(this.time * 5) * 0.2;
this.mouseCursor.scale.set(scale, scale, scale);
}
private onWindowResize = (): void => {
if (!this.container || !this.renderer) return;
const width = this.container.clientWidth;
const height = this.container.clientHeight;
this.camera.aspect = width / height;
this.camera.updateProjectionMatrix();
this.renderer.setSize(width, height);
};
private animate = (): void => {
this.animationFrameId = requestAnimationFrame(this.animate);
// Update time and delta
const delta = this.clock.getDelta();
this.time += delta;
// Update shader uniforms
(this.particleSystem.material as THREE.ShaderMaterial).uniforms.time.value = this.time;
// Update cursor effect if visible
if (this.mouseCursor.visible) {
// Make cursor material pulse with time
const pulse = 0.5 + Math.sin(this.time * 3) * 0.3;
(this.mouseCursor.material as THREE.MeshBasicMaterial).opacity = pulse;
// If mouse is pressed, make cursor larger
const baseScale = this.mousePressed ? 1.5 : 1.0;
const scale = baseScale + Math.sin(this.time * 5) * 0.2;
this.mouseCursor.scale.set(scale, scale, scale);
}
// Update particle positions
this.updateParticles(delta);
// Update lights for dramatic effect
const lightMovementSpeed = 0.5;
this.scene.children.forEach(child => {
if (child instanceof THREE.PointLight) {
child.position.x += Math.sin(this.time * lightMovementSpeed) * 0.05;
child.position.y += Math.cos(this.time * lightMovementSpeed * 1.3) * 0.05;
child.position.z += Math.sin(this.time * lightMovementSpeed * 0.7) * 0.05;
// Keep lights within bounds
if (Math.abs(child.position.x) > 15) child.position.x *= 0.98;
if (Math.abs(child.position.y) > 15) child.position.y *= 0.98;
if (Math.abs(child.position.z) > 15) child.position.z *= 0.98;
}
});
// Render the scene
this.renderer.render(this.scene, this.camera);
};
private updateParticles(delta: number): void {
const positionAttr = this.particleSystem.geometry.attributes.position;
const sizeAttr = this.particleSystem.geometry.attributes.size;
// Phase 1: Update spatial grid for repulsion
if (this.repulsionEnabled) {
this.spatialGrid.clear();
// Populate grid with current particle positions
for (let i = 0; i < PARTICLE_COUNT; i++) {
const i3 = i * 3;
const x = positionAttr.array[i3] as number;
const y = positionAttr.array[i3 + 1] as number;
const z = positionAttr.array[i3 + 2] as number;
this.spatialGrid.addParticle(i, x, y, z);
}
}
// Phase 2: Update each particle
for (let i = 0; i < PARTICLE_COUNT; i++) {
const i3 = i * 3;
const x = positionAttr.array[i3] as number;
const y = positionAttr.array[i3 + 1] as number;
const z = positionAttr.array[i3 + 2] as number;
// Current position
const particlePos = new THREE.Vector3(x, y, z);
// Movement vector (accumulate all forces)
const movement = new THREE.Vector3();
// Apply curl noise for interesting motion
const curl = this.curlNoise(x * 0.1, y * 0.1, z * 0.1 + this.time * 0.1);
movement.add(curl.multiplyScalar(CURL_STRENGTH * delta));
// Add slight random noise
movement.x += (Math.random() - 0.5) * NOISE_STRENGTH;
movement.y += (Math.random() - 0.5) * NOISE_STRENGTH;
movement.z += (Math.random() - 0.5) * NOISE_STRENGTH;
// Add particle repulsion if enabled
if (this.repulsionEnabled) {
const repulsionForce = new THREE.Vector3();
// Get potential neighbors from spatial grid
const neighbors = this.spatialGrid.getNeighbors(x, y, z);
// Calculate repulsion from nearby particles
for (let j = 0; j < neighbors.length; j++) {
const neighborIndex = neighbors[j];
// Skip self
if (neighborIndex === i) continue;
const ni3 = neighborIndex * 3;
const nx = positionAttr.array[ni3] as number;
const ny = positionAttr.array[ni3 + 1] as number;
const nz = positionAttr.array[ni3 + 2] as number;
// Calculate distance
const dx = x - nx;
const dy = y - ny;
const dz = z - nz;
const distSq = dx * dx + dy * dy + dz * dz;
// Only repel if within repulsion radius
if (distSq < REPEL_RADIUS * REPEL_RADIUS && distSq > 0.0001) {
const dist = Math.sqrt(distSq);
const force = 1.0 - dist / REPEL_RADIUS;
// Normalize and scale repulsion vector
const repelX = dx / dist * force * REPEL_STRENGTH;
const repelY = dy / dist * force * REPEL_STRENGTH;
const repelZ = dz / dist * force * REPEL_STRENGTH;
repulsionForce.x += repelX;
repulsionForce.y += repelY;
repulsionForce.z += repelZ;
}
}
// Add repulsion force to movement
movement.add(repulsionForce);
}
// Always check distance to mouse for nearby particles
const distToMouse = particlePos.distanceTo(this.mousePosition);
if (distToMouse < REPULSION_RADIUS) {
// Very close particles are pushed away from cursor point
const force = particlePos.clone().sub(this.mousePosition);
force.normalize().multiplyScalar(REPULSION_STRENGTH * (1 - distToMouse / REPULSION_RADIUS));
movement.add(force);
// Make particles glow when interacted with
sizeAttr.array[i] = Math.min(sizeAttr.array[i] as number + 0.02, PARTICLE_SIZE_MAX * 1.5);
}
// Only apply gravity if enabled
else if (this.gravityEnabled) {
// For ray attraction, calculate distance from particle to the ray
const ray = this.raycaster.ray;
// Calculate the distance from particle to the ray
const closestPointOnRay = ray.closestPointToPoint(particlePos, new THREE.Vector3());
// Distance from particle to closest point on ray
const distToRay = particlePos.distanceTo(closestPointOnRay);
// Only affect particles within range
if (distToRay < ATTRACTION_RADIUS * 8) {
// Calculate vector from particle to closest point on ray
const toRay = closestPointOnRay.clone().sub(particlePos);
// Calculate perpendicular direction for orbital motion
// Cross product with ray direction gives us a vector perpendicular to both
const rayDirection = ray.direction.clone();
const orbitDirection = new THREE.Vector3().crossVectors(toRay, rayDirection).normalize();
// Mix of attraction to ray and perpendicular orbital force
const attractStrength = ATTRACTION_STRENGTH * 3 * (1 - distToRay / (ATTRACTION_RADIUS * 8));
const orbitStrength = ATTRACTION_STRENGTH * 5 * (1 - distToRay / (ATTRACTION_RADIUS * 6));
// Add attraction to ray
const attractForce = toRay.clone().normalize().multiplyScalar(attractStrength);
// Add perpendicular force for orbital motion
const orbitForce = orbitDirection.multiplyScalar(orbitStrength);
// Combined force
movement.add(attractForce);
movement.add(orbitForce);
// Slightly increase size for particles near the ray
sizeAttr.array[i] = Math.min(sizeAttr.array[i] as number + 0.01, PARTICLE_SIZE_MAX * 1.2);
}
}
// Gradually return to normal size
if (!this.gravityEnabled && (sizeAttr.array[i] as number) > PARTICLE_SIZE_MAX) {
sizeAttr.array[i] = (sizeAttr.array[i] as number) - 0.01;
}
// Apply containment force (keep particles within a sphere)
const distToCenter = particlePos.length();
if (distToCenter > SPREAD_RADIUS) {
const containmentForce = particlePos.clone().negate().normalize().multiplyScalar(0.02);
movement.add(containmentForce);
}
// Update position
positionAttr.array[i3] = x + movement.x;
positionAttr.array[i3 + 1] = y + movement.y;
positionAttr.array[i3 + 2] = z + movement.z;
}
// Mark attributes for update
positionAttr.needsUpdate = true;
sizeAttr.needsUpdate = true;
}
public teardown(): void {
if (!this.isInitialized) return;
// Stop animation loop
if (this.animationFrameId) {
cancelAnimationFrame(this.animationFrameId);
this.animationFrameId = null;
}
// Remove event listeners
window.removeEventListener('resize', this.onWindowResize);
window.removeEventListener('keydown', this.onKeyDown);
if (this.renderer?.domElement) {
this.renderer.domElement.removeEventListener('mousemove', this.onMouseMove);
this.renderer.domElement.removeEventListener('mousedown', () => { this.mousePressed = true; });
this.renderer.domElement.removeEventListener('mouseup', () => { this.mousePressed = false; });
this.renderer.domElement.removeEventListener('touchstart', this.onTouchStart);
this.renderer.domElement.removeEventListener('touchmove', this.onTouchMove);
this.renderer.domElement.removeEventListener('touchend', () => { this.mousePressed = false; });
}
// Dispose of Three.js resources
if (this.particleSystem) {
if (this.particleSystem.geometry) this.particleSystem.geometry.dispose();
if (this.particleSystem.material) {
if (Array.isArray(this.particleSystem.material)) {
this.particleSystem.material.forEach(material => material.dispose());
} else {
this.particleSystem.material.dispose();
}
}
this.scene.remove(this.particleSystem);
}
// Remove all lights
this.scene.children.forEach(child => {
if (child instanceof THREE.Light) {
this.scene.remove(child);
}
});
// Clean up scene
while (this.scene.children.length > 0) {
const object = this.scene.children[0];
this.scene.remove(object);
}
// Remove renderer
if (this.renderer) {
this.renderer.domElement.parentNode?.removeChild(this.renderer.domElement);
this.renderer.dispose();
}
this.isInitialized = false;
this.container = null;
}
}
// Create instance and export the functions
const cosmicParticles = new CosmicParticles();
export const initialize = (container: HTMLElement | null): void => {
cosmicParticles.initialize(container);
};
export const teardown = (): void => {
cosmicParticles.teardown();
};