All files / components/decorations shooting-star.tsx

90.19% Statements 46/51
61.9% Branches 13/21
100% Functions 6/6
89.79% Lines 44/49

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 129 130 131 132 133 134 135 136 1371x                       1x 2x 2x   2x       1x       1x           1x   2x 2x 2x 2x   2x 2x 2x     2x                   2x 2x       2x 2x     2x 2x 2x     2x 2x 2x 2x     2x                     2x 2x 2x 2x     2x 2x 2x 2x 2x             2x       2x 2x     2x 2x 2x       2x                                            
"use client";
import React, { useEffect, useRef } from "react";
 
interface ShootingStar {
  x: number;
  y: number;
  angle: number;
  scale: number;
  speed: number;
  distance: number;
}
 
const getRandomStartPoint = () => {
  const side = Math.floor(Math.random() * 4);
  const offset = Math.random() * window.innerWidth;
 
  switch (side) {
    case 0:
      return { x: offset, y: 0, angle: 45 };
    case 1:
      return { x: window.innerWidth, y: offset, angle: 135 };
    case 2:
      return { x: offset, y: window.innerHeight, angle: 225 };
    case 3:
      return { x: 0, y: offset, angle: 315 };
    default:
      return { x: 0, y: 0, angle: 45 };
  }
};
 
const ShootingStars: React.FC = () => {
  // Use refs instead of state to prevent 60fps React re-renders
  const rectRef = useRef<SVGRectElement>(null);
  const starData = useRef<ShootingStar | null>(null);
  const animationFrameId = useRef<number | null>(null);
  const timeoutId = useRef<NodeJS.Timeout | null>(null);
 
  useEffect(() => {
    const spawnStar = () => {
      const { x, y, angle } = getRandomStartPoint();
      
      // Reset the star data
      starData.current = {
        x,
        y,
        angle,
        scale: 1,
        speed: Math.random() * 20 + 10,
        distance: 0,
      };
 
      // Make the rect visible again
      Eif (rectRef.current) {
        rectRef.current.style.display = "block";
      }
 
      // Schedule the next star
      const randomDelay = Math.random() * 4500 + 4200;
      timeoutId.current = setTimeout(spawnStar, randomDelay);
    };
 
    const animate = () => {
      Eif (starData.current && rectRef.current) {
        const current = starData.current;
        
        // Calculate new positions
        const newX = current.x + current.speed * Math.cos((current.angle * Math.PI) / 180);
        const newY = current.y + current.speed * Math.sin((current.angle * Math.PI) / 180);
        const newDistance = current.distance + current.speed;
        const newScale = 1 + newDistance / 100;
 
        // Check bounds
        Iif (
          newX < -20 ||
          newX > window.innerWidth + 20 ||
          newY < -20 ||
          newY > window.innerHeight + 20
        ) {
          // Hide the star when it goes off-screen
          rectRef.current.style.display = "none";
          starData.current = null;
        } else {
          // Update data
          current.x = newX;
          current.y = newY;
          current.distance = newDistance;
          current.scale = newScale;
 
          // Directly mutate the DOM for maximum performance
          const width = 10 * newScale;
          rectRef.current.setAttribute("x", String(newX));
          rectRef.current.setAttribute("y", String(newY));
          rectRef.current.setAttribute("width", String(width));
          rectRef.current.setAttribute(
            "transform",
            `rotate(${current.angle}, ${newX + width / 2}, ${newY + 1})`
          );
        }
      }
 
      animationFrameId.current = requestAnimationFrame(animate);
    };
 
    // Start the loops
    spawnStar();
    animate();
 
    // Proper cleanup on unmount
    return () => {
      Eif (timeoutId.current) clearTimeout(timeoutId.current);
      Eif (animationFrameId.current) cancelAnimationFrame(animationFrameId.current);
    };
  }, []);
 
  return (
    <svg
      width="100%"
      height="100%"
      className="absolute top-0 left-0 pointer-events-none"
    >
      <defs>
        <linearGradient id="star-gradient" x1="0%" y1="0%" x2="100%" y2="100%">
          <stop offset="0%" style={{ stopColor: "#2EB9DF", stopOpacity: 0 }} />
          <stop offset="100%" style={{ stopColor: "#9E00FF", stopOpacity: 1 }} />
        </linearGradient>
      </defs>
      <rect
        ref={rectRef}
        height="2"
        fill="url(#star-gradient)"
        style={{ display: "none" }} // Hidden until spawned
      />
    </svg>
  );
};
 
export default ShootingStars;