diff --git a/public/marina_cropped_v3.jpg b/public/marina_cropped_v3.jpg index 70e6d80..ccb90a5 100644 Binary files a/public/marina_cropped_v3.jpg and b/public/marina_cropped_v3.jpg differ diff --git a/src/app/page.tsx b/src/app/page.tsx index 5b68794..5a1e478 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -32,6 +32,16 @@ export default function Home() { const [contactTop, setContactTop] = useState(0); const [windowHeight, setWindowHeight] = useState(0); + // Orientation state management for smooth transitions + const [orientation, setOrientation] = useState<'portrait' | 'landscape'>('landscape'); + const [isTransitioning, setIsTransitioning] = useState(false); + const transitionTimeoutRef = useRef(null); + const animationProgressRef = useRef(0); + + // Refs for orientation to avoid stale closures in animation loops + const orientationRef = useRef<'portrait' | 'landscape'>('landscape'); + const isTransitioningRef = useRef(false); + // Animation values as refs to avoid re-renders const logoPositionRef = useRef('center'); const buttonRef = useRef(null); @@ -50,6 +60,7 @@ export default function Home() { const imageContainerRef = useRef(null); const isMobile = useMediaQuery("(max-width: 768px)"); + const isTablet = useMediaQuery("(min-width: 769px) and (max-width: 1024px)"); const isDesktop = useMediaQuery("(min-width: 1280px)"); // Manage form state @@ -93,13 +104,13 @@ export default function Home() { // Memoized logo dimensions based on screen size const logoWidth = useMemo(() => - isMobile ? 240 : isDesktop ? 316 : 280, - [isMobile, isDesktop] + isMobile ? 240 : isTablet ? 280 : isDesktop ? 316 : 280, + [isMobile, isTablet, isDesktop] ); const logoHeight = useMemo(() => - isMobile ? 115 : isDesktop ? 151 : 134, - [isMobile, isDesktop] + isMobile ? 115 : isTablet ? 134 : isDesktop ? 151 : 134, + [isMobile, isTablet, isDesktop] ); // Memoized animation constants @@ -110,6 +121,14 @@ export default function Home() { finalScale: 0.5, fadeThreshold: 5 }, + tablet: { + targetTopPosition: 10, // Same as mobile - position higher up + startYFactor: 0.3, // Start lower than center for smoother animation + logoSpeed: 0.35, // Slightly slower than desktop + finalScale: 0.5, // Same as mobile for consistency + fadeThreshold: 8, // Between mobile and desktop + maxScroll: 450 // Shorter than desktop + }, desktop: { targetTopPosition: 20, logoSpeed: 0.4, @@ -121,13 +140,25 @@ export default function Home() { useEffect(() => { setMounted(true); - // Set initial dimensions on mount + // Set initial dimensions and orientation on mount setWindowHeight(window.innerHeight); + const initialOrientation = window.innerHeight > window.innerWidth ? 'portrait' : 'landscape'; + setOrientation(initialOrientation); + orientationRef.current = initialOrientation; if (contactSectionRef.current) { setContactTop(contactSectionRef.current.offsetTop); } }, []); + // Sync refs with state to avoid stale closures + useEffect(() => { + orientationRef.current = orientation; + }, [orientation]); + + useEffect(() => { + isTransitioningRef.current = isTransitioning; + }, [isTransitioning]); + // Helper function to update element styles directly const updateElementStyle = (element: HTMLElement | null, styles: Partial) => { if (!element) return; @@ -228,6 +259,106 @@ export default function Home() { }; }; + const calculateTabletLogoStyles = ( + scrollY: number, + windowHeight: number, + logoHeight: number, + contactSection: HTMLElement | null + ): Partial => { + const { targetTopPosition, startYFactor, logoSpeed, finalScale, maxScroll } = animationConstants.tablet; + + // Use ref-based orientation to avoid stale closures + const isPortrait = orientationRef.current === 'portrait'; + + // Start position - adjust based on orientation for smoother animation + // Portrait mode starts lower for a longer, smoother animation path + const startY = windowHeight * (isPortrait ? 0.4 : startYFactor); + + // End position - adjust based on orientation to prevent cutoff + // Portrait needs more space from top due to taller viewport + const endY = isPortrait ? 30 : targetTopPosition; // 30px for portrait (higher up), 10px for landscape + + // Total distance to travel (simpler calculation like mobile) + const totalDistance = startY - endY; + + // Calculate Y position during animation + let currentY: number; + + if (contactSection) { + const contactTop = contactSection.offsetTop; + + // Adjust multiplier based on orientation + // Portrait mode - complete animation closer to the contact section + // This makes the logo become sticky lower on the page + const multiplier = isPortrait ? 1.0 : 0.7; // Use 100% for portrait (like mobile), 70% for landscape + + // Use dynamic contact position with orientation-aware multiplier + const animationEndScroll = contactTop * multiplier; + + if (scrollY < animationEndScroll) { + // During animation - use progress-based positioning like mobile + const progress = scrollY / animationEndScroll; + currentY = startY - (totalDistance * progress); + + // Calculate scale based on animation progress + const scale = 1 - ((1 - finalScale) * progress); + + // Update logo position state + if (scrollY > 10) { + logoPositionRef.current = 'animating'; + } else { + logoPositionRef.current = 'center'; + } + + return { + position: 'fixed', + top: `${currentY}px`, + left: '50%', + transform: `translate3d(-50%, 0, 0) scale3d(${scale}, ${scale}, 1) translateZ(0)`, + transformOrigin: 'center', + willChange: 'transform', + backfaceVisibility: 'hidden', + transition: isTransitioningRef.current ? 'all 0.3s ease-out' : 'none', + zIndex: '50' + }; + } else { + // After animation - implement scrollPastEnd logic like mobile + logoPositionRef.current = 'top'; + const scrollPastEnd = scrollY - animationEndScroll; + currentY = endY - scrollPastEnd; + + return { + position: 'fixed', + top: `${currentY}px`, + left: '50%', + transform: `translate3d(-50%, 0, 0) scale3d(${finalScale}, ${finalScale}, 1) translateZ(0)`, + transformOrigin: 'center', + willChange: scrollY > 0 ? 'transform' : 'auto', + backfaceVisibility: 'hidden', + transition: isTransitioningRef.current ? 'all 0.3s ease-out' : 'none', + zIndex: '50' + }; + } + } else { + // Fallback if contact section not found + const progress = Math.min(scrollY / 800, 1); // Use reasonable fallback + currentY = startY - (totalDistance * progress); + const scale = 1 - ((1 - finalScale) * progress); + + return { + position: 'fixed', + top: `${currentY}px`, + left: '50%', + transform: `translate3d(-50%, 0, 0) scale3d(${scale}, ${scale}, 1) translateZ(0)`, + transformOrigin: 'center', + willChange: scrollY > 0 ? 'transform' : 'auto', + backfaceVisibility: 'hidden', + transition: isTransitioning ? 'all 0.3s ease-out' : 'none', + zIndex: '50' + }; + } + }; + // Update button and chevron opacity const updateControlsOpacity = (scrollY: number, isMobile: boolean) => { const fadeThreshold = isMobile ? animationConstants.mobile.fadeThreshold : animationConstants.desktop.fadeThreshold; @@ -265,14 +396,19 @@ export default function Home() { } // Calculate and apply logo styles based on device type - const logoStyles = isMobile - ? calculateMobileLogoStyles(scrollY, currentWindowHeight, currentContactTop, logoHeight) - : calculateDesktopLogoStyles(scrollY, currentWindowHeight, logoHeight); + let logoStyles; + if (isMobile) { + logoStyles = calculateMobileLogoStyles(scrollY, currentWindowHeight, currentContactTop, logoHeight); + } else if (isTablet) { + logoStyles = calculateTabletLogoStyles(scrollY, currentWindowHeight, logoHeight, contactSectionRef.current); + } else { + logoStyles = calculateDesktopLogoStyles(scrollY, currentWindowHeight, logoHeight); + } updateElementStyle(logoRef.current, logoStyles); // Update controls opacity - updateControlsOpacity(scrollY, isMobile); + updateControlsOpacity(scrollY, isMobile || isTablet); }; // Animated scroll to form @@ -310,6 +446,33 @@ export default function Home() { }; const handleResize = () => { + const newOrientation = window.innerHeight > window.innerWidth ? 'portrait' : 'landscape'; + + // Only trigger transition if orientation actually changed and we're on tablet + if (newOrientation !== orientationRef.current && isTablet) { + // Store current animation progress + const scrollY = window.scrollY; + const contactTop = contactSectionRef.current?.offsetTop || 0; + animationProgressRef.current = Math.min(scrollY / contactTop, 1); + + // Enable transition mode + setIsTransitioning(true); + setOrientation(newOrientation); + + // Clear any existing timeout + if (transitionTimeoutRef.current) { + clearTimeout(transitionTimeoutRef.current); + } + + // Disable transition after animation completes + transitionTimeoutRef.current = setTimeout(() => { + setIsTransitioning(false); + }, 300); + } else if (newOrientation !== orientation) { + // Update orientation for non-tablet devices without transition + setOrientation(newOrientation); + } + setWindowHeight(window.innerHeight); if (contactSectionRef.current) { setContactTop(contactSectionRef.current.offsetTop); @@ -328,8 +491,11 @@ export default function Home() { if (animationFrameRef.current) { cancelAnimationFrame(animationFrameRef.current); } + if (transitionTimeoutRef.current) { + clearTimeout(transitionTimeoutRef.current); + } }; - }, [isMobile, isDesktop, logoHeight, windowHeight]); + }, [isMobile, isTablet, isDesktop, logoHeight, windowHeight]); // Dynamic height adjustment for image alignment useEffect(() => { @@ -658,7 +824,7 @@ export default function Home() { {/* Image positioned to align with heading */}