0

I am having issues putting together a React three fiber component. I have the basics of what I want, a cool animation with a nice shader. However, I am a bit of a noob with React. The component becomes more and more laggy after 15 seconds. I assume its a memory leak or something of the sort. But I dont know where the issue would be.

import React, { useEffect, useMemo, useRef, useState } from "react";
import { Canvas, useFrame, useThree } from "@react-three/fiber";
import { Bloom, EffectComposer } from "@react-three/postprocessing";
import * as THREE from "three";

const PARTICLE_COUNT = 15000;
const STAR_COUNT = 6000;
const TRANSITION_DURATION = 2000;
const Y_OFFSET = 10;

const createSphere = (i: number, count: number): THREE.Vector3 => {
  const t = i / count;
  const phi = Math.acos(2 * t - 1);
  const theta = 2 * Math.PI * (i / count) * Math.sqrt(count);
  return new THREE.Vector3(
    Math.sin(phi) * Math.cos(theta) * 30 + 40,
    Math.sin(phi) * Math.sin(theta) * 30 + Y_OFFSET,
    Math.cos(phi) * 30
  );
};

const createSpiral = (i: number, count: number): THREE.Vector3 => {
  const t = i / count;
  const numArms = 3;
  const armIndex = i % numArms;
  const angleOffset = ((2 * Math.PI) / numArms) * armIndex;
  const angle = t ** 0.7 * 15 + angleOffset;
  const radius = t * 40;
  const height = Math.sin(t * Math.PI * 2) * 5;
  return new THREE.Vector3(
    Math.cos(angle) * radius + 40,
    Math.sin(angle) * radius + Y_OFFSET,
    height
  );
};

const createGrid = (i: number, count: number): THREE.Vector3 => {
  const sideLength = Math.ceil(Math.cbrt(count));
  const spacing = 60 / sideLength;
  const halfGrid = ((sideLength - 1) * spacing) / 2;
  const iz = Math.floor(i / (sideLength * sideLength));
  const iy = Math.floor((i % (sideLength * sideLength)) / sideLength);
  const ix = i % sideLength;
  if (
    ix === Math.floor(sideLength / 2) &&
    iy === Math.floor(sideLength / 2) &&
    iz === Math.floor(sideLength / 2) &&
    sideLength % 2 !== 0
  ) {
    return new THREE.Vector3(spacing * 0.1 + 40, spacing * 0.1 + Y_OFFSET, spacing * 0.1);
  }
  return new THREE.Vector3(
    ix * spacing - halfGrid + 40,
    iy * spacing - halfGrid + Y_OFFSET,
    iz * spacing - halfGrid
  );
};

const createPyramid = (i: number, count: number): THREE.Vector3 => {
  const baseSize = 45;
  const height = 50;
  const faces = 4;
  const pointsPerFace = Math.floor(count / faces);
  const face = Math.floor(i / pointsPerFace);
  const idxInFace = i % pointsPerFace;
  const t = idxInFace / pointsPerFace;

  const pyramidYOffset = -10;

  const apex = new THREE.Vector3(40, height + pyramidYOffset, 0);
  const base = [
    new THREE.Vector3(40 - baseSize / 2, pyramidYOffset, -baseSize / 2),
    new THREE.Vector3(40 + baseSize / 2, pyramidYOffset, -baseSize / 2),
    new THREE.Vector3(40 + baseSize / 2, pyramidYOffset, baseSize / 2),
    new THREE.Vector3(40 - baseSize / 2, pyramidYOffset, baseSize / 2),
  ];

  const v0 = apex;
  const v1 = base[face];
  const v2 = base[(face + 1) % 4];

  let a = Math.random();
  let b = Math.random();
  if (a + b > 1) {
    a = 1 - a;
    b = 1 - b;
  }

  const c = 1 - a - b;

  const x = a * v0.x + b * v1.x + c * v2.x;
  const y = a * v0.y + b * v1.y + c * v2.y;
  const z = a * v0.z + b * v1.z + c * v2.z;

  return new THREE.Vector3(x, y, z);
};

const createTorus = (i: number, count: number): THREE.Vector3 => {
  const R = 30;
  const r = 10;
  const angle1 = Math.random() * Math.PI * 2;
  const angle2 = Math.random() * Math.PI * 2;
  return new THREE.Vector3(
    (R + r * Math.cos(angle2)) * Math.cos(angle1) + 40,
    (R + r * Math.cos(angle2)) * Math.sin(angle1) + Y_OFFSET,
    r * Math.sin(angle2)
  );
};

const PATTERNS = [createSphere, createSpiral, createPyramid, createGrid, createTorus];

const COLOR_PALETTES = [
  [
    new THREE.Color(0x0077ff),
    new THREE.Color(0x00aaff),
    new THREE.Color(0x44ccff),
    new THREE.Color(0x0055cc),
  ],
  [
    new THREE.Color(0x8800cc),
    new THREE.Color(0xcc00ff),
    new THREE.Color(0x660099),
    new THREE.Color(0xaa33ff),
  ],
  [
    new THREE.Color(0x00cc66),
    new THREE.Color(0x33ff99),
    new THREE.Color(0x99ff66),
    new THREE.Color(0x008844),
  ],
  [
    new THREE.Color(0xff9900),
    new THREE.Color(0xffcc33),
    new THREE.Color(0xff6600),
    new THREE.Color(0xffaa55),
  ],
  [
    new THREE.Color(0xff3399),
    new THREE.Color(0xff66aa),
    new THREE.Color(0xff0066),
    new THREE.Color(0xcc0055),
  ],
];

const Stars: React.FC = () => {
  const materialRef = useRef<THREE.ShaderMaterial>(null!);

  const [positions, colors, starInfo] = useMemo(() => {
    const positions = new Float32Array(STAR_COUNT * 3);
    const colors = new Float32Array(STAR_COUNT * 3);
    const starInfo = new Float32Array(STAR_COUNT);
    const color = new THREE.Color();
    const starRadius = 700;
    for (let i = 0; i < STAR_COUNT; i++) {
      const u = Math.random();
      const v = Math.random();
      const theta = 2 * Math.PI * u;
      const phi = Math.acos(2 * v - 1);
      positions.set(
        [
          starRadius * Math.sin(phi) * Math.cos(theta),
          starRadius * Math.sin(phi) * Math.sin(theta),
          starRadius * Math.cos(phi),
        ],
        i * 3
      );
      const rand = Math.random();
      if (rand < 0.7) {
        color.setHSL(0, 0, Math.random() * 0.2 + 0.7);
      } else if (rand < 0.9) {
        color.setHSL(0.6, 0.7, Math.random() * 0.2 + 0.6);
      } else {
        color.setHSL(0.1, 0.7, Math.random() * 0.2 + 0.6);
      }
      colors.set([color.r, color.g, color.b], i * 3);
      starInfo[i] = Math.random();
    }
    return [positions, colors, starInfo];
  }, []);

  useFrame(({ clock }) => {
    materialRef.current.uniforms.time.value = clock.getElapsedTime();
  });

  return (
    <points renderOrder={-1}>
      <bufferGeometry>
        <bufferAttribute attach="attributes-position" args={[positions, 3]} />
        <bufferAttribute attach="attributes-color" args={[colors, 3]} />
        <bufferAttribute attach="attributes-starInfo" args={[starInfo, 1]} />
      </bufferGeometry>
      <shaderMaterial
        ref={materialRef}
        uniforms={{ time: { value: 0 }, pointSize: { value: 1.7 } }}
        vertexShader={`attribute float starInfo; varying vec3 vColor; varying float vStarInfo; uniform float pointSize; void main() { vColor = color; vStarInfo = starInfo; vec4 mvPosition = modelViewMatrix * vec4(position, 1.0); gl_Position = projectionMatrix * mvPosition; gl_PointSize = pointSize * (150.0 / -mvPosition.z); }`}
        fragmentShader={`uniform float time; varying vec3 vColor; varying float vStarInfo; void main() { vec2 uv = gl_PointCoord - vec2(0.5); float dist = length(uv); if (dist > 0.5) discard; float speed = vStarInfo * 2.0 + 0.5; float offset = vStarInfo * 3.14 * 2.0; float twinkle = sin(time * speed + offset) * 0.2 + 0.8; float alpha = pow(1.0 - dist * 2.0, 1.5); gl_FragColor = vec4(vColor, alpha * twinkle * 0.8); }`}
        transparent
        depthWrite={false}
        blending={THREE.AdditiveBlending}
        vertexColors
      />
    </points>
  );
};

interface ParticlesProps {
  targetPattern: number;
  worldMouse: THREE.Vector3;
  onTransitionComplete: () => void;
}

const Particles: React.FC<ParticlesProps> = ({
  targetPattern,
  worldMouse,
  onTransitionComplete,
}) => {
  const pointsRef = useRef<THREE.Points>(null!);
  const materialRef = useRef<THREE.ShaderMaterial>(null!);

  // Transition state
  const transitionRef = useRef({
    isTransitioning: false,
    startTime: 0,
    fromPos: null as Float32Array | null,
    toPos: null as Float32Array | null,
    fromCol: null as Float32Array | null,
    toCol: null as Float32Array | null,
    currentPattern: 0,
  });

  const [positions, colors, sizes, indices, particleTypes] = useMemo(() => {
    const positions = new Float32Array(PARTICLE_COUNT * 3);
    const colors = new Float32Array(PARTICLE_COUNT * 3);
    const sizes = new Float32Array(PARTICLE_COUNT);
    const indices = new Float32Array(PARTICLE_COUNT);
    const particleTypes = new Float32Array(PARTICLE_COUNT);
    const initialPattern = PATTERNS[0];
    const initialPalette = COLOR_PALETTES[0];

    for (let i = 0; i < PARTICLE_COUNT; i++) {
      indices[i] = i;
      particleTypes[i] = Math.floor(Math.random() * 3);
      const pos = initialPattern(i, PARTICLE_COUNT);
      positions.set([pos.x, pos.y, pos.z], i * 3);
      const colorIndex = Math.floor(Math.random() * initialPalette.length);
      const baseColor = initialPalette[colorIndex];
      const variation = 0.85 + Math.random() * 0.3;
      const finalColor = baseColor.clone().multiplyScalar(variation);
      colors.set([finalColor.r, finalColor.g, finalColor.b], i * 3);
      sizes[i] = 1.0 + Math.random() * 1.5;
    }

    return [positions, colors, sizes, indices, particleTypes];
  }, []);

  // Initialize transition when target pattern changes
  useEffect(() => {
    if (targetPattern !== transitionRef.current.currentPattern) {
      // Store current positions and colors as starting point
      const currentPos = pointsRef.current.geometry.attributes.position.array as Float32Array;
      const currentCol = pointsRef.current.geometry.attributes.color.array as Float32Array;

      transitionRef.current.fromPos = new Float32Array(currentPos);
      transitionRef.current.fromCol = new Float32Array(currentCol);

      // Generate target positions and colors
      const toPos = new Float32Array(PARTICLE_COUNT * 3);
      const toCol = new Float32Array(PARTICLE_COUNT * 3);
      const patternFn = PATTERNS[targetPattern];
      const palette = COLOR_PALETTES[targetPattern];

      for (let i = 0; i < PARTICLE_COUNT; i++) {
        const p = patternFn(i, PARTICLE_COUNT);
        toPos.set([p.x, p.y, p.z], i * 3);
        const idx = Math.floor(Math.random() * palette.length);
        const base = palette[idx];
        const variation = 0.85 + Math.random() * 0.3;
        const final = base.clone().multiplyScalar(variation);
        toCol.set([final.r, final.g, final.b], i * 3);
      }

      transitionRef.current.toPos = toPos;
      transitionRef.current.toCol = toCol;
      transitionRef.current.isTransitioning = true;
      transitionRef.current.startTime = Date.now();
      transitionRef.current.currentPattern = targetPattern;
    }
  }, [targetPattern]);

  useFrame(({ clock }) => {
    materialRef.current.uniforms.time.value = clock.getElapsedTime();
    materialRef.current.uniforms.mousePos.value.copy(worldMouse);

    const transition = transitionRef.current;

    if (
      transition.isTransitioning &&
      transition.fromPos &&
      transition.toPos &&
      transition.fromCol &&
      transition.toCol
    ) {
      const elapsed = Date.now() - transition.startTime;
      const progress = Math.min(elapsed / TRANSITION_DURATION, 1);

      const easeProgress =
        progress < 0.5 ? 4 * progress * progress * progress : 1 - (-2 * progress + 2) ** 3 / 2;

      const posAttr = pointsRef.current.geometry.attributes.position as THREE.BufferAttribute;
      const colAttr = pointsRef.current.geometry.attributes.color as THREE.BufferAttribute;

      // Interpolate positions and colors
      for (let i = 0; i < PARTICLE_COUNT * 3; i++) {
        posAttr.array[i] =
          transition.fromPos[i] * (1 - easeProgress) + transition.toPos[i] * easeProgress;
        colAttr.array[i] =
          transition.fromCol[i] * (1 - easeProgress) + transition.toCol[i] * easeProgress;
      }

      posAttr.needsUpdate = true;
      colAttr.needsUpdate = true;

      // Check if transition is complete
      if (progress >= 1) {
        transition.isTransitioning = false;
        transition.fromPos = null;
        transition.toPos = null;
        transition.fromCol = null;
        transition.toCol = null;
        onTransitionComplete();
      }
    }
  });

  return (
    <points ref={pointsRef}>
      <bufferGeometry>
        <bufferAttribute attach="attributes-position" args={[positions, 3]} />
        <bufferAttribute attach="attributes-color" args={[colors, 3]} />
        <bufferAttribute attach="attributes-size" args={[sizes, 1]} />
        <bufferAttribute attach="attributes-index" args={[indices, 1]} />
        <bufferAttribute attach="attributes-particleType" args={[particleTypes, 1]} />
      </bufferGeometry>
      <shaderMaterial
        ref={materialRef}
        uniforms={{ time: { value: 0 }, mousePos: { value: new THREE.Vector3(10000, 10000, 0) } }}
        vertexShader={`
          uniform float time; uniform vec3 mousePos; attribute float size; attribute float index; attribute float particleType; varying vec3 vColor; varying float vDistanceToMouse; varying float vType; varying float vIndex;
          float rand(vec2 co){ return fract(sin(dot(co.xy ,vec2(12.9898,78.233))) * 43758.5453); }
          void main() {
            vColor = color; vType = particleType; vIndex = index; vec3 pos = position; float T = time * 0.5; float idx = index * 0.01;
            float noiseFactor1 = sin(idx * 30.0 + T * 15.0) * 0.4 + 0.6; vec3 offset1 = vec3( cos(T * 1.2 + idx * 5.0) * noiseFactor1, sin(T * 0.9 + idx * 6.0) * noiseFactor1, cos(T * 1.1 + idx * 7.0) * noiseFactor1 ) * 0.4;
            float noiseFactor2 = rand(vec2(idx, idx * 0.5)) * 0.5 + 0.5; float speedFactor = 0.3; vec3 offset2 = vec3( sin(T * speedFactor * 1.3 + idx * 1.1) * noiseFactor2, cos(T * speedFactor * 1.7 + idx * 1.2) * noiseFactor2, sin(T * speedFactor * 1.1 + idx * 1.3) * noiseFactor2 ) * 0.8;
            pos += offset1 + offset2;
            vec3 toMouse = mousePos - pos; float dist = length(toMouse); vDistanceToMouse = 0.0; float interactionRadius = 30.0; float falloffStart = 5.0;
            if (dist < interactionRadius) { float influence = smoothstep(interactionRadius, falloffStart, dist); vec3 repelDir = normalize(pos - mousePos); pos += repelDir * influence * 15.0; vDistanceToMouse = influence; }
            vec4 mvPosition = modelViewMatrix * vec4(pos, 1.0); gl_Position = projectionMatrix * mvPosition; float perspectiveFactor = 700.0 / -mvPosition.z; gl_PointSize = size * perspectiveFactor * (1.0 + vDistanceToMouse * 0.5);
          }`}
        fragmentShader={`
          uniform float time; varying vec3 vColor; varying float vDistanceToMouse; varying float vType; varying float vIndex;
          vec3 rgb2hsl( vec3 c ){ vec4 K = vec4(0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0); vec4 p = mix( vec4( c.bg, K.wz ), vec4( c.gb, K.xy ), step( c.b, c.g ) ); vec4 q = mix( vec4( p.xyw, c.r ), vec4( c.r, p.yzx ), step( p.x, c.r ) ); float d = q.x - min( q.w, q.y ); float e = 1.0e-10; return vec3( abs(q.z + (q.w - q.y) / (6.0 * d + e)), d / (q.x + e), q.x); }
          vec3 hsl2rgb( vec3 c ){ vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0); vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www); return c.z * mix( K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y ); }
          void main() {
            vec2 uv = gl_PointCoord * 2.0 - 1.0; float dist = length(uv); if (dist > 1.0) { discard; } float alpha = 0.0; vec3 baseColor = vColor;
            vec3 hsl = rgb2hsl(baseColor); float hueShift = sin(time * 0.05 + vIndex * 0.001) * 0.02; hsl.x = fract(hsl.x + hueShift); baseColor = hsl2rgb(hsl); vec3 finalColor = baseColor;
            if (vType < 0.5) { float core = smoothstep(0.2, 0.15, dist) * 0.9; float glow = pow(max(0.0, 1.0 - dist), 3.0) * 0.5; alpha = core + glow; }
            else if (vType < 1.5) { float ringWidth = 0.1; float ringCenter = 0.65; float ringShape = exp(-pow(dist - ringCenter, 2.0) / (2.0 * ringWidth * ringWidth)); alpha = smoothstep(0.1, 0.5, ringShape) * 0.8; alpha += smoothstep(0.3, 0.0, dist) * 0.1; }
            else { float pulse = sin(dist * 5.0 - time * 2.0 + vIndex * 0.1) * 0.1 + 0.9; alpha = pow(max(0.0, 1.0 - dist), 2.5) * pulse * 0.9; }
            finalColor = mix(finalColor, finalColor * 1.3 + 0.1, vDistanceToMouse * 1.0); alpha *= 0.9; alpha = clamp(alpha, 0.0, 1.0); gl_FragColor = vec4(finalColor * alpha, alpha);
          }`}
        transparent
        depthWrite={false}
        blending={THREE.AdditiveBlending}
        vertexColors
      />
    </points>
  );
};

interface SceneProps {
  targetPattern: number;
  worldMouse: THREE.Vector3;
  onTransitionComplete: () => void;
}

const Scene: React.FC<SceneProps> = ({ targetPattern, worldMouse, onTransitionComplete }) => {
  const { mouse } = useThree();
  const plane = useMemo(() => new THREE.Plane(new THREE.Vector3(0, 0, 1), 0), []);

  useFrame(({ camera, clock, raycaster }) => {
    const time = clock.getElapsedTime();
    const cameraRadius = 120;
    const angleX = time * 0.08;
    const angleY = time * 0.06;

    camera.position.x = Math.cos(angleX) * cameraRadius + 20;
    camera.position.z = Math.sin(angleX) * cameraRadius;
    camera.position.y = Math.sin(angleY) * 35 + 5;
    camera.lookAt(40, 0, 0);

    raycaster.setFromCamera(mouse, camera);
    const intersectPoint = new THREE.Vector3();
    if (raycaster.ray.intersectPlane(plane, intersectPoint)) {
      worldMouse.lerp(intersectPoint, 0.1);
    }
  });

  return (
    <>
      <Stars />
      <Particles
        targetPattern={targetPattern}
        worldMouse={worldMouse}
        onTransitionComplete={onTransitionComplete}
      />
      <EffectComposer>
        <Bloom luminanceThreshold={0.3} intensity={0.85} levels={9} mipmapBlur />
      </EffectComposer>
    </>
  );
};

const CosmicWeb: React.FC = () => {
  const [isNameVisible, setIsNameVisible] = useState<boolean>(true);
  const [currentPattern, setCurrentPattern] = useState(0);
  const [targetPattern, setTargetPattern] = useState(0);
  const [isTransitioning, setIsTransitioning] = useState(false);
  const worldMouse = useRef(new THREE.Vector3(10000, 10000, 0)).current;

  const changePattern = () => {
    if (isTransitioning) return;

    const nextPattern = (currentPattern + 1) % PATTERNS.length;
    setTargetPattern(nextPattern);
    setIsNameVisible(true);
    setIsTransitioning(true);

    setTimeout(() => setIsNameVisible(false), 2500);
  };

  const handleTransitionComplete = () => {
    setCurrentPattern(targetPattern);
    setIsTransitioning(false);
  };

  useEffect(() => {
    const interval = setInterval(changePattern, 5000);
    return () => clearInterval(interval);
  }, [currentPattern, isTransitioning]);

  return (
    <div className="fixed h-full w-full overflow-hidden bg-black">
      <Canvas
        camera={{ fov: 65, position: [0, 0, 100], near: 0.1, far: 1500 }}
        dpr={window.devicePixelRatio}
      >
        <Scene
          targetPattern={targetPattern}
          worldMouse={worldMouse}
          onTransitionComplete={handleTransitionComplete}
        />
      </Canvas>
    </div>
  );
};

export default CosmicWeb;

How can I optomize this? What steps can I take to improve it?

0

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.