import { gsap } from 'gsap'; import { ScrollTrigger } from 'gsap/ScrollTrigger'; import { createLogisticsBackground } from './logistics-scenes'; import './style.css'; gsap.registerPlugin(ScrollTrigger); const ANIMATION_DURATION = 1.2; const background = createLogisticsBackground(); document.body.prepend(background); const compactScreenMedia = window.matchMedia('(max-width: 640px)'); function updateSceneViewBoxes() { background.querySelectorAll('svg[data-mobile-viewbox]').forEach((svg) => { const mobileViewBox = svg.dataset.mobileViewbox; svg.setAttribute('viewBox', compactScreenMedia.matches && mobileViewBox ? mobileViewBox : '0 0 1600 900'); }); } updateSceneViewBoxes(); compactScreenMedia.addEventListener('change', updateSceneViewBoxes); window.addEventListener('resize', updateSceneViewBoxes, { passive: true }); const sceneTargets: Array<{ section: HTMLElement; scene: HTMLElement | null }> = []; [ { section: '.hero', scene: 'forklift' }, { section: '.truck-section', scene: 'truck' }, { section: '.services', scene: 'container' }, { section: '.milestone', scene: 'ship' }, { section: '.ship-section', scene: 'ship' }, { section: '.airplane-section', scene: 'airplane' }, { section: '.team', scene: 'airplane' }, { section: '.footer', scene: 'airplane' }, ].forEach(({ section, scene }) => { const sectionElement = document.querySelector(section); const sceneElement = scene ? background.querySelector(`[data-scene="${scene}"]`) : null; if (sectionElement) sceneTargets.push({ section: sectionElement, scene: sceneElement }); }); let activeScene = background.querySelector('.logistics-scene.is-active'); function syncBackgroundToScroll() { const viewportCenter = window.innerHeight * 0.5; let nextScene: HTMLElement | null = null; let nearestDistance = Number.POSITIVE_INFINITY; for (const { section, scene } of sceneTargets) { const bounds = section.getBoundingClientRect(); if (bounds.top <= viewportCenter && bounds.bottom >= viewportCenter) { nextScene = scene; nearestDistance = 0; break; } const distance = Math.min(Math.abs(bounds.top - viewportCenter), Math.abs(bounds.bottom - viewportCenter)); if (distance < nearestDistance) { nearestDistance = distance; nextScene = scene; } } if (nextScene === activeScene) return; activeScene?.classList.remove('is-active'); nextScene?.classList.add('is-active'); activeScene = nextScene; } ScrollTrigger.create({ trigger: document.documentElement, start: 'top top', end: 'bottom bottom', onUpdate: syncBackgroundToScroll, onRefresh: syncBackgroundToScroll, }); let sceneSyncRequested = false; window.addEventListener('scroll', () => { if (sceneSyncRequested) return; sceneSyncRequested = true; requestAnimationFrame(() => { sceneSyncRequested = false; syncBackgroundToScroll(); }); }, { passive: true }); window.addEventListener('resize', syncBackgroundToScroll, { passive: true }); syncBackgroundToScroll(); const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; const isCompactScreen = compactScreenMedia.matches; const motionScale = prefersReducedMotion ? 0.55 : isCompactScreen ? 0.72 : 1; gsap.timeline({ scrollTrigger: { trigger: '.hero', start: 'top top', end: 'bottom top', scrub: true, invalidateOnRefresh: true }, }) .fromTo('.forklift-drive', { x: -440 * motionScale }, { x: 0, duration: ANIMATION_DURATION, ease: 'none' }, 0) .fromTo('.forklift-lift', { y: 0 }, { y: -125 * motionScale, duration: ANIMATION_DURATION, ease: 'none' }, 0) .to('.forklift-wheel', { rotation: 1080 * motionScale, duration: ANIMATION_DURATION, transformOrigin: '50% 50%', ease: 'none' }, 0); gsap.timeline({ scrollTrigger: { trigger: '.truck-section', start: 'top 125%', end: 'bottom -25%', scrub: true }, }) .to('.truck-road', { x: -250 * motionScale, duration: ANIMATION_DURATION, ease: 'none' }, 0) .to('.truck-hills', { x: -360 * motionScale, duration: ANIMATION_DURATION, ease: 'none' }, 0) .to('.truck-wheel', { rotation: 1440 * motionScale, duration: ANIMATION_DURATION, transformOrigin: '50% 50%', ease: 'none' }, 0) .fromTo('.truck-body', { y: 5 * motionScale }, { y: -5 * motionScale, duration: ANIMATION_DURATION, ease: 'none' }, 0); gsap.timeline({ scrollTrigger: { trigger: '.milestone', endTrigger: '.ship-section', start: 'top 125%', end: 'bottom -25%', scrub: true }, }) .fromTo('.ship-wave-back', { x: 0 }, { x: -800 * motionScale, duration: ANIMATION_DURATION, ease: 'none' }, 0) .fromTo('.ship-wave-front', { x: 0 }, { x: -1200 * motionScale, duration: ANIMATION_DURATION, ease: 'none' }, 0) .fromTo('.ship-rock', { rotation: -1.2 * motionScale, transformOrigin: '50% 50%' }, { rotation: 1.2 * motionScale, duration: ANIMATION_DURATION, ease: 'none' }, 0); gsap.timeline({ scrollTrigger: { trigger: '.airplane-section', endTrigger: '.footer', start: 'top 125%', end: 'bottom -25%', scrub: true }, }) .to('.air-clouds', { x: -650 * motionScale, duration: ANIMATION_DURATION, ease: 'none' }, 0) .fromTo('.airplane-float', { y: 18 * motionScale, rotation: -1 * motionScale, transformOrigin: '50% 50%' }, { y: -18 * motionScale, rotation: 1 * motionScale, duration: ANIMATION_DURATION, ease: 'none' }, 0); gsap.timeline({ scrollTrigger: { trigger: '.services', start: 'top 125%', end: 'bottom -25%', scrub: true }, }) .fromTo('.port-trolley', { x: -160 * motionScale }, { x: 100 * motionScale, duration: ANIMATION_DURATION, ease: 'none' }, 0) .fromTo('.port-crane-load', { y: 0 }, { y: 190 * motionScale, duration: ANIMATION_DURATION, ease: 'none' }, 0); const heroText = document.querySelector('.hero-copy'); const truckWords = document.querySelectorAll('.word-line span'); const progressFill = document.querySelector('.progress-fill'); if (heroText) { gsap.from(heroText.querySelectorAll('span'), { y: 24 * motionScale, opacity: 0, stagger: 0.08, duration: ANIMATION_DURATION, ease: 'power2.out', delay: 0.15, }); } gsap.fromTo( '.lead', { opacity: 0, y: 24 * motionScale }, { opacity: 1, y: 0, duration: ANIMATION_DURATION, ease: 'power2.out', delay: 0.4 }, ); gsap.fromTo( truckWords, { opacity: 0, y: 54 * motionScale }, { opacity: 1, y: 0, duration: ANIMATION_DURATION, stagger: 0.15, ease: 'power3.out', scrollTrigger: { trigger: '.truck-section', start: 'top 72%', end: 'bottom 40%', scrub: false, once: true, }, }, ); if (progressFill) { gsap.to(progressFill, { height: '100%', duration: ANIMATION_DURATION, ease: 'none', scrollTrigger: { trigger: '.milestone', start: 'top 80%', end: 'bottom 20%', scrub: true, }, }); }