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                                                                  4x   4x 2000x   2000x 2000x 2000x   2000x 2000x 2000x     4x                         4x     4x 4x   4x 4x 4x   4x 4x             4x   4x 4x       4x                           4x                                
/**
 * 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);
 
  for (let i = 0; i < count; i++) {
    const t = i / count; // Normalized index [0, 1]
 
    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());
 
  const spreadX = 40 * scale;
  const spreadY = 20;
  const spreadZ = 40 * scale;
 
  useEffect(() => {
    const positions = generateParticlePositions(
      count,
      spreadX,
      spreadY,
      spreadZ
    );
    
    geometry.setAttribute("position", new THREE.BufferAttribute(positions, 3));
 
    return () => {
      geometry.dispose();
    };
  }, [count, spreadX, spreadY, spreadZ, geometry]);
 
  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;