From f9719ba617c7f45732b3820ecf2a5fb22d4adfeb Mon Sep 17 00:00:00 2001 From: Jacob Edley Date: Tue, 9 Dec 2025 20:10:46 -0600 Subject: [PATCH 1/5] add shooting stars --- app/challenge/landing-page/page.tsx | 4 + app/landing/About.tsx | 4 + app/landing/HackVoyagers.tsx | 4 + app/landing/Hero.tsx | 4 + .../ShootingStars/ShootingStars.module.scss | 28 ++++++ components/ShootingStars/ShootingStars.tsx | 89 ++++++++++++++++++ package-lock.json | 19 ++++ package.json | 1 + public/assets/shooting_star.lottie | Bin 0 -> 9217 bytes 9 files changed, 153 insertions(+) create mode 100644 components/ShootingStars/ShootingStars.module.scss create mode 100644 components/ShootingStars/ShootingStars.tsx create mode 100644 public/assets/shooting_star.lottie diff --git a/app/challenge/landing-page/page.tsx b/app/challenge/landing-page/page.tsx index 327ffb5a..907b5f48 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 { ShootingStars } from "@/components/ShootingStars/ShootingStars"; const ProChallenge: React.FC = () => { const { offsetY, ref } = useParallaxScrollY(); @@ -120,6 +121,9 @@ const ProChallenge: React.FC = () => { }} /> + {/* Shooting Stars */} + + {/* OVERLAY LAYER */} {/* pill and text */} diff --git a/app/landing/About.tsx b/app/landing/About.tsx index 1990f7a1..90f4a8d0 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 { ShootingStars } from "@/components/ShootingStars/ShootingStars"; const About = () => { const { offsetY, ref } = useParallaxScrollY(); @@ -100,6 +101,9 @@ const About = () => { /> + {/* Shooting Stars */} + + {/* 3. Convert container to motion.div and apply container variants */} { const { offsetY, ref } = useParallaxScrollY(); @@ -82,6 +83,9 @@ const HackVoyagers = () => { /> + {/* Shooting Stars */} + + {/* 3. Wrap Robot container with motion div */} { /> + {/* Shooting Stars */} + + {/* 3. Apply the container variants to the main content wrapper */} { + const [shootingStars, setShootingStars] = useState([]); + + useEffect(() => { + const spawnShootingStar = () => { + const id = Date.now(); + + const maxX = 100 - (size / window.innerWidth) * 100; + const maxY = 100 - (size / window.innerHeight) * 100; + + const x = Math.random() * Math.max(0, maxX - 10) + 5; // 5% to (100 - size)% of width + const y = Math.random() * Math.max(0, maxY - 10) + 5; // 5% to (100 - size)% of height + const rotation = Math.random() * -25; // 0 to -25 degrees + + const newStar: ShootingStar = { id, x, y, rotation }; + setShootingStars(prev => [...prev, newStar]); + + // Remove after animation completes + setTimeout(() => { + setShootingStars(prev => prev.filter(star => star.id !== id)); + }, duration); + }; + + // Spawn a shooting star at random intervals + const scheduleNext = () => { + const delay = Math.random() * (maxDelay - minDelay) + minDelay; + return setTimeout(() => { + spawnShootingStar(); + scheduleNext(); + }, delay); + }; + + const timeoutId = scheduleNext(); + + return () => clearTimeout(timeoutId); + }, [minDelay, maxDelay, duration, size]); + + return ( +
+ {shootingStars.map(star => ( +
+ +
+ ))} +
+ ); +}; diff --git a/package-lock.json b/package-lock.json index 40a3dda4..028dfa60 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", @@ -988,6 +989,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 73a44a1f..ed007488 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 0000000000000000000000000000000000000000..f6f407d94aa297eb6782693ac68abdbe1f62641a GIT binary patch literal 9217 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 literal 0 HcmV?d00001 From 30e8bba7f8fba7f832b1d65e016cebb0537a68c9 Mon Sep 17 00:00:00 2001 From: Jacob Edley Date: Fri, 12 Dec 2025 14:02:46 -0600 Subject: [PATCH 2/5] optimize shooting star effect --- app/challenge/landing-page/page.tsx | 2 +- components/ShootingStars/ShootingStars.tsx | 120 ++++++++++++++++----- public/assets/shooting_star.lottie | Bin 9217 -> 1933 bytes 3 files changed, 93 insertions(+), 29 deletions(-) diff --git a/app/challenge/landing-page/page.tsx b/app/challenge/landing-page/page.tsx index 7ac52f9a..10b592b8 100644 --- a/app/challenge/landing-page/page.tsx +++ b/app/challenge/landing-page/page.tsx @@ -122,7 +122,7 @@ const ProChallenge: React.FC = () => { /> {/* Shooting Stars */} - + {/* OVERLAY LAYER */} diff --git a/components/ShootingStars/ShootingStars.tsx b/components/ShootingStars/ShootingStars.tsx index 33bc6e70..1f1eb65b 100644 --- a/components/ShootingStars/ShootingStars.tsx +++ b/components/ShootingStars/ShootingStars.tsx @@ -1,7 +1,7 @@ "use client"; import { DotLottieReact } from "@lottiefiles/dotlottie-react"; -import { useState, useEffect } from "react"; +import { useState, useEffect, useRef } from "react"; import styles from "./ShootingStars.module.scss"; interface ShootingStar { @@ -22,32 +22,95 @@ interface ShootingStarsProps { size?: number; } +// Cache the lottie data at module level to avoid re-fetching +let cachedLottieData: ArrayBuffer | null = null; +let fetchPromise: Promise | 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 ShootingStars = ({ - minDelay = 1000, - maxDelay = 6000, + minDelay = 2000, + maxDelay = 4000, duration = 2000, - size = 500 + size = 600 }: ShootingStarsProps) => { const [shootingStars, setShootingStars] = useState([]); + const [lottieData, setLottieData] = useState(null); + const containerRef = useRef(null); + + // Preload lottie data once + useEffect(() => { + fetchLottieData().then(setLottieData); + }, []); useEffect(() => { const spawnShootingStar = () => { const id = Date.now(); - const maxX = 100 - (size / window.innerWidth) * 100; - const maxY = 100 - (size / window.innerHeight) * 100; + // 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; + + // 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 = (size / containerHeight) * 100; + const starWidthPercent = (size / 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 x = Math.random() * Math.max(0, maxX - 10) + 5; // 5% to (100 - size)% of width - const y = Math.random() * Math.max(0, maxY - 10) + 5; // 5% to (100 - size)% of height const rotation = Math.random() * -25; // 0 to -25 degrees const newStar: ShootingStar = { id, x, y, rotation }; setShootingStars(prev => [...prev, newStar]); // Remove after animation completes + const actualDuration = duration + (Math.random() - 0.5) * 500; setTimeout(() => { setShootingStars(prev => prev.filter(star => star.id !== id)); - }, duration); + }, actualDuration); }; // Spawn a shooting star at random intervals @@ -65,25 +128,26 @@ export const ShootingStars = ({ }, [minDelay, maxDelay, duration, size]); return ( -
- {shootingStars.map(star => ( -
- -
- ))} +
+ {lottieData && + shootingStars.map(star => ( +
+ +
+ ))}
); }; diff --git a/public/assets/shooting_star.lottie b/public/assets/shooting_star.lottie index f6f407d94aa297eb6782693ac68abdbe1f62641a..3ef8f9bc462760bc854c6536999bf20275f21de8 100644 GIT binary patch delta 1679 zcmV;A25|X-NR1B-P)h>@6aWAK008x?i;)eM4)v>xTW#rmdoKn60F5M(u^WF`%TnV; z6#W&K*0QSK5ATv8i&Uj*7FoJjI7S2qV^g*>lPQXS&$%s2YDsN_fe;vG#z=NQZr}Iq zK7E&c$tIaDY@xF}yWC_G&RCu;ZnFtqYrG`={)%=%U#_x=r}ydx(>|?lZbuyRE-o&@ z_?vP!yO`}}+2rm%&t}_gx!Zqclj&WyK!3(Cayk1}ZmPC=(qe@-&$HdPY@+s*f4`dD zmZRTj@JM3XmKe*kYs|dMz|>uaH8A%Yl*Zb)e5|B82d#gyiJ+ysLs-tD&OoH?B!Tf4 z4Zvd20Ag|9vF>zKnyQJSAw6l^Wv1yNHbD@-w(q+NwhCIG@)-6Qahp+edk==TZ5nDa5L|WVK zptX6=19JS2=o~RK0wI6yYCD#oiJ!~A!s})UUvAdikv)6N8Rc@#W#REpI**@9y4UmI zj;B-}6m0rLBUA>jU2e}yG+an)P5UH4kNK2vp2we|OFe_vSdXU%?Rl6Vr$^ErSi|o> z5VDLo?W8twB(;q`e?dB&&sGoQxUTu%cXr#%2YIXJ)qqzM^V)w?gJZdSgeXnmT~dnd z1v|9W(N|aMNV3Wtl}#VAIb3dEBfzAQrlJzn3u&c~94H2s~`w@3vi zlr|z61S?R&P)CgB4zv_XWR~XN@W6F5L zyzo-w!=_iOZ7F|uKcTk84xcPt2^)B+a%sZWD-uH~t9AQ4u!Sg`bDYCY_4A;bjbJ@r5O^BE<6_|q?&ZX;2zmg#+l#^6;F&v z)XYtJ`D=Y{qys2+2Xy`n@8B8mW#Y6xj-GO8q%-4tJ~DqEQ)r%*myOQ|C@E;?Y_q-{ zK}8qquh7lS3Qa$q{rt)!1f@(-`z+vN|>&xA$ z+-{H2BOHIGOHa1+pvx6ZzHcB)gAA{>pPi&>4j(#5rynI@rjZoVK)l`DUNvd8(nzz! z9VNia*>YP3qfD{k<60RbnbTgPmTX{)^E++x?3E2|2w6l>&vUDtV@fD4e~71h$B1Gayiok&p)z zt2liO!SRqASs3P&u~t%C3uS1HBt@AI+i0LJ6tFDDwf9CkX?);vxKu)H3$%N<4hE*Q zHq3uBkADs`(g|}zU`3#^Wk?;yZ0HO z*w%9^1mjXdHQ|8XZ6e(j%0rvn3#~M=w^hL)L#maPA4hbYU`iF9&|c{@^9jO8x{<8kc`m-`jAW7PZmd7{QxQ|Df6h9dvVSZJkE} zflE1$lu#+6*>ENiKVt7X(p!YN2RI=)l8J(q`%S@h5Q17_O(;$&7c83hNmeF)9L4c~ zACy?e87>JNRatLar|C@(xgkZ;U{8W7f?$90u%rg000000QIYjlYJvN4)v>x ZTW#rmdoKn60F5M*!y`Bb`UU_100087H&g%s delta 9029 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 From d7c4e5c6ba117da16004cb2727e7f07e041a920e Mon Sep 17 00:00:00 2001 From: Jacob Edley Date: Fri, 12 Dec 2025 14:28:08 -0600 Subject: [PATCH 3/5] mobile responsiveness --- components/ShootingStars/ShootingStars.tsx | 23 ++++++++++++++++++---- package-lock.json | 2 +- package.json | 2 +- 3 files changed, 21 insertions(+), 6 deletions(-) diff --git a/components/ShootingStars/ShootingStars.tsx b/components/ShootingStars/ShootingStars.tsx index 1f1eb65b..a7ef4343 100644 --- a/components/ShootingStars/ShootingStars.tsx +++ b/components/ShootingStars/ShootingStars.tsx @@ -9,6 +9,7 @@ interface ShootingStar { x: number; y: number; rotation: number; + size: number; } interface ShootingStarsProps { @@ -69,6 +70,9 @@ export const ShootingStars = ({ const containerHeight = container.offsetHeight; const containerWidth = container.offsetWidth; + // Scale down star size on smaller screens (max 70% of container width) + 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( @@ -81,8 +85,8 @@ export const ShootingStars = ({ if (visibleHeight <= 0) return; // Calculate spawn position as percentage of container - const starHeightPercent = (size / containerHeight) * 100; - const starWidthPercent = (size / containerWidth) * 100; + const starHeightPercent = (effectiveSize / containerHeight) * 100; + const starWidthPercent = (effectiveSize / containerWidth) * 100; // X position: across the full width (with padding) const maxX = 100 - starWidthPercent; @@ -103,7 +107,13 @@ export const ShootingStars = ({ const rotation = Math.random() * -25; // 0 to -25 degrees - const newStar: ShootingStar = { id, x, y, rotation }; + const newStar: ShootingStar = { + id, + x, + y, + rotation, + size: effectiveSize + }; setShootingStars(prev => [...prev, newStar]); // Remove after animation completes @@ -140,11 +150,16 @@ export const ShootingStars = ({ transform: `rotate(${star.rotation}deg)` }} > + {/* Size is adjusted to account for large padding in the asset */}
))} diff --git a/package-lock.json b/package-lock.json index 0a5d57d0..12f5dbd6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "dependencies": { "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.1", - "@lottiefiles/dotlottie-react": "0.17.10", + "@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/package.json b/package.json index 1ab4333d..bd1ef2a3 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "dependencies": { "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.1", - "@lottiefiles/dotlottie-react": "0.17.10", + "@lottiefiles/dotlottie-react": "^0.17.10", "@mui/icons-material": "^7.3.4", "@mui/material": "^7.3.4", "@mui/material-nextjs": "^7.3.5", From 143e39eedd86031a8d32e8d0d26fae92013a9478 Mon Sep 17 00:00:00 2001 From: Jacob Edley Date: Fri, 12 Dec 2025 16:29:49 -0600 Subject: [PATCH 4/5] stars go both ways --- components/ShootingStars/ShootingStars.tsx | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/components/ShootingStars/ShootingStars.tsx b/components/ShootingStars/ShootingStars.tsx index a7ef4343..d4a3e81e 100644 --- a/components/ShootingStars/ShootingStars.tsx +++ b/components/ShootingStars/ShootingStars.tsx @@ -10,6 +10,7 @@ interface ShootingStar { y: number; rotation: number; size: number; + flipped: boolean; } interface ShootingStarsProps { @@ -106,13 +107,15 @@ export const ShootingStars = ({ 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 + size: effectiveSize, + flipped }; setShootingStars(prev => [...prev, newStar]); @@ -147,7 +150,7 @@ export const ShootingStars = ({ style={{ left: `${star.x}%`, top: `${star.y}%`, - transform: `rotate(${star.rotation}deg)` + transform: `rotate(${star.rotation}deg) scaleX(${star.flipped ? -1 : 1})` }} > {/* Size is adjusted to account for large padding in the asset */} @@ -156,7 +159,9 @@ export const ShootingStars = ({ loop={false} autoplay style={{ - transform: `translate(-${star.size}px, -${star.size / 2}px)`, + transform: star.flipped + ? `translateY(-${star.size / 2}px)` + : `translate(-${star.size}px, -${star.size / 2}px)`, width: `${star.size * 2}px`, height: `${star.size * 2}px` }} From 589dd3a180cb127e9cfc86da77a11c375dde9ae4 Mon Sep 17 00:00:00 2001 From: Jacob Edley Date: Sat, 13 Dec 2025 16:07:20 -0600 Subject: [PATCH 5/5] optimize shooting stars and add more --- app/challenge/landing-page/page.tsx | 6 +- app/landing/About.tsx | 6 +- app/landing/HackVoyagers.tsx | 6 +- app/landing/Hero.tsx | 6 +- ...s.module.scss => ShootingStar.module.scss} | 0 components/ShootingStars/ShootingStar.tsx | 157 ++++++++++++++++ components/ShootingStars/ShootingStars.tsx | 173 ------------------ 7 files changed, 173 insertions(+), 181 deletions(-) rename components/ShootingStars/{ShootingStars.module.scss => ShootingStar.module.scss} (100%) create mode 100644 components/ShootingStars/ShootingStar.tsx delete mode 100644 components/ShootingStars/ShootingStars.tsx diff --git a/app/challenge/landing-page/page.tsx b/app/challenge/landing-page/page.tsx index 10b592b8..67778102 100644 --- a/app/challenge/landing-page/page.tsx +++ b/app/challenge/landing-page/page.tsx @@ -5,7 +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 { ShootingStars } from "@/components/ShootingStars/ShootingStars"; +import { ShootingStar } from "@/components/ShootingStars/ShootingStar"; const ProChallenge: React.FC = () => { const { offsetY, ref } = useParallaxScrollY(); @@ -122,7 +122,9 @@ const ProChallenge: React.FC = () => { /> {/* Shooting Stars */} - + + + {/* OVERLAY LAYER */} diff --git a/app/landing/About.tsx b/app/landing/About.tsx index def0776b..110782dd 100644 --- a/app/landing/About.tsx +++ b/app/landing/About.tsx @@ -6,7 +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 { ShootingStars } from "@/components/ShootingStars/ShootingStars"; +import { ShootingStar } from "@/components/ShootingStars/ShootingStar"; const About = () => { const { offsetY, ref } = useParallaxScrollY(); @@ -101,7 +101,9 @@ const About = () => { {/* Shooting Stars */} - + + + {/* 3. Convert container to motion.div and apply container variants */} { const { offsetY, ref } = useParallaxScrollY(); @@ -84,7 +84,9 @@ 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/components/ShootingStars/ShootingStars.tsx b/components/ShootingStars/ShootingStars.tsx deleted file mode 100644 index d4a3e81e..00000000 --- a/components/ShootingStars/ShootingStars.tsx +++ /dev/null @@ -1,173 +0,0 @@ -"use client"; - -import { DotLottieReact } from "@lottiefiles/dotlottie-react"; -import { useState, useEffect, useRef } from "react"; -import styles from "./ShootingStars.module.scss"; - -interface ShootingStar { - id: number; - x: number; - y: number; - rotation: number; - size: number; - flipped: boolean; -} - -interface ShootingStarsProps { - /** Minimum delay between shooting stars in milliseconds (default: 1000) */ - minDelay?: number; - /** Maximum delay between shooting stars in milliseconds (default: 6000) */ - maxDelay?: number; - /** Duration of each shooting star animation in milliseconds (default: 2000) */ - duration?: number; - /** Width of each shooting star in pixels (default: 500) */ - size?: number; -} - -// Cache the lottie data at module level to avoid re-fetching -let cachedLottieData: ArrayBuffer | null = null; -let fetchPromise: Promise | 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 ShootingStars = ({ - minDelay = 2000, - maxDelay = 4000, - duration = 2000, - size = 600 -}: ShootingStarsProps) => { - const [shootingStars, setShootingStars] = useState([]); - const [lottieData, setLottieData] = useState(null); - const containerRef = useRef(null); - - // Preload lottie data once - useEffect(() => { - fetchLottieData().then(setLottieData); - }, []); - - useEffect(() => { - 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 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 - }; - setShootingStars(prev => [...prev, newStar]); - - // Remove after animation completes - const actualDuration = duration + (Math.random() - 0.5) * 500; - setTimeout(() => { - setShootingStars(prev => prev.filter(star => star.id !== id)); - }, actualDuration); - }; - - // Spawn a shooting star at random intervals - const scheduleNext = () => { - const delay = Math.random() * (maxDelay - minDelay) + minDelay; - return setTimeout(() => { - spawnShootingStar(); - scheduleNext(); - }, delay); - }; - - const timeoutId = scheduleNext(); - - return () => clearTimeout(timeoutId); - }, [minDelay, maxDelay, duration, size]); - - return ( -
- {lottieData && - shootingStars.map(star => ( -
- {/* Size is adjusted to account for large padding in the asset */} - -
- ))} -
- ); -};