/** * Home page animations using GSAP Timeline and Flip * Uses sessionStorage to ensure animation only plays once per session */ gsap.registerPlugin(Flip); /* -------------------------------------------------- CONFIG -------------------------------------------------- */ const HOMECONFIG = { selectors: { heroSection: '[r-home-hero-section]', heroCards: '[r-home-hero-card]', heroTitle: '[r-home-hero-title]', heroNr: '[r-home-hero-nr]', heroText: '[r-home-hero-text]' }, durations: { textElements: 0.8, flip: 1.5, fadeIn: 0.4, cardEntrance: 2, centerPause: 0.1 }, delays: { cardStagger: 0.05, heroNrDelay: 0.05, textStartOffset: 0.2 }, easings: { textElements: 'power4.out', flip: 'power4.inOut', fadeIn: 'power2.inOut' }, cssClasses: { disablePointer: 'page-loading' }, transforms: { textElementsY: '130%', textFadeY: '0.5rem' }, magneticHover: { hoverAreaNormal: 0.5, hoverAreaActive: 0.7, moveIntensity: 0.1, hoverDuration: 0.4, leaveDuration: 0.7, hoverEase: 'power2.out', leaveEase: 'power2.out', hoverZIndex: 10, defaultZIndex: 1 }, parallaxCursor: { moveIntensity: 0, duration: 0.8, ease: 'power2.out' } }; /* -------------------------------------------------- UTILS -------------------------------------------------- */ function debounce(func, wait) { let timeout; return (...args) => { clearTimeout(timeout); timeout = setTimeout(() => func(...args), wait); }; } function shouldRunAnimations() { return window.innerWidth >= 992; } /* -------------------------------------------------- MAGNETIC HOVER -------------------------------------------------- */ class HoverButton { constructor(el) { this.el = el; this.hover = false; this.calculatePosition(); this.attachEventsListener(); } attachEventsListener() { window.addEventListener('mousemove', e => this.onMouseMove(e)); window.addEventListener('resize', debounce(() => this.calculatePosition(), 250)); } calculatePosition() { gsap.set(this.el, { x: 0, y: 0, scale: 1 }); const box = this.el.getBoundingClientRect(); this.x = box.left + box.width * 0.5; this.y = box.top + box.height * 0.5; this.width = box.width; this.height = box.height; } onMouseMove(e) { let hoverArea = this.hover ? HOMECONFIG.magneticHover.hoverAreaActive : HOMECONFIG.magneticHover.hoverAreaNormal; let dx = e.clientX - this.x; let dy = e.clientY - this.y; let distance = Math.sqrt(dx * dx + dy * dy); if (distance < this.width * hoverArea) { if (!this.hover) this.hover = true; this.onHover(e.clientX, e.clientY); } else if (this.hover) { this.hover = false; this.onLeave(); } } onHover(x, y) { this.el.classList.add('magnetic-hover-active'); gsap.to(this.el, { x: (x - this.x) * HOMECONFIG.magneticHover.moveIntensity, y: (y - this.y) * HOMECONFIG.magneticHover.moveIntensity, duration: HOMECONFIG.magneticHover.hoverDuration, ease: HOMECONFIG.magneticHover.hoverEase, overwrite: true }); this.el.style.zIndex = HOMECONFIG.magneticHover.hoverZIndex; } onLeave() { this.el.classList.remove('magnetic-hover-active'); gsap.to(this.el, { x: 0, y: 0, duration: HOMECONFIG.magneticHover.leaveDuration, ease: HOMECONFIG.magneticHover.leaveEase, overwrite: true }); this.el.style.zIndex = HOMECONFIG.magneticHover.defaultZIndex; } } /* -------------------------------------------------- PARALLAX CURSOR -------------------------------------------------- */ class ParallaxCursor { constructor(heroSection, heroCards) { this.heroSection = heroSection; this.heroCards = heroCards; this.isTouch = 'ontouchstart' in window || navigator.maxTouchPoints > 0; if (!this.isTouch) this.init(); } init() { this.updateBounds(); this.heroSection.addEventListener('mousemove', e => this.onMouseMove(e)); this.heroSection.addEventListener('mouseleave', () => this.onMouseLeave()); window.addEventListener('resize', () => this.updateBounds()); } updateBounds() { this.bounds = this.heroSection.getBoundingClientRect(); } onMouseMove(e) { const x = (e.clientX - this.bounds.left - this.bounds.width / 2) / this.bounds.width; const y = (e.clientY - this.bounds.top - this.bounds.height / 2) / this.bounds.height; this.heroCards.forEach(card => { if (!card.classList.contains('magnetic-hover-active')) { gsap.to(card, { x: x * HOMECONFIG.parallaxCursor.moveIntensity, y: y * HOMECONFIG.parallaxCursor.moveIntensity, duration: HOMECONFIG.parallaxCursor.duration, ease: HOMECONFIG.parallaxCursor.ease, overwrite: 'auto' }); } }); } onMouseLeave() { this.heroCards.forEach(card => { if (!card.classList.contains('magnetic-hover-active')) { gsap.to(card, { x: 0, y: 0, duration: HOMECONFIG.parallaxCursor.duration, ease: HOMECONFIG.parallaxCursor.ease, overwrite: 'auto' }); } }); } } /* -------------------------------------------------- EFFECT INIT -------------------------------------------------- */ function initMagneticHover() { document .querySelectorAll(HOMECONFIG.selectors.heroCards) .forEach(card => new HoverButton(card)); } function initParallaxCursor() { const heroSection = document.querySelector(HOMECONFIG.selectors.heroSection); const heroCards = document.querySelectorAll(HOMECONFIG.selectors.heroCards); if (heroSection && heroCards.length) { new ParallaxCursor(heroSection, heroCards); } } /* -------------------------------------------------- MAIN PAGE ANIMATION -------------------------------------------------- */ function initPageLoadAnimation() { if (!shouldRunAnimations()) return; const heroCards = document.querySelectorAll(HOMECONFIG.selectors.heroCards); const heroTitle = document.querySelector(HOMECONFIG.selectors.heroTitle); const heroNr = document.querySelector(HOMECONFIG.selectors.heroNr); const heroText = document.querySelector(HOMECONFIG.selectors.heroText); if (!heroCards.length || !heroTitle || !heroNr || !heroText) return; const hasPlayed = sessionStorage.getItem('homeAnimationPlayed') === 'true'; /* -------------------------------------------------- NO ANIMATION → SHOW IMMEDIATELY -------------------------------------------------- */ if (hasPlayed) { gsap.set(heroCards, { opacity: 1, clearProps: 'transform' }); gsap.set([heroTitle, heroNr, heroText], { opacity: 1, clearProps: 'transform' }); initMagneticHover(); initParallaxCursor(); return; } /* -------------------------------------------------- ANIMATION FLOW -------------------------------------------------- */ document.body.classList.add(HOMECONFIG.cssClasses.disablePointer); /* ensure hidden BEFORE anything moves */ gsap.set(heroCards, { opacity: 1 }); /* 1 — CAPTURE FINAL RELATIVE LAYOUT */ const finalState = Flip.getState(heroCards); /* 2 — STACK CARDS BELOW VIEWPORT */ gsap.set(heroCards, { position: 'fixed', top: '150%', left: '50%', xPercent: -50, yPercent: -50 }); gsap.set([heroTitle, heroNr], { y: HOMECONFIG.transforms.textElementsY }); gsap.set(heroText, { opacity: 0, y: HOMECONFIG.transforms.textFadeY }); const tl = gsap.timeline(); /* 3 — MOVE TO CENTER */ tl.to(heroCards, { top: '50%', duration: HOMECONFIG.durations.cardEntrance, ease: HOMECONFIG.easings.flip, stagger: HOMECONFIG.delays.cardStagger }); /* 4 — PAUSE */ tl.to({}, { duration: HOMECONFIG.durations.centerPause }); /* 5 — FLIP INTO RELATIVE LAYOUT */ tl.add(() => { Flip.to(finalState, { duration: HOMECONFIG.durations.flip, ease: HOMECONFIG.easings.flip, stagger: HOMECONFIG.delays.cardStagger, absolute: false, onComplete: () => { gsap.set(heroCards, { opacity: 1, clearProps: 'position,top,left,transform' }); gsap.set([heroTitle, heroNr, heroText], { opacity: 1, clearProps: 'transform' }); document.body.classList.remove(HOMECONFIG.cssClasses.disablePointer); sessionStorage.setItem('homeAnimationPlayed', 'true'); initMagneticHover(); initParallaxCursor(); } }); }); /* TEXT OVERLAP */ const flipStart = HOMECONFIG.durations.cardEntrance + HOMECONFIG.durations.centerPause; const textStart = flipStart + HOMECONFIG.durations.flip - HOMECONFIG.delays.textStartOffset; tl.to(heroTitle, { y: 0, opacity: 1, duration: HOMECONFIG.durations.textElements, ease: HOMECONFIG.easings.textElements }, textStart); tl.to(heroNr, { opacity: 1, y: 0, duration: HOMECONFIG.durations.textElements, ease: HOMECONFIG.easings.textElements }, `${textStart}+=${HOMECONFIG.delays.heroNrDelay}`); tl.to(heroText, { opacity: 1, y: 0, duration: HOMECONFIG.durations.fadeIn, ease: HOMECONFIG.easings.fadeIn }, `${textStart}+=${HOMECONFIG.delays.heroNrDelay + HOMECONFIG.delays.textStartOffset}`); } /* -------------------------------------------------- BOOTSTRAP -------------------------------------------------- */ function initHome() { if (!window.gsap || !window.Flip) return; initPageLoadAnimation(); } document.readyState === 'loading' ? document.addEventListener('DOMContentLoaded', initHome) : initHome();