All files / components/shared/three/scene AtmosphericParticles3D.tsx

73.52% Statements 25/34
42.85% Branches 3/7
83.33% Functions 5/6
76.66% Lines 23/30

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128                                                                  53x       53x 26500x     26500x 26500x 26500x   26500x 26500x 26500x     53x                             4x 123x 123x     123x 123x 123x     123x 53x                 53x   53x   53x               123x                           123x                                
/**
 * AtmosphericParticles3D - Three.js 3D atmospheric particle effects
 *
 * Renders rain/mist particles for environmental depth in the combat arena
 * Creates an immersive cyberpunk urban night atmosphere
 */
 
import { useFrame } from "@react-three/fiber";
import React, { useEffect, useRef, useState } from "react";
import * as THREE from "three";
 
/**
 * Props for the AtmosphericParticles3D component.
 */
export interface AtmosphericParticles3DProps {
  /** Number of particles to render. Defaults to 500 */
  readonly count?: number;
  /** Scale factor for particle spread (1.0 = desktop, <1.0 = mobile). Defaults to 1.0 */
  readonly scale?: number;
  /** Particle fall speed. Defaults to 2 */
  readonly speed?: number;
}
 
/**
 * Generate particle positions using a deterministic pattern
 * This avoids Math.random() in render functions for React purity
 */
function generateParticlePositions(
  count: number,
  spreadX: number,
  spreadY: number,
  spreadZ: number
): Float32Array {
  const pos = new Float32Array(count * 3);
 
  // Use a simple pseudo-random pattern based on index
  // This creates deterministic but varied positions
  for (let i = 0; i < count; i++) {
    const t = i / count; // Normalized index [0, 1]
 
    // Create pseudo-random offsets using trigonometric functions
    const offsetX = Math.sin(t * 123.456) * Math.cos(t * 789.012);
    const offsetY = Math.sin(t * 234.567) * Math.cos(t * 890.123);
    const offsetZ = Math.sin(t * 345.678) * Math.cos(t * 901.234);
 
    pos[i * 3] = offsetX * spreadX * 0.5;
    pos[i * 3 + 1] = offsetY * spreadY * 0.5 + spreadY * 0.5; // Bias upward
    pos[i * 3 + 2] = offsetZ * spreadZ * 0.5;
  }
 
  return pos;
}
 
/**
 * AtmosphericParticles3D Component
 * Creates rain/mist particle effects for atmospheric depth
 *
 * Performance optimized with:
 * - BufferGeometry for efficient rendering
 * - Additive blending for transparent particles
 * - Configurable particle count for mobile optimization
 * - Deterministic position generation (no Math.random in render)
 */
export const AtmosphericParticles3D: React.FC<
  AtmosphericParticles3DProps
> = ({ count = 500, scale = 1.0, speed = 2 }) => {
  const particlesRef = useRef<THREE.Points>(null);
  const [geometry] = useState(() => new THREE.BufferGeometry());
 
  // Scale-aware spread dimensions
  const spreadX = 40 * scale;
  const spreadY = 20;
  const spreadZ = 40 * scale;
 
  // Initialize particle positions once on mount
  useEffect(() => {
    const positions = generateParticlePositions(
      count,
      spreadX,
      spreadY,
      spreadZ
    );
    
    // Note: BufferAttribute doesn't have a dispose method in Three.js
    // The geometry cleanup on unmount will handle attribute cleanup
    geometry.setAttribute("position", new THREE.BufferAttribute(positions, 3));
 
    return () => {
      // Clean up geometry on unmount (this also cleans up attributes)
      geometry.dispose();
    };
  }, [count, spreadX, spreadY, spreadZ, geometry]);
 
  // Animate particles (rain/mist effect)
  // Performance note: Current implementation modifies geometry buffer every frame.
  // This is acceptable for up to ~500 particles but may impact performance beyond 1000.
  // For larger particle counts, consider using shader-based vertex displacement or instancing.
  useFrame((_state, delta) => {
    if (!particlesRef.current) return;
 
    const positions =
      particlesRef.current.geometry.attributes.position.array as Float32Array;
    for (let i = 0; i < count; i++) {
      positions[i * 3 + 1] -= delta * speed; // Fall down
      if (positions[i * 3 + 1] < 0) {
        positions[i * 3 + 1] = spreadY; // Reset to top
      }
    }
    particlesRef.current.geometry.attributes.position.needsUpdate = true;
  });
 
  return (
    <points ref={particlesRef} geometry={geometry}>
      <pointsMaterial
        size={0.05}
        color={0xffffff}
        transparent
        opacity={0.3}
        sizeAttenuation
        blending={THREE.AdditiveBlending}
        depthWrite={false}
      />
    </points>
  );
};
 
export default AtmosphericParticles3D;