diff --git a/app/challenge/landing-page/page.tsx b/app/challenge/landing-page/page.tsx index 2c62223a..67778102 100644 --- a/app/challenge/landing-page/page.tsx +++ b/app/challenge/landing-page/page.tsx @@ -5,6 +5,7 @@ import styles from "./LandingPage.module.scss"; import { useParallaxScrollY } from "@/hooks/use-parallax-scrollY"; import clsx from "clsx"; import { motion, Variants } from "framer-motion"; +import { ShootingStar } from "@/components/ShootingStars/ShootingStar"; const ProChallenge: React.FC = () => { const { offsetY, ref } = useParallaxScrollY(); @@ -120,6 +121,11 @@ const ProChallenge: React.FC = () => { }} /> + {/* Shooting Stars */} + + + + {/* OVERLAY LAYER */} {/* pill and text */} diff --git a/app/landing/About.tsx b/app/landing/About.tsx index ec3108ae..110782dd 100644 --- a/app/landing/About.tsx +++ b/app/landing/About.tsx @@ -6,6 +6,7 @@ import Image from "next/image"; import styles from "./About.module.scss"; import { useParallaxScrollY } from "@/hooks/use-parallax-scrollY"; import { motion, Variants } from "framer-motion"; +import { ShootingStar } from "@/components/ShootingStars/ShootingStar"; const About = () => { const { offsetY, ref } = useParallaxScrollY(); @@ -99,6 +100,11 @@ const About = () => { /> + {/* Shooting Stars */} + + + + {/* 3. Convert container to motion.div and apply container variants */} { const { offsetY, ref } = useParallaxScrollY(); @@ -82,6 +83,11 @@ const HackVoyagers = () => { /> + {/* Shooting Stars */} + + + + {/* 3. Wrap Robot container with motion div */} { /> + {/* Shooting Stars */} + + + + {/* 3. Apply the container variants to the main content wrapper */} | null = null; + +const fetchLottieData = async (): Promise => { + if (cachedLottieData) { + return cachedLottieData; + } + if (fetchPromise) { + return fetchPromise; + } + fetchPromise = fetch("/assets/shooting_star.lottie") + .then(res => res.arrayBuffer()) + .then(data => { + cachedLottieData = data; + return data; + }); + return fetchPromise; +}; + +export const ShootingStar = ({ + delay = 2000, + jitter = 1600 +}: ShootingStarProps) => { + const [star, setStar] = useState(null); + const [lottieData, setLottieData] = useState(null); + const containerRef = useRef(null); + + const spawnShootingStar = () => { + const id = Date.now(); + + // Calculate the visible portion of the container + const container = containerRef.current; + if (!container) return; + + const rect = container.getBoundingClientRect(); + const containerHeight = container.offsetHeight; + const containerWidth = container.offsetWidth; + + // Scale down star size on smaller screens (max 70% of container width) + const size = 600; + const effectiveSize = Math.min(size, containerWidth * 0.7); + + // Calculate visible area within the container + const visibleTop = Math.max(0, -rect.top); + const visibleBottom = Math.min( + containerHeight, + window.innerHeight - rect.top + ); + const visibleHeight = visibleBottom - visibleTop; + + // Don't spawn if container isn't visible + if (visibleHeight <= 0) return; + + // Calculate spawn position as percentage of container + const starHeightPercent = (effectiveSize / containerHeight) * 100; + const starWidthPercent = (effectiveSize / containerWidth) * 100; + + // X position: across the full width (with padding) + const maxX = 100 - starWidthPercent; + const x = Math.random() * Math.max(0, maxX - 10) + 5; + + // Y position: only within the visible area + const visibleTopPercent = (visibleTop / containerHeight) * 100; + const visibleHeightPercent = (visibleHeight / containerHeight) * 100; + const maxYInVisible = visibleHeightPercent - starHeightPercent; + + if (maxYInVisible <= 0) return; + + const y = + visibleTopPercent + + Math.random() * Math.max(0, maxYInVisible - 5) + + 2.5; + + const rotation = Math.random() * -25; // 0 to -25 degrees + const flipped = Math.random() < 0.5; // 50% chance to flip + + const newStar: ShootingStar = { + id, + x, + y, + rotation, + size: effectiveSize, + flipped + }; + setStar(newStar); + }; + + // Preload lottie data once + useEffect(() => { + fetchLottieData().then(setLottieData); + }, []); + + useEffect(() => { + // Spawn a shooting star at random intervals + const intervalId = setInterval(() => { + const variance = Math.random() * jitter; + setTimeout(() => { + spawnShootingStar(); + }, variance); + }, delay); + + return () => clearInterval(intervalId); + }, [delay, jitter]); + + return ( +
+ {lottieData && star && ( +
+ {/* Size is adjusted to account for large padding in the asset */} + +
+ )} +
+ ); +}; diff --git a/package-lock.json b/package-lock.json index 22e6f75c..12f5dbd6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.1", + "@lottiefiles/dotlottie-react": "^0.17.10", "@mui/icons-material": "^7.3.4", "@mui/material": "^7.3.4", "@mui/material-nextjs": "^7.3.5", @@ -957,6 +958,24 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@lottiefiles/dotlottie-react": { + "version": "0.17.10", + "resolved": "https://registry.npmjs.org/@lottiefiles/dotlottie-react/-/dotlottie-react-0.17.10.tgz", + "integrity": "sha512-ikrN05/q0/KjqIU+n48uNwmE7DeZIC9y3Nd19httcKqe273zoOeNYycEaQzLSdcpEGnWLmHaZpgtoo07aQZAXg==", + "license": "MIT", + "dependencies": { + "@lottiefiles/dotlottie-web": "0.58.1" + }, + "peerDependencies": { + "react": "^17 || ^18 || ^19" + } + }, + "node_modules/@lottiefiles/dotlottie-web": { + "version": "0.58.1", + "resolved": "https://registry.npmjs.org/@lottiefiles/dotlottie-web/-/dotlottie-web-0.58.1.tgz", + "integrity": "sha512-YC4pmScrV0R3rd11gU5xHrjeNczlCic69zlnMH/buDIzYxIbpR88oPUhGtKgu5ln7EJchoLpeRJbA3uLCzSeTA==", + "license": "MIT" + }, "node_modules/@mui/core-downloads-tracker": { "version": "7.3.6", "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-7.3.6.tgz", diff --git a/package.json b/package.json index 82d09136..bd1ef2a3 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "dependencies": { "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.1", + "@lottiefiles/dotlottie-react": "^0.17.10", "@mui/icons-material": "^7.3.4", "@mui/material": "^7.3.4", "@mui/material-nextjs": "^7.3.5", diff --git a/public/assets/shooting_star.lottie b/public/assets/shooting_star.lottie new file mode 100644 index 00000000..3ef8f9bc Binary files /dev/null and b/public/assets/shooting_star.lottie differ