From cd067b8ce445c0ace9189aa329e689c7e6edbcfd Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 20 Nov 2025 18:43:09 +0000 Subject: [PATCH 1/5] Add sticky blob effect to navigation - follows cursor and snaps to nearest button --- src/routes/+layout.svelte | 75 ++++++++++++++++++++++++++++++++------- 1 file changed, 62 insertions(+), 13 deletions(-) diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 391f4b9..57a2bbc 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -21,26 +21,69 @@ // Navigation sliding background let navItemElements: (HTMLAnchorElement | null)[] = []; let activeIndicatorStyle = ''; + let isHoveringNav = false; + let hoveredIndex = -1; $: if (navItemElements.length === navItems.length && BROWSER) { updateActiveIndicator(); } function updateActiveIndicator() { - const activeIndex = navItems.findIndex(item => $page.url.pathname === item.href); - if (activeIndex !== -1 && navItemElements[activeIndex]) { - const activeElement = navItemElements[activeIndex]; - const navContainer = activeElement?.parentElement; - if (activeElement && navContainer) { + const targetIndex = isHoveringNav && hoveredIndex !== -1 ? hoveredIndex : navItems.findIndex(item => $page.url.pathname === item.href); + + if (targetIndex !== -1 && navItemElements[targetIndex]) { + const targetElement = navItemElements[targetIndex]; + const navContainer = targetElement?.parentElement; + if (targetElement && navContainer) { const containerRect = navContainer.getBoundingClientRect(); - const activeRect = activeElement.getBoundingClientRect(); - const left = activeRect.left - containerRect.left; - const width = activeRect.width; - activeIndicatorStyle = `left: ${left}px; width: ${width}px;`; + const targetRect = targetElement.getBoundingClientRect(); + const left = targetRect.left - containerRect.left; + const width = targetRect.width; + const transition = isHoveringNav ? 'all 0.2s cubic-bezier(0.34, 1.56, 0.64, 1)' : 'all 0.3s ease-out'; + activeIndicatorStyle = `left: ${left}px; width: ${width}px; transition: ${transition};`; } } } + function handleNavMouseMove(event: MouseEvent) { + if (!isHoveringNav) return; + + const navElement = event.currentTarget as HTMLElement; + const mouseX = event.clientX; + + // Find closest nav item + let closestIndex = -1; + let closestDistance = Infinity; + + navItemElements.forEach((element, index) => { + if (element) { + const rect = element.getBoundingClientRect(); + const centerX = rect.left + rect.width / 2; + const distance = Math.abs(mouseX - centerX); + + if (distance < closestDistance) { + closestDistance = distance; + closestIndex = index; + } + } + }); + + if (closestIndex !== -1 && closestIndex !== hoveredIndex) { + hoveredIndex = closestIndex; + updateActiveIndicator(); + } + } + + function handleNavMouseEnter() { + isHoveringNav = true; + } + + function handleNavMouseLeave() { + isHoveringNav = false; + hoveredIndex = -1; + updateActiveIndicator(); + } + // Reactive statement to check if win2000 theme is active $: isWin2000Theme = ($currentTheme === 'win2000'); @@ -208,25 +251,31 @@