From 9f31ee3bd5e1526ac23c0d68d2a248d1f86ebd42 Mon Sep 17 00:00:00 2001 From: Kelly Xu Date: Sun, 11 Jan 2026 15:32:54 -0500 Subject: [PATCH] this might be better? --- package-lock.json | 23 +++++-- src/sections/IntroSection.css | 11 ++- src/sections/IntroSection.jsx | 123 ++++++++++++++++++++++------------ 3 files changed, 104 insertions(+), 53 deletions(-) diff --git a/package-lock.json b/package-lock.json index a06ef04..4aaecdb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,10 +8,10 @@ "name": "frontend", "version": "0.0.0", "dependencies": { - "framer-motion": "^12.23.24", "@tailwindcss/vite": "^4.1.17", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "framer-motion": "^12.23.24", "lucide-react": "^0.555.0", "react": "^19.1.1", "react-dom": "^19.1.1", @@ -62,6 +62,7 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -1597,6 +1598,7 @@ "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -1638,6 +1640,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1743,6 +1746,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.19", "caniuse-lite": "^1.0.30001751", @@ -2006,6 +2010,7 @@ "integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -2267,9 +2272,9 @@ "license": "ISC" }, "node_modules/framer-motion": { - "version": "12.23.24", - "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.23.24.tgz", - "integrity": "sha512-HMi5HRoRCTou+3fb3h9oTLyJGBxHfW+HnNE25tAXOvVx/IvwMHK0cx7IR4a2ZU6sh3IX1Z+4ts32PcYBOqka8w==", + "version": "12.23.25", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.23.25.tgz", + "integrity": "sha512-gUHGl2e4VG66jOcH0JHhuJQr6ZNwrET9g31ZG0xdXzT0CznP7fHX4P8Bcvuc4MiUB90ysNnWX2ukHRIggkl6hQ==", "license": "MIT", "dependencies": { "motion-dom": "^12.23.23", @@ -2443,9 +2448,9 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", "dev": true, "license": "MIT", "dependencies": { @@ -2987,6 +2992,7 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -3047,6 +3053,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -3249,6 +3256,7 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" + }, "node_modules/tw-animate-css": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/tw-animate-css/-/tw-animate-css-1.4.0.tgz", @@ -3318,6 +3326,7 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.12.tgz", "integrity": "sha512-ZWyE8YXEXqJrrSLvYgrRP7p62OziLW7xI5HYGWFzOvupfAlrLvURSzv/FyGyy0eidogEM3ujU+kUG1zuHgb6Ug==", "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", diff --git a/src/sections/IntroSection.css b/src/sections/IntroSection.css index cd352bc..f04074f 100644 --- a/src/sections/IntroSection.css +++ b/src/sections/IntroSection.css @@ -257,14 +257,21 @@ position: relative; transform: translateX(100%); transition: transform 0.4s ease-in, opacity 0.4s ease-in; - opacity: 0; + opacity: 1; pointer-events: none; } /* active state set upon mouse hover */ .tracks_label.active { transform: translateX(0); - transition: transform 0.4s ease-out, opacity 0.4s ease-out; + opacity: 1; + pointer-events: auto; +} + +/* peek state - partially slides out when no tracks are active */ +.tracks_label.peek { + transform: translateX(60%); + transition: transform 0.4s ease-out; opacity: 1; pointer-events: auto; } diff --git a/src/sections/IntroSection.jsx b/src/sections/IntroSection.jsx index 752691d..8284e30 100644 --- a/src/sections/IntroSection.jsx +++ b/src/sections/IntroSection.jsx @@ -4,9 +4,15 @@ import { useEffect, useRef, useState } from 'react'; export default function IntroSection() { const [canFall, setCanFall] = useState(false); const [activeTracks, setActiveTracks] = useState(new Set()); - const [animatingTracks, setAnimatingTracks] = useState(new Set()); + const [peekingTracks, setPeekingTracks] = useState(new Set()); + const [isIdle, setIsIdle] = useState(true); const canRef = useRef(null); const timeoutRef = useRef({}); + let TRACK_SLIDE_DELAY = 1000; //change this to change how fast track slides back in + + useEffect(() => { + setIsIdle(activeTracks.size === 0); + }, [activeTracks]); useEffect(() => { const handleScroll = () => { @@ -28,45 +34,18 @@ export default function IntroSection() { return () => window.removeEventListener('scroll', handleScroll); }, [canFall]); - useEffect(() => { - if (canFall) { - // Slide out tracks one at a time during the Can Falling animation - const slideOutTimeouts = []; - for (let i = 0; i < 4; i++) { - slideOutTimeouts.push( - setTimeout(() => { - setAnimatingTracks(prev => new Set(prev).add(i)); - }, i * 200) // 200ms delay between each track - ); - } - - // Slide them back in after the fall completes (1.25s) + a delay - const slideBackDelay = 1250 + 200; // After fall animation + buffer - const slideInTimeouts = []; - for (let i = 0; i < 4; i++) { - slideInTimeouts.push( - setTimeout(() => { - setAnimatingTracks(prev => { - const newSet = new Set(prev); - newSet.delete(i); - return newSet; - }); - }, slideBackDelay + i * 200) - ); - } - - return () => { - [...slideOutTimeouts, ...slideInTimeouts].forEach(timeout => clearTimeout(timeout)); - }; - } - }, [canFall]); - + // hovering over a track label causes it to be active (slide into view) const handleTrackMouseEnter = (trackIndex) => { - // Hovering over a track label causes it to be active (slide into view) - if (timeoutRef.current[trackIndex]) clearTimeout(timeoutRef.current[trackIndex]); - if (!activeTracks.has(trackIndex)){ - setActiveTracks(prev => new Set(prev).add(trackIndex)); - } + setPeekingTracks(new Set()); + setIsIdle(false); + + if (timeoutRef.current[trackIndex]) { + clearTimeout(timeoutRef.current[trackIndex]); + } + + if (!activeTracks.has(trackIndex)){ + setActiveTracks(prev => new Set(prev).add(trackIndex)); + } }; const handleTrackMouseLeave = (trackIndex) => { @@ -77,9 +56,65 @@ export default function IntroSection() { newSet.delete(trackIndex); return newSet; }); - }, 2000); + }, TRACK_SLIDE_DELAY); }; + // function that makes the tracks peek out a little one by one + const runPeekSequence = () => { + const timeouts = []; + const PEEK_DURATION = 600; + const STAGGER = 90; + + setPeekingTracks(new Set()); + + for (let i = 0; i < 4; i++) { + const start = i * STAGGER; + + timeouts.push( + setTimeout(() => { + setPeekingTracks(prev => new Set(prev).add(i)); + }, start) + ); + + timeouts.push( + setTimeout(() => { + setPeekingTracks(prev => { + const ns = new Set(prev); + ns.delete(i); + return ns; + }); + }, start + PEEK_DURATION) + ); + } + + return timeouts; +}; + + + //if user not hovering over any tracks, play peeking animation to draw their eye to it + useEffect(() => { + if (!isIdle) return; + + let peekTimeouts = []; + let intervalId; + + const startPeekLoop = () => { + peekTimeouts = runPeekSequence(); + }; + + // Run immediately + startPeekLoop(); + + // Then repeat every 3s + intervalId = setInterval(startPeekLoop, 3000); + + return () => { + clearInterval(intervalId); + peekTimeouts.forEach(clearTimeout); + setPeekingTracks(new Set()); + }; + }, [isIdle]); + return (
@@ -116,21 +151,21 @@ export default function IntroSection() {
{/* if this track label has already slid into view + user hovers over it, reset the timer of how long before label slides in*/} - handleTrackMouseEnter(0)} onMouseLeave={() => handleTrackMouseLeave(0)}/> + handleTrackMouseEnter(0)} onMouseLeave={() => handleTrackMouseLeave(0)}/> {/* slide the track label out if 1) user hovers over it or 2) we're showing users to click on the track icon */} handleTrackMouseEnter(0)} onMouseLeave={() => handleTrackMouseLeave(0)}/>
- handleTrackMouseEnter(1)} onMouseLeave={() => handleTrackMouseLeave(1)}/> + handleTrackMouseEnter(1)} onMouseLeave={() => handleTrackMouseLeave(1)}/> handleTrackMouseEnter(1)} onMouseLeave={() => handleTrackMouseLeave(1)}/>
- handleTrackMouseEnter(2)} onMouseLeave={() => handleTrackMouseLeave(2)}/> + handleTrackMouseEnter(2)} onMouseLeave={() => handleTrackMouseLeave(2)}/> handleTrackMouseEnter(2)} onMouseLeave={() => handleTrackMouseLeave(2)}/>
- handleTrackMouseEnter(3)} onMouseLeave={() => handleTrackMouseLeave(3)}/> + handleTrackMouseEnter(3)} onMouseLeave={() => handleTrackMouseLeave(3)}/> handleTrackMouseEnter(3)} onMouseLeave={() => handleTrackMouseLeave(3)}/>