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 137 | 1x 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; |