Shooting Stars and Stars Background
A shooting star animation on top of a starry background, as seen on figmaplug.in
Shooting StarxStar Background
Installation
Install util dependencies
npm i framer-motion clsx tailwind-merge
Add util file
lib/utils.ts
import { ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
Copy the source code for Shooting Stars
components/ui/shooting-stars.tsx
"use client";
import { cn } from "@/lib/utils";
import React, { useEffect, useState, useRef } from "react";
interface ShootingStar {
id: number;
x: number;
y: number;
angle: number;
scale: number;
speed: number;
distance: number;
}
interface ShootingStarsProps {
minSpeed?: number;
maxSpeed?: number;
minDelay?: number;
maxDelay?: number;
starColor?: string;
trailColor?: string;
starWidth?: number;
starHeight?: number;
className?: string;
}
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 };
}
};
export const ShootingStars: React.FC<ShootingStarsProps> = ({
minSpeed = 10,
maxSpeed = 30,
minDelay = 1200,
maxDelay = 4200,
starColor = "#9E00FF",
trailColor = "#2EB9DF",
starWidth = 10,
starHeight = 1,
className,
}) => {
const [star, setStar] = useState<ShootingStar | null>(null);
const svgRef = useRef<SVGSVGElement>(null);
useEffect(() => {
const createStar = () => {
const { x, y, angle } = getRandomStartPoint();
const newStar: ShootingStar = {
id: Date.now(),
x,
y,
angle,
scale: 1,
speed: Math.random() * (maxSpeed - minSpeed) + minSpeed,
distance: 0,
};
setStar(newStar);
const randomDelay = Math.random() * (maxDelay - minDelay) + minDelay;
setTimeout(createStar, randomDelay);
};
createStar();
return () => {};
}, [minSpeed, maxSpeed, minDelay, maxDelay]);
useEffect(() => {
const moveStar = () => {
if (star) {
setStar((prevStar) => {
if (!prevStar) return null;
const newX =
prevStar.x +
prevStar.speed * Math.cos((prevStar.angle * Math.PI) / 180);
const newY =
prevStar.y +
prevStar.speed * Math.sin((prevStar.angle * Math.PI) / 180);
const newDistance = prevStar.distance + prevStar.speed;
const newScale = 1 + newDistance / 100;
if (
newX < -20 ||
newX > window.innerWidth + 20 ||
newY < -20 ||
newY > window.innerHeight + 20
) {
return null;
}
return {
...prevStar,
x: newX,
y: newY,
distance: newDistance,
scale: newScale,
};
});
}
};
const animationFrame = requestAnimationFrame(moveStar);
return () => cancelAnimationFrame(animationFrame);
}, [star]);
return (
<svg
ref={svgRef}
className={cn("w-full h-full absolute inset-0", className)}
>
{star && (
<rect
key={star.id}
x={star.x}
y={star.y}
width={starWidth * star.scale}
height={starHeight}
fill="url(#gradient)"
transform={`rotate(${star.angle}, ${
star.x + (starWidth * star.scale) / 2
}, ${star.y + starHeight / 2})`}
/>
)}
<defs>
<linearGradient id="gradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style={{ stopColor: trailColor, stopOpacity: 0 }} />
<stop
offset="100%"
style={{ stopColor: starColor, stopOpacity: 1 }}
/>
</linearGradient>
</defs>
</svg>
);
};
Copy the source code for Stars Background
components/ui/stars-background.tsx
"use client";
import { cn } from "@/lib/utils";
import React, {
useState,
useEffect,
useRef,
RefObject,
useCallback,
} from "react";
interface StarProps {
x: number;
y: number;
radius: number;
opacity: number;
twinkleSpeed: number | null;
}
interface StarBackgroundProps {
starDensity?: number;
allStarsTwinkle?: boolean;
twinkleProbability?: number;
minTwinkleSpeed?: number;
maxTwinkleSpeed?: number;
className?: string;
}
export const StarsBackground: React.FC<StarBackgroundProps> = ({
starDensity = 0.00015,
allStarsTwinkle = true,
twinkleProbability = 0.7,
minTwinkleSpeed = 0.5,
maxTwinkleSpeed = 1,
className,
}) => {
const [stars, setStars] = useState<StarProps[]>([]);
const canvasRef: RefObject<HTMLCanvasElement> =
useRef<HTMLCanvasElement>(null);
const generateStars = useCallback(
(width: number, height: number): StarProps[] => {
const area = width * height;
const numStars = Math.floor(area * starDensity);
return Array.from({ length: numStars }, () => {
const shouldTwinkle =
allStarsTwinkle || Math.random() < twinkleProbability;
return {
x: Math.random() * width,
y: Math.random() * height,
radius: Math.random() * 0.05 + 0.5,
opacity: Math.random() * 0.5 + 0.5,
twinkleSpeed: shouldTwinkle
? minTwinkleSpeed +
Math.random() * (maxTwinkleSpeed - minTwinkleSpeed)
: null,
};
});
},
[
starDensity,
allStarsTwinkle,
twinkleProbability,
minTwinkleSpeed,
maxTwinkleSpeed,
]
);
useEffect(() => {
const updateStars = () => {
if (canvasRef.current) {
const canvas = canvasRef.current;
const ctx = canvas.getContext("2d");
if (!ctx) return;
const { width, height } = canvas.getBoundingClientRect();
canvas.width = width;
canvas.height = height;
setStars(generateStars(width, height));
}
};
updateStars();
const resizeObserver = new ResizeObserver(updateStars);
if (canvasRef.current) {
resizeObserver.observe(canvasRef.current);
}
return () => {
if (canvasRef.current) {
resizeObserver.unobserve(canvasRef.current);
}
};
}, [
starDensity,
allStarsTwinkle,
twinkleProbability,
minTwinkleSpeed,
maxTwinkleSpeed,
generateStars,
]);
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext("2d");
if (!ctx) return;
let animationFrameId: number;
const render = () => {
ctx.clearRect(0, 0, canvas.width, canvas.height);
stars.forEach((star) => {
ctx.beginPath();
ctx.arc(star.x, star.y, star.radius, 0, Math.PI * 2);
ctx.fillStyle = `rgba(255, 255, 255, ${star.opacity})`;
ctx.fill();
if (star.twinkleSpeed !== null) {
star.opacity =
0.5 +
Math.abs(Math.sin((Date.now() * 0.001) / star.twinkleSpeed) * 0.5);
}
});
animationFrameId = requestAnimationFrame(render);
};
render();
return () => {
cancelAnimationFrame(animationFrameId);
};
}, [stars]);
return (
<canvas
ref={canvasRef}
className={cn("h-full w-full absolute inset-0", className)}
/>
);
};
Props
Shooting Stars
Prop | Type | Default | Description |
---|---|---|---|
minSpeed | number | 10 | Minimum speed of the shooting stars |
maxSpeed | number | 30 | Maximum speed of the shooting stars |
minDelay | number | 4200 | Minimum delay between shooting stars (in milliseconds) |
maxDelay | number | 8700 | Maximum delay between shooting stars (in milliseconds) |
starColor | string | "#9E00FF" | Color of the star (end of the gradient) |
trailColor | string | "#2EB9DF" | Color of the star's trail (start of the gradient) |
starWidth | number | 10 | Width of the shooting star |
starHeight | number | 1 | Height of the shooting star |
className | string | undefined | Additional CSS classes to apply to the SVG container |
Stars Background
Prop | Type | Default | Description |
---|---|---|---|
starDensity | number | 0.00015 | Determines the number of stars per pixel area. Higher values create more stars. |
allStarsTwinkle | boolean | true | If true, all stars will twinkle. If false, only a portion of stars will twinkle based on twinkleProbability. |
twinkleProbability | number | 0.7 | The probability (0-1) that a star will twinkle when allStarsTwinkle is false. |
minTwinkleSpeed | number | 0.5 | The minimum duration (in seconds) for a star's twinkle animation. |
maxTwinkleSpeed | number | 1 | The maximum duration (in seconds) for a star's twinkle animation. |
className | string | undefined | Additional CSS classes to apply to the container div. |
This component is created by Vijay Verma for figmaplug.in. You can checkout the original code here