/** * 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(); } // Initialize when DOM is ready if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", initHome); } else { initHome(); } })();