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();
};