1/** 2 * Force Scroll to Top on Page Load/Reload 3 * Assicura che ogni reload parta dall'inizio della pagina 4 * Previene problemi con animazioni ScrollTrigger e storico scroll 5 */ 6(function() { 7 'use strict'; 8 9 // Funzione helper per controllare se c'è un anchor nell'URL 10 function hasAnchorInUrl() { 11 return window.location.hash && window.location.hash.length > 1; 12 } 13 14 function normalizeHash(hash) { 15 if (!hash) return ''; 16 return hash.charAt(0) === '#' ? hash : ('#' + hash); 17 } 18 19 function isElementActuallyVisible(el) { 20 if (!el || !(el instanceof Element)) return false; 21 const style = window.getComputedStyle(el); 22 if (style.display === 'none' || style.visibility === 'hidden') return false; 23 // offsetParent è null anche per position:fixed in alcuni casi, quindi aggiungiamo un check su rect 24 const rect = el.getBoundingClientRect(); 25 if ((rect.width === 0 && rect.height === 0) && el.offsetParent === null) return false; 26 // Evita anchor dentro wrapper responsive nascosti 27 const mobileWrapper = el.closest('.responsive-template-mobile'); 28 if (mobileWrapper && window.getComputedStyle(mobileWrapper).display === 'none') return false; 29 const desktopWrapper = el.closest('.responsive-template-desktop'); 30 if (desktopWrapper && window.getComputedStyle(desktopWrapper).display === 'none') return false; 31 return true; 32 } 33 34 /** 35 * Quando usiamo Lenis + inizializzazioni GSAP/ScrollTrigger, 36 * il jump nativo verso l'anchor può non avvenire (o essere annullato). 37 * Qui forziamo lo scroll all'anchor dopo il load, con retry. 38 */ 39 function scrollToInitialAnchor() { 40 if (!hasAnchorInUrl()) return; 41 42 // Se lo scroll è ancora bloccato dal loader, non ha senso tentare. 43 if (document.body && document.body.classList.contains('loading')) { 44 return; 45 } 46 47 const hash = normalizeHash(window.location.hash); 48 if (!hash || hash === '#') return; 49 50 // decode per gestire id con caratteri speciali 51 let selector = hash; 52 try { 53 selector = decodeURIComponent(hash); 54 } catch (e) {} 55 56 // Serve essere "tolleranti": su alcune pagine i blocchi arrivano tardi (Gutenberg, immagini, init JS). 57 const maxAttempts = 80; // ~8s con delay 100ms 58 const attemptDelayMs = 100; 59 let attempt = 0; 60 61 const tryScroll = () => { 62 attempt += 1; 63 64 let el = null; 65 try { 66 // Se ci fossero più occorrenze, preferiamo la prima visibile 67 const all = document.querySelectorAll(selector); 68 if (all && all.length) { 69 el = Array.from(all).find(isElementActuallyVisible) || all[0]; 70 } 71 } catch (e) { 72 // selector non valido (es. hash con caratteri non escapati) 73 el = document.getElementById(selector.replace(/^#/, '')); 74 } 75 76 if (!el || !isElementActuallyVisible(el)) { 77 if (attempt < maxAttempts) { 78 setTimeout(tryScroll, attemptDelayMs); 79 } 80 return; 81 } 82 83 // 1) Prima fai il jump nativo (è il più affidabile per hash deep-link). 84 // 2) Poi (se c'è Lenis) riallinea la posizione con un secondo pass. 85 el.scrollIntoView({ behavior: 'auto', block: 'start' }); 86 87 const lenisScroll = () => { 88 if (window.lenis && typeof window.lenis.scrollTo === 'function') { 89 window.lenis.scrollTo(el, { immediate: true, force: true }); 90 } 91 }; 92 93 requestAnimationFrame(lenisScroll); 94 setTimeout(lenisScroll, 50); 95 }; 96 97 tryScroll(); 98 } 99 100 // Espone la funzione: serve al page loader che sblocca lo scroll dopo la sua animazione. 101 window.iuadScrollToInitialAnchor = scrollToInitialAnchor; 102 103 // Observer: se l'anchor arriva dopo (blocchi inseriti tardi / template responsive), 104 // proviamo non appena compare nel DOM. 105 let anchorObserver = null;
106 function ensureAnchorObserver() { 107 if (!hasAnchorInUrl()) return; 108 if (anchorObserver) return; 109 if (typeof MutationObserver === 'undefined') return; 110 111 anchorObserver = new MutationObserver(() => { 112 // appena lo scroll è libero e l'elemento esiste, scrolla e poi spegne l'observer 113 if (document.body && document.body.classList.contains('loading')) return; 114 scrollToInitialAnchor(); 115 116 // Se dopo lo scroll l'elemento è visibile, possiamo fermarci 117 const hash = normalizeHash(window.location.hash); 118 if (!hash || hash === '#') return; 119 try { 120 const candidates = document.querySelectorAll(decodeURIComponent(hash)); 121 const visible = candidates && candidates.length ? Array.from(candidates).find(isElementActuallyVisible) : null; 122 if (visible) { 123 anchorObserver.disconnect(); 124 anchorObserver = null; 125 } 126 } catch (e) {} 127 }); 128 129 anchorObserver.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); 130 } 131 132 // Se l'utente inizia a interagire (scroll/wheel/touch/keys) prima del "load", 133 // non forziamo più lo scroll-top a fine caricamento. 134 var userHasInteractedWithScroll = false; 135 function markUserInteraction() { 136 userHasInteractedWithScroll = true; 137 } 138 window.addEventListener('scroll', markUserInteraction, { passive: true }); 139 window.addEventListener('wheel', markUserInteraction, { passive: true }); 140 window.addEventListener('touchmove', markUserInteraction, { passive: true }); 141 window.addEventListener('keydown', function(e) { 142 // tasti tipici di scroll 143 var keys = ['ArrowDown', 'ArrowUp', 'PageDown', 'PageUp', 'Home', 'End', ' ']; 144 if (keys.indexOf(e.key) !== -1) { 145 markUserInteraction(); 146 } 147 }, { passive: true }); 148 149 // 1. Disabilita completamente il comportamento nativo di scroll restoration 150 // MA solo se non c'è un anchor nell'URL 151 if ('scrollRestoration' in history && !hasAnchorInUrl()) { 152 history.scrollRestoration = 'manual'; 153 } 154 155 // 2. Forza scroll a 0 PRIMA che il DOM sia renderizzato 156 // MA solo se non c'è un anchor nell'URL (per permettere lo scroll automatico all'anchor) 157 if (!hasAnchorInUrl()) { 158 window.scrollTo(0, 0); 159 document.documentElement.scrollTop = 0; 160 document.body.scrollTop = 0; 161 } 162 163 // 3. Assicura scroll a 0 anche dopo il caricamento completo 164 // MA solo se non c'è un anchor nell'URL 165 window.addEventListener('load', function() { 166 if (hasAnchorInUrl()) { 167 // Se l'utente ha già iniziato a scrollare, non interveniamo. 168 if (!userHasInteractedWithScroll) { 169 scrollToInitialAnchor(); 170 // Secondo pass: utile quando plugin/scroll management ricalcolano dopo il load. 171 setTimeout(() => { 172 if (!userHasInteractedWithScroll) { 173 scrollToInitialAnchor(); 174 } 175 }, 500); 176 } 177 ensureAnchorObserver(); 178 return; 179 } 180 181 if (!hasAnchorInUrl() && !userHasInteractedWithScroll) { 182 // Usa Lenis se disponibile, altrimenti scroll nativo 183 if (window.lenis) { 184 window.lenis.scrollTo(0, { immediate: true, force: true }); 185 } else { 186 window.scrollTo({ top: 0, left: 0, behavior: 'instant' }); 187 document.documentElement.scrollTop = 0; 188 document.body.scrollTop = 0; 189 } 190 } 191 }); 192 193 // Se l'hash cambia (o viene "riapplicato" dal browser), riallineiamo lo scroll. 194 window.addEventListener('hashchange', function() { 195 if (!userHasInteractedWithScroll) { 196 scrollToInitialAnchor(); 197 } 198 ensureAnchorObserver(); 199 }); 200 201 // 4. Previeni il salvataggio della posizione prima dell'unload 202 window.addEventListener('beforeunload', function() { 203 if (window.lenis) { 204 window.lenis.scrollTo(0, { immediate: true }); 205 } else { 206 window.scrollTo(0, 0); 207 } 208 }); 209 210 // 5. Gestisci anche la navigazione con pulsante back/forward 211 window.addEventListener('pageshow', function(event) {
212 // Se la pagina è stata caricata dalla cache (bfcache) 213 if (event.persisted) { 214 // Non forzare lo scroll a 0 se c'è un anchor nell'URL 215 if (!hasAnchorInUrl()) { 216 if (window.lenis) { 217 window.lenis.scrollTo(0, { immediate: true, force: true }); 218 } else { 219 window.scrollTo({ top: 0, left: 0, behavior: 'instant' }); 220 } 221 } 222 223 // Refresh ScrollTrigger se presente 224 if (typeof ScrollTrigger !== 'undefined') { 225 ScrollTrigger.refresh(); 226 } 227 } 228 }); 229 230})(); 231 232 233/** 234 * Lenis Smooth Scroll Initialization 235 * Integrates Lenis with GSAP ScrollTrigger 236 */ 237 238(function() { 239 'use strict'; 240 241 /** 242 * Esclude dalla smooth-scroll Lenis zone con scroll interno (es. webchat Botpress). 243 * Senza questo, la rotella tende a muovere la pagina invece della lista messaggi. 244 * Cfr. Lenis: opzione prevent(node) e data-lenis-prevent-wheel. 245 */ 246 function iuadLenisPreventWheelOnNode(node) { 247 if (!(node instanceof HTMLElement)) { 248 return false; 249 } 250 if (node.hasAttribute('data-lenis-prevent') || node.hasAttribute('data-lenis-prevent-wheel')) { 251 return true; 252 } 253 var selectors = [ 254 '[data-botpress-webchat]', 255 '[id*="botpress"]', 256 '[class*="bpWebchat"]', 257 '[class*="bpw-"]', 258 '[class*="bpMessageList"]' 259 ].join(', '); 260 if (node.matches && node.matches(selectors)) { 261 return true; 262 } 263 if (typeof node.closest === 'function' && node.closest(selectors)) { 264 return true; 265 } 266 if (node.tagName === 'IFRAME' && node.src) { 267 try { 268 var src = node.src; 269 if (/botpress|bpcontent\.cloud/i.test(src)) { 270 return true; 271 } 272 } catch (e) {} 273 } 274 return false; 275 } 276 277 // Initialize a new Lenis instance for smooth scrolling 278 const lenis = new Lenis({ 279 duration: 1.2, 280 easing: (t) => Math.min(1, 1.001 - Math.pow(2, -10 * t)), 281 orientation: 'vertical', 282 gestureOrientation: 'vertical', 283 smoothWheel: true, 284 wheelMultiplier: 1, 285 smoothTouch: false, 286 touchMultiplier: 2, 287 infinite: false, 288 allowNestedScroll: true, 289 prevent: iuadLenisPreventWheelOnNode, 290 }); 291 292 // Synchronize Lenis scrolling with GSAP's ScrollTrigger plugin 293 lenis.on('scroll', ScrollTrigger.update); 294 295 // Add Lenis's requestAnimationFrame (raf) method to GSAP's ticker 296 // This ensures Lenis's smooth scroll animation updates on each GSAP tick 297 gsap.ticker.add((time) => { 298 lenis.raf(time * 1000); // Convert time from seconds to milliseconds 299 }); 300 301 // Disable lag smoothing in GSAP to prevent any delay in scroll animations 302 gsap.ticker.lagSmoothing(0); 303 304 // Optional: Stop/Start smooth scroll based on user preference 305 // Uncomment if you want to respect users' motion preferences 306 /* 307 const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)'); 308 if (prefersReducedMotion.matches) { 309 lenis.stop(); 310 } 311 */ 312 313 // Expose lenis globally for potential use in other scripts 314 window.lenis = lenis; 315 316})(); 317 318/** 319 * Scroll lock when portfolio lightboxes are open 320 * - Image/video lightbox adds: body.portfolio-lightbox-open 321 * - PDF lightbox adds: body.portfolio-pdf-lightbox-open 322 * 323 * We lock both html/body (and stop Lenis) to avoid background scroll on all browsers. 324 */ 325(function() { 326 'use strict'; 327 328 function isLightboxOpen() { 329 return document.body.classList.contains('portfolio-lightbox-open') 330 || document.body.classList.contains('portfolio-pdf-lightbox-open'); 331 } 332 333 function lockScroll() { 334 if (document.body.dataset.iuadScrollLock === '1') return; 335 document.body.dataset.iuadScrollLock = '1'; 336 337 const scrollY = window.scrollY || window.pageYOffset || 0; 338 document.body.dataset.iuadScrollLockY = String(scrollY); 339 340 // preserve previous inline styles (only inline, CSS stays intact) 341 document.documentElement.dataset.iuadPrevOverflow = document.documentElement.style.overflow || ''; 342 document.body.dataset.iuadPrevOverflow = document.body.style.overflow || ''; 343 document.body.dataset.iuadPrevPosition = document.body.style.position || ''; 344 document.body.dataset.iuadPrevTop = document.body.style.top || ''; 345 document.body.dataset.iuadPrevWidth = document.body.style.width || ''; 346 347 document.documentElement.style.overflow = 'hidden'; 348 document.body.style.overflow = 'hidden'; 349 document.body.style.position = 'fixed'; 350 document.body.style.top = (-scrollY) + 'px'; 351 document.body.style.width = '100%'; 352 353 if (window.lenis && typeof window.lenis.stop === 'function') { 354 window.lenis.stop(); 355 } 356 } 357 358 function unlockScroll() { 359 if (document.body.dataset.iuadScrollLock !== '1') return; 360 document.body.dataset.iuadScrollLock = '0'; 361 362 const scrollY = parseInt(document.body.dataset.iuadScrollLockY || '0', 10) || 0; 363 364 document.documentElement.style.overflow = document.documentElement.dataset.iuadPrevOverflow || ''; 365 document.body.style.overflow = document.body.dataset.iuadPrevOverflow || ''; 366 document.body.style.position = document.body.dataset.iuadPrevPosition || ''; 367 document.body.style.top = document.body.dataset.iuadPrevTop || ''; 368 document.body.style.width = document.body.dataset.iuadPrevWidth || ''; 369 370 // restore scroll position 371 window.scrollTo(0, scrollY); 372 373 if (window.lenis && typeof window.lenis.start === 'function') { 374 window.lenis.start(); 375 } 376 } 377 378 function sync() { 379 if (isLightboxOpen()) lockScroll(); 380 else unlockScroll(); 381 } 382 383 // Initial sync + observe class changes 384 if (document.readyState === 'loading') { 385 document.addEventListener('DOMContentLoaded', sync); 386 } else { 387 sync(); 388 } 389 390 const observer = new MutationObserver(sync); 391 observer.observe(document.body, { attributes: true, attributeFilter: ['class'] }); 392})(); 393 394/** 395 * SplitText Animation for Percent Blocks
396 * Animates text from bottom when block enters viewport 397 */ 398(function() { 399 'use strict'; 400 401 // Register SplitText plugin 402 gsap.registerPlugin(SplitText); 403 404 // Wait for fonts to be ready 405 /* 406 document.fonts.ready.then(() => { 407 // Find all percent blocks 408 const percentBlocks = document.querySelectorAll('.percent-block'); 409 410 if (percentBlocks.length === 0) return; 411 412 percentBlocks.forEach((block) => { 413 // Find title and text elements within each block 414 const title = block.querySelector('.percent-block-title'); 415 const text = block.querySelector('.percent-block-text'); 416 417 // Create array of elements to animate 418 const elements = []; 419 if (title) elements.push(title); 420 if (text) elements.push(text); 421 422 if (elements.length === 0) return; 423 424 // Set initial opacity to 1 (will be controlled by animation) 425 gsap.set(elements, { opacity: 1 }); 426 427 // Apply SplitText to each element 428 elements.forEach((element) => { 429 const split = new SplitText(element, { 430 type: "words,lines", 431 linesClass: "line", 432 }); 433 434 // Create the animation timeline 435 const tl = gsap.timeline({ 436 scrollTrigger: { 437 trigger: block, 438 start: "top 80%", // Start when block is 80% from top of viewport 439 end: "top 20%", 440 toggleActions: "play none none none", // Play once when entering 441 // markers: true, // Uncomment for debugging 442 } 443 }); 444 445 // Animate lines from bottom 446 tl.from(split.lines, { 447 duration: 1, 448 yPercent: 100, 449 opacity: 0, 450 stagger: 0.15, 451 ease: "power3.out", 452 }); 453 }); 454 }); 455 }); 456 */ 457 458})(); 459 460/** 461 * Percent Block Circles Animation 462 * Slide animation with opposite directions when entering viewport 463 */ 464(function() { 465 'use strict'; 466 467 // Wait for DOM to be ready 468 document.addEventListener('DOMContentLoaded', () => { 469 // Find all percent block containers 470 const containers = document.querySelectorAll('.percent-block-container-circle'); 471 472 if (containers.length === 0) return; 473 474 containers.forEach((container) => { 475 const blockOne = container.querySelector('.percent-block-one'); 476 const blockTwo = container.querySelector('.percent-block-two'); 477 478 if (!blockOne || !blockTwo) return; 479 480 // Set initial state 481 gsap.set(blockOne, { 482 x: 0, 483 zIndex: 1 484 }); 485 486 gsap.set(blockTwo, { 487 x: 0, 488 zIndex: 2, 489 scale: 1 490 }); 491 492 // Create the animation timeline 493 const tl = gsap.timeline({ 494 scrollTrigger: { 495 trigger: container, 496 start: "center bottom", // Start when 50% of container is visible in viewport 497 end: "bottom top", 498 toggleActions: "play none none none", 499 // markers: true, // Uncomment for debugging 500 } 501 }); 502 503 // Animate both blocks 504 tl.to(blockOne, { 505 x: -250, // Slide left 506 duration: 1.2, 507 ease: "power3.out" 508 }, 0) // Start at time 0 509 .to(blockTwo, { 510 x: 250, // Slide right 511 width: "+=100px", // Increase width by 20px 512 height: "+=100px", // Increase height by 20px to keep proportion 513 duration: 1.2, 514 ease: "power3.out" 515 }, 0); // Start at time 0 (same time as blockOne) 516 }); 517 }); 518 519})(); 520 521 522/** 523 * Vertical Card Carousel with Stack Effect 524 * Carousel verticale con effetto mazzo di carte
525 */ 526(function() { 527 'use strict'; 528 529 document.addEventListener('DOMContentLoaded', () => { 530 const carousel = document.querySelector('.vertical-card-carousel'); 531 532 if (!carousel) return; 533 534 const cards = carousel.querySelectorAll('.carousel-card'); 535 const dots = carousel.querySelectorAll('.carousel-dot'); 536 537 if (cards.length === 0 || dots.length === 0) return; 538 539 let currentIndex = 0; 540 let previousIndex = -1; 541 let isAnimating = false; 542 543 // Funzione per calcolare l'indice della slide precedente 544 function getPreviousSlideIndex(activeIndex) { 545 return activeIndex > 0 ? activeIndex - 1 : -1; 546 } 547 548 // Funzione per animare il cambio di slide 549 function goToSlide(index) { 550 if (isAnimating || index === currentIndex || index < 0 || index >= cards.length) { 551 return; 552 } 553 554 isAnimating = true; 555 556 const currentCard = cards[currentIndex]; 557 const nextCard = cards[index]; 558 const currentDot = dots[currentIndex]; 559 const nextDot = dots[index]; 560 561 // Calcola nuova slide precedente 562 const newPreviousIndex = getPreviousSlideIndex(index); 563 const newPreviousCard = newPreviousIndex >= 0 ? cards[newPreviousIndex] : null; 564 565 // Determina se siamo su mobile (stesso breakpoint dello swipe) 566 const isMobile = window.innerWidth <= 1024; 567 568 // Timeline GSAP per l'animazione 569 const tl = gsap.timeline({ 570 onComplete: () => { 571 isAnimating = false; 572 currentIndex = index; 573 previousIndex = newPreviousIndex; 574 } 575 }); 576 577 // Determina la direzione (avanti o indietro) 578 const isGoingForward = index > currentIndex; 579 580 if (isMobile) { 581 // ANIMAZIONE ORIZZONTALE SU MOBILE CON EFFETTO SOVRAPPOSIZIONE 582 // La card previous rimane a x: 0 (stessa posizione della card attiva) 583 if (isGoingForward) { 584 // NAVIGAZIONE IN AVANTI (swipe left) 585 586 // La vecchia slide previous scompare al centro senza movimento (se esiste e non è quella che diventerà attiva) 587 if (previousIndex >= 0 && previousIndex !== index) { 588 const oldPreviousCard = cards[previousIndex]; 589 tl.to(oldPreviousCard, { 590 x: 0, 591 y: 0, 592 opacity: 0, 593 duration: 0.4, 594 ease: "power2.in", 595 onComplete: () => { 596 oldPreviousCard.classList.remove('previous'); 597 } 598 }, 0); 599 } 600 601 // La card corrente diventa previous (rimane a x: 0, solo cambia classe) 602 tl.to(currentCard, { 603 x: 0, 604 y: 0, 605 opacity: 1, 606 duration: 0.5, 607 ease: "power2.out", 608 onStart: () => { 609 currentCard.classList.remove('active'); 610 currentCard.classList.add('previous'); 611 } 612 }, 0); 613 614 // La nuova card attiva entra da destra con bounce 615 tl.fromTo(nextCard, 616 { 617 x: 100, 618 y: 0, 619 opacity: 0 620 }, 621 { 622 x: 0, 623 y: 0, 624 opacity: 1, 625 duration: 0.8, 626 ease: "back.out(1.7)", 627 onStart: () => { 628 nextCard.classList.remove('previous'); 629 nextCard.classList.add('active'); 630 startCardMaskIntro(nextCard); 631 } 632 }, 0.2); 633 634 } else { 635 // NAVIGAZIONE ALL'INDIETRO (swipe right) 636 637 // La card corrente scompare completamente a destra 638 tl.to(currentCard, { 639 x: 100, 640 y: 0, 641 opacity: 0, 642 duration: 0.5, 643 ease: "power2.in", 644 onComplete: () => { 645 currentCard.classList.remove('active'); 646 gsap.set(currentCard, { x: 100, y: 0, opacity: 0 }); 647 } 648 }, 0); 649 650 // Assicura che la nextCard sia nella posizione previous (x: 0) prima di animarla 651 // Se non ha già la classe previous, imposta la posizione iniziale 652 if (!nextCard.classList.contains('previous')) { 653 gsap.set(nextCard, { x: 0, y: 0, opacity: 1 }); 654 nextCard.classList.add('previous'); 655 } 656 657 // La vecchia previous (che è la nuova attiva) rimane a x: 0, solo cambia classe 658 tl.to(nextCard, { 659 x: 0, 660 y: 0, 661 opacity: 1, 662 duration: 0.8, 663 ease: "back.out(1.7)", 664 onStart: () => { 665 nextCard.classList.remove('previous'); 666 nextCard.classList.add('active'); 667 startCardMaskIntro(nextCard); 668 } 669 }, 0.2); 670 671 // La vecchia "old previous" scompare al centro senza movimento se non è quella che diventa la nuova previous 672 if (previousIndex >= 0 && previousIndex !== newPreviousIndex && previousIndex !== index) { 673 const oldPreviousCard = cards[previousIndex]; 674 tl.to(oldPreviousCard, { 675 x: 0, 676 y: 0, 677 opacity: 0, 678 duration: 0.4, 679 ease: "power2.in", 680 onComplete: () => { 681 oldPreviousCard.classList.remove('previous'); 682 } 683 }, 0); 684 } 685 } 686 687 // Mostra la nuova slide previous (se esiste e non è già visibile) - solo su mobile 688 if (newPreviousCard && newPreviousIndex !== previousIndex && newPreviousIndex !== currentIndex) { 689 tl.fromTo(newPreviousCard, 690 { 691 x: 100, 692 y: 0, 693 opacity: 0 694 }, 695 { 696 x: 0, 697 y: 0, 698 opacity: 1, 699 duration: 0.6, 700 ease: "power2.out", 701 onStart: () => { 702 newPreviousCard.classList.add('previous'); 703 } 704 }, 0.1); 705 } 706 } else { 707 // ANIMAZIONE VERTICALE SU DESKTOP 708 if (isGoingForward) { 709 // NAVIGAZIONE IN AVANTI 710 711 // La vecchia slide previous scompare (se esiste e non è quella che diventerà attiva) 712 if (previousIndex >= 0 && previousIndex !== index) { 713 const oldPreviousCard = cards[previousIndex]; 714 tl.to(oldPreviousCard, { 715 y: 100, 716 opacity: 0, 717 duration: 0.4, 718 ease: "power2.in", 719 onComplete: () => { 720 oldPreviousCard.classList.remove('previous'); 721 } 722 }, 0); 723 } 724 725 // La card corrente diventa previous (scivola
725sotto) 726 tl.to(currentCard, { 727 y: 30, 728 opacity: 1, 729 duration: 0.5, 730 ease: "power2.out", 731 onStart: () => { 732 currentCard.classList.remove('active'); 733 currentCard.classList.add('previous'); 734 } 735 }, 0); 736 737 // La nuova card attiva appare dal basso con bounce 738 tl.fromTo(nextCard, 739 { 740 y: 100, 741 opacity: 0 742 }, 743 { 744 y: 0, 745 opacity: 1, 746 duration: 0.8, 747 ease: "back.out(1.7)", 748 onStart: () => { 749 nextCard.classList.remove('previous'); 750 nextCard.classList.add('active'); 751 startCardMaskIntro(nextCard); 752 } 753 }, 0.2); 754 755 } else { 756 // NAVIGAZIONE ALL'INDIETRO 757 758 // La card corrente scompare verso il basso 759 tl.to(currentCard, { 760 y: 100, 761 opacity: 0, 762 duration: 0.5, 763 ease: "power2.in", 764 onComplete: () => { 765 currentCard.classList.remove('active'); 766 gsap.set(currentCard, { y: 100, opacity: 0 }); 767 } 768 }, 0); 769 770 // La vecchia previous (che è la nuova attiva) sale con bounce 771 tl.to(nextCard, { 772 y: 0, 773 opacity: 1, 774 duration: 0.8, 775 ease: "back.out(1.7)", 776 onStart: () => { 777 nextCard.classList.remove('previous'); 778 nextCard.classList.add('active'); 779 startCardMaskIntro(nextCard); 780 } 781 }, 0.2); 782 783 // La vecchia "old previous" scompare se non è quella che diventa la nuova previous 784 if (previousIndex >= 0 && previousIndex !== newPreviousIndex && previousIndex !== index) { 785 const oldPreviousCard = cards[previousIndex]; 786 tl.to(oldPreviousCard, { 787 y: 100, 788 opacity: 0, 789 duration: 0.4, 790 ease: "power2.in", 791 onComplete: () => { 792 oldPreviousCard.classList.remove('previous'); 793 } 794 }, 0); 795 } 796 } 797 798 // Mostra la nuova slide previous (solo su desktop) 799 if (newPreviousCard && newPreviousIndex !== previousIndex && newPreviousIndex !== currentIndex) { 800 tl.fromTo(newPreviousCard, 801 { 802 y: 100, 803 opacity: 0 804 }, 805 { 806 y: 30, 807 opacity: 1, 808 duration: 0.6, 809 ease: "power2.out", 810 onStart: () => { 811 newPreviousCard.classList.add('previous'); 812 } 813 }, 0.1); 814 } 815 } 816 817 // Aggiorna i dots 818 currentDot.classList.remove('active'); 819 nextDot.classList.add('active'); 820 821 // Animazione del dot che perde l'active (torna alla scala originale) 822 /* 823 gsap.to(currentDot, { 824 scale: 1, 825 duration: 0.3, 826 ease: "power2.out" 827 }); 828 829 // Animazione del dot attivo (scala leggermente più grande) 830 gsap.fromTo(nextDot, 831 { scale: 1 }, 832 { 833 scale: 0.8, 834 duration: 0.3, 835 ease: "back.out(2)" 836 } 837 ); 838 */ 839 } 840 841 // Event listener per i dots
842 dots.forEach((dot, index) => { 843 dot.addEventListener('click', () => { 844 goToSlide(index); 845 }); 846 }); 847 848 // Swipe su mobile: abilita next/prev con gesture orizzontale 849 if (window.innerWidth <= 1024) { 850 let touchStartX = 0; 851 let touchCurrentX = 0; 852 const swipeThreshold = 50; // px 853 854 const onTouchStart = (e) => { 855 const touch = e.touches[0]; 856 touchStartX = touch.clientX; 857 touchCurrentX = touchStartX; 858 }; 859 860 const onTouchMove = (e) => { 861 const touch = e.touches[0]; 862 touchCurrentX = touch.clientX; 863 }; 864 865 const onTouchEnd = () => { 866 const deltaX = touchCurrentX - touchStartX; 867 if (Math.abs(deltaX) < swipeThreshold) return; 868 869 if (deltaX < 0) { 870 // swipe left -> next 871 const nextIndex = (currentIndex + 1) % cards.length; 872 goToSlide(nextIndex); 873 } else { 874 // swipe right -> prev 875 const prevIndex = (currentIndex - 1 + cards.length) % cards.length; 876 goToSlide(prevIndex); 877 } 878 }; 879 880 carousel.addEventListener('touchstart', onTouchStart, { passive: true }); 881 carousel.addEventListener('touchmove', onTouchMove, { passive: true }); 882 carousel.addEventListener('touchend', onTouchEnd); 883 } 884 885 // Navigazione da tastiera (frecce su/giù) 886 document.addEventListener('keydown', (e) => { 887 if (e.key === 'ArrowDown') { 888 e.preventDefault(); 889 const nextIndex = (currentIndex + 1) % cards.length; 890 goToSlide(nextIndex); 891 } else if (e.key === 'ArrowUp') { 892 e.preventDefault(); 893 const prevIndex = (currentIndex - 1 + cards.length) % cards.length; 894 goToSlide(prevIndex); 895 } 896 }); 897 898 // Navigazione con scroll del mouse (solo desktop) 899 if (window.innerWidth > 1024) { 900 const wrapper = carousel.querySelector('.carousel-wrapper') || carousel; 901 let wheelTimeout = null; 902 903 const onWheel = (e) => { 904 const deltaY = e.deltaY || 0; 905 if (deltaY === 0) { 906 return; 907 } 908 909 // Calcola quanto del wrapper è effettivamente visibile in viewport 910 const rect = wrapper.getBoundingClientRect(); 911 const viewportHeight = window.innerHeight || document.documentElement.clientHeight; 912 const visibleTop = Math.max(rect.top, 0); 913 const visibleBottom = Math.min(rect.bottom, viewportHeight); 914 const visibleHeight = Math.max(0, visibleBottom - visibleTop); 915 const visibilityRatio = rect.height > 0 ? visibleHeight / rect.height : 0; 916 917 // Se il wrapper non è almeno al 60% visibile, non intercettiamo lo scroll 918 if (visibilityRatio < 0.85) { 919 return; 920 } 921 922 // Determina l'indice di destinazione SENZA loop 923 let targetIndex = currentIndex; 924 if (deltaY > 0 && currentIndex < cards.length - 1) { 925 // Scroll verso il basso -> slide successiva (se esiste) 926 targetIndex = currentIndex + 1; 927 } else if (deltaY < 0 && currentIndex > 0) { 928 // Scroll verso l'alto -> slide precedente (se esiste) 929 targetIndex = currentIndex - 1; 930 } 931 932 // Se non cambierebbe slide, lascia scorrere la pagina normalmente 933 if (targetIndex === currentIndex) { 934 return; 935 } 936 937 // A questo punto intercettiamo lo scroll per controllare il carousel 938 e.preventDefault(); 939 e.stopPropagation(); 940 941 // Semplice throttling: evita troppi cambi slide in una singola "raffica" 942 if (wheelTimeout) { 943 return; 944 } 945 wheelTimeout = setTimeout(() => { 946 wheelTimeout = null; 947 }, 150); 948 949 goToSlide(targetIndex); 950 }; 951 952 wrapper.addEventListener('wheel', onWheel, { passive: false }); 953 } 954 955 // Inizializza lo stato iniziale (senza avviare la maschera sulla prima slide) 956 const isMobileInit = window.innerWidth <= 1024; 957 cards.forEach((card, index) => { 958 if (index === 0) { 959 if (isMobileInit) { 960 // Su mobile: solo x, y deve essere 0 961 gsap.set(card, { x: 0, y: 0, opacity: 1 }); 962 } else { 963 // Su desktop: solo y, x deve essere 0 964 gsap.set(card, { x: 0, y: 0, opacity: 1 }); 965 } 966 } else { 967 if (isMobileInit) { 968 // Su mobile: solo x, y deve essere 0 969 gsap.set(card, { x: 100, y: 0, opacity: 0 }); 970 } else { 971 // Su desktop: solo y, x deve essere 0 972 gsap.set(card, { x: 0, y: 100, opacity: 0 }); 973 } 974 } 975 }); 976 977 // Funzione per avviare l'animazione sulla card attiva 978 const triggerMaskAnimation = () => { 979 const activeCard = carousel.querySelector('.carousel-card.active'); 980 if (activeCard) { 981 // Verifica che la card abbia effettivamente la maschera SVG 982 const hasMask = activeCard.querySelector('.card-image .masked-svg'); 983 if (hasMask) { 984 //console.log('Trovata maschera su slide attiva, avvio animazione'); 985 986 // PRIMA: ferma TUTTE le animazioni su TUTTE le card 987 const allCards = carousel.querySelectorAll('.carousel-card'); 988 allCards.forEach(card => { 989 const cardSvg = card.querySelector('.card-image .masked-svg'); 990 if (cardSvg) { 991 const cardBlobs = cardSvg.querySelectorAll('.blob');
992 cardBlobs.forEach(blob => { 993 gsap.killTweensOf(blob); 994 gsap.set(blob, { clearProps: "all" }); 995 }); 996 } 997 }); 998 999 // POI: avvia solo sulla card attiva 1000 setTimeout(() => { 1001 startCardMaskIntro(activeCard); 1002 }, 50); 1003 } else { 1004 //console.log('Nessuna maschera trovata su questa slide'); 1005 } 1006 } 1007 }; 1008 1009 // Avvia l'animazione quando il carousel entra in viewport (solo per la prima volta) 1010 let hasEnteredViewport = false; 1011 if ('IntersectionObserver' in window) { 1012 const observer = new IntersectionObserver((entries, obs) => { 1013 entries.forEach((entry) => { 1014 if (entry.isIntersecting && !hasEnteredViewport) { 1015 hasEnteredViewport = true; 1016 triggerMaskAnimation(); 1017 obs.disconnect(); 1018 } 1019 }); 1020 }, { root: null, threshold: 0.2 }); 1021 observer.observe(carousel); 1022 } else { 1023 // Fallback: delay dopo load 1024 setTimeout(() => { 1025 if (!hasEnteredViewport) { 1026 hasEnteredViewport = true; 1027 triggerMaskAnimation(); 1028 } 1029 }, 300); 1030 } 1031 1032 // Hook diretto nella funzione goToSlide per intercettare i cambi 1033 const originalGoToSlide = goToSlide; 1034 goToSlide = function(index) { 1035 const result = originalGoToSlide.call(this, index); 1036 // Trigger l'animazione dopo che la slide è cambiata 1037 setTimeout(() => { 1038 const activeCard = carousel.querySelector('.carousel-card.active'); 1039 if (activeCard) { 1040 triggerMaskAnimation(); 1041 } 1042 }, 100); 1043 return result; 1044 }; 1045 }); 1046 1047})(); 1048 1049// --- Utility: animazione maschera bolle per card attiva --- 1050function startCardMaskIntro(card) { 1051 if (!card) return; 1052 1053 // Ottieni l'index della card corrente 1054 const cardIndex = card.dataset.index; 1055 //console.log('Avvio animazione per card index:', cardIndex); 1056 1057 // Kill eventuali animazioni precedenti per evitare conflitti 1058 const svg = card.querySelector('.card-image .masked-svg'); 1059 if (!svg) { 1060 //console.log('SVG non trovato in questa card'); 1061 return; 1062 } 1063 1064 // Kill solo i tween di questa specifica card e resetta tutti gli elementi 1065 const blobs = svg.querySelectorAll('.blob'); 1066 blobs.forEach(blob => { 1067 gsap.killTweensOf(blob); 1068 // Forza il reset immediato 1069 blob.style.transform = ''; 1070 }); 1071 1072 // Cerca i blob SOLO con l'index specifico di questa card 1073 const cBig = svg.querySelector(`.blob-big.blob-index-${cardIndex}`); 1074 const cUR = svg.querySelector(`.blob-small-ur.blob-index-${cardIndex}`); 1075 const cBL = svg.querySelector(`.blob-small-bl.blob-index-${cardIndex}`); 1076 if (!cBig || !cUR || !cBL) { 1077 //console.log('Blob non trovati per index:', cardIndex); 1078 return; 1079 } 1080 1081 //console.log('Blob trovati per index', cardIndex, '- avvio animazione'); 1082 //console.log('Big:', cBig, 'UR:', cUR, 'BL:', cBL); 1083 1084 // Valori finali di riferimento (come in card_carousel.php) 1085 const finalCircles = [ 1086 { key: 'big', cx: 500, cy: 500, r: 480 }, 1087 { key: 'ur', cx: 780, cy: 240, r: 200 }, 1088 { key: 'bl', cx: 240, cy: 780, r: 200 }, 1089 ]; 1090 1091 // Usa i valori finali definiti in "finalCircles" 1092 const targets = [ 1093 { el: cBig, cx: finalCircles[0].cx, cy: finalCircles[0].cy, r: finalCircles[0].r }, 1094 { el: cUR, cx: finalCircles[1].cx, cy: finalCircles[1].cy, r: finalCircles[1].r }, 1095 { el: cBL, cx: finalCircles[2].cx, cy: finalCircles[2].cy, r: finalCircles[2].r }, 1096 ]; 1097 1098 // Imposta stato iniziale: tutti dal centro r=0 (solo per questa card)
1099 targets.forEach(({ el }) => { 1100 //console.log('Reset blob:', el); 1101 // Forza il reset con gsap.set per essere sicuri 1102 gsap.set(el, { 1103 attr: { cx: 500, cy: 500, r: 0 }, 1104 clearProps: "transform" 1105 }); 1106 }); 1107 1108 // Helper: movimento continuo random nell'intera area del contenitore 1109 const wander = (el, pad, isBig = false, baseR = 200) => { 1110 const next = () => { 1111 // Per il cerchio big, genera PRIMA il nuovo raggio 1112 let nextRadius = baseR; 1113 if (isBig) { 1114 nextRadius = gsap.utils.random(baseR * 0.38, baseR * 1); 1115 } 1116 1117 // Calcola limiti basandosi sul raggio che il cerchio AVRà nel prossimo stato 1118 // In questo modo il cerchio non esce mai dai bordi 1119 const min = nextRadius; 1120 const max = 1000 - nextRadius; 1121 1122 // Movimento randomico entro i limiti calcolati 1123 const nx = Math.floor(gsap.utils.random(min, max, 1)); 1124 const ny = Math.floor(gsap.utils.random(min, max, 1)); 1125 1126 // Durata randomica per tutti i cerchi 1127 const duration = gsap.utils.random(2.5, 4.5); 1128 1129 // Costruisci gli attributi da animare 1130 const attr = { cx: nx, cy: ny }; 1131 if (isBig) { 1132 attr.r = nextRadius; // Usa il raggio già calcolato 1133 } 1134 1135 gsap.to(el, { 1136 duration: duration, 1137 ease: 'sine.inOut', 1138 attr: attr, 1139 onComplete: next 1140 }); 1141 }; 1142 next(); 1143 }; 1144 1145 // Animazioni sequenziali: ur -> bl -> big 1146 const tl = gsap.timeline(); 1147 1148 // 1) UR parte subito: crescita + spostamento verso angolo alto-destra (3 secondi) 1149 tl.to(targets[1].el, { 1150 duration: 3, 1151 ease: 'power2.out', 1152 attr: { 1153 cx: targets[1].cx, 1154 cy: targets[1].cy, 1155 r: targets[1].r 1156 } 1157 }, 0); 1158 1159 // 2) BL parte dopo 2 secondi: crescita + spostamento verso angolo basso-sinistra (3 secondi) 1160 tl.to(targets[2].el, { 1161 duration: 3, 1162 ease: 'power2.out', 1163 attr: { 1164 cx: targets[2].cx, 1165 cy: targets[2].cy, 1166 r: targets[2].r 1167 } 1168 }, 0.7); 1169 1170 // 3) BIG parte dopo altri 2 secondi: crescita + spostamento verso centro (4 secondi) 1171 tl.to(targets[0].el, { 1172 duration: 4, 1173 ease: 'power2.out', 1174 attr: { 1175 cx: targets[0].cx, 1176 cy: targets[0].cy, 1177 r: targets[0].r 1178 } 1179 }, 2); 1180 1181 // 4) Avvia il movimento continuo di TUTTI i blob contemporaneamente 1182 tl.add(() => { 1183 // Tutti i blob iniziano il movimento continuo insieme 1184 targets.forEach(({ el, r }, i) => { 1185 const isBig = (i === 0); 1186 //wander(el, null, isBig, r); 1187 }); 1188 }, 3.7); // Parte a 3.7s, verso la fine delle animazioni iniziali 1189 1190 // 5) NON segnare come animata per permettere riavvio 1191 // tl.call(() => { card.dataset.maskAnimated = 'true'; }); 1192} 1193 1194/** 1195 * Rettangolo in coordinate utente del viewBox effettivamente visibile nel layout. 1196 * Con xMidYMid slice la viewport mostra solo la parte centrale del viewBox: i limiti 0â1000 1197 * non coincidono con l'area visibile e le bolle risultano tagliate se non si usa questo rettangolo. 1198 */ 1199function iuadSvgSliceVisibleUserRect(svg) { 1200 if (!svg || !svg.viewBox || !svg.viewBox.baseVal) { 1201 return { minX: 0, maxX: 1000, minY: 0, maxY: 1000 }; 1202 } 1203 const vb = svg.viewBox.baseVal; 1204 const vbx = vb.x; 1205 const vby = vb.y; 1206 const vbw = vb.width; 1207 const vbh = vb.height; 1208 const W = svg.clientWidth; 1209 const H = svg.clientHeight; 1210 if (!(W > 0) || !(H > 0) || !vbw || !vbh) { 1211 return { minX: vbx, maxX: vbx + vbw, minY: vby, maxY: vby + vbh }; 1212 } 1213 const raw = (svg.getAttribute('preserveAspectRatio') || '').trim(); 1214 const tokens = raw.split(/\s+/).filter(function (t) { 1215 return t && String(t).toLowerCase() !== 'defer'; 1216 }); 1217 const meetOrSlice = (tokens[1] || 'meet').toLowerCase(); 1218 if (meetOrSlice !== 'slice') { 1219 return { minX: vbx, maxX: vbx + vbw, minY: vby, maxY: vby + vbh }; 1220 } 1221 const s = Math.max(W / vbw, H / vbh); 1222 const ox = (W - vbw * s) / 2; 1223 const oy = (H - vbh * s) / 2; 1224 const uxLo = vbx - ox / s; 1225 const uxHi = vbx + (W - ox) / s; 1226 const uyLo = vby - oy / s; 1227 const uyHi = vby + (H - oy) / s; 1228 return { 1229 minX: Math.max(vbx, uxLo), 1230 maxX: Math.min(vbx + vbw, uxHi), 1231 minY: Math.max(vby, uyLo), 1232 maxY: Math.min(vby + vbh, uyHi) 1233 }; 1234} 1235 1236function iuadHeroMaskCircleCenterBounds(vis, r, gooeyPad) {
1237 const g = typeof gooeyPad === 'number' ? gooeyPad : 0; 1238 const rad = typeof r === 'number' && r > 0 ? r : 0; 1239 let minCx = vis.minX + rad + g; 1240 let maxCx = vis.maxX - rad - g; 1241 let minCy = vis.minY + rad + g; 1242 let maxCy = vis.maxY - rad - g; 1243 if (maxCx < minCx) { 1244 const m = (vis.minX + vis.maxX) / 2; 1245 minCx = m; 1246 maxCx = m; 1247 } 1248 if (maxCy < minCy) { 1249 const m = (vis.minY + vis.maxY) / 2; 1250 minCy = m; 1251 maxCy = m; 1252 } 1253 return { minCx: minCx, maxCx: maxCx, minCy: minCy, maxCy: maxCy }; 1254} 1255 1256function iuadClampCircleCenterToHeroMaskBounds(cx, cy, bounds) { 1257 return { 1258 cx: Math.min(bounds.maxCx, Math.max(bounds.minCx, cx)), 1259 cy: Math.min(bounds.maxCy, Math.max(bounds.minCy, cy)) 1260 }; 1261} 1262 1263 1264/** 1265 * Hero Mask Animation - Generic Function 1266 * Funzione generica per animare maschere SVG con blob su hero sections 1267 * 1268 * @param {Object} config - Configurazione dell'animazione 1269 * @param {string} config.wrapperSelector - Selettore CSS del wrapper dell'hero 1270 * @param {string} config.svgSelector - Selettore CSS dell'SVG (default: '.hero-masked-svg') 1271 * @param {Object} config.blobConfig - Configurazione dei blob finali 1272 * @param {Object} config.initialState - Stato iniziale dei blob (default: centro con r=0) 1273 * @param {number} config.observerThreshold - Threshold per IntersectionObserver (default: 0.2) 1274 * @param {number} config.wanderGooeyPad - Margine in unità utente per dilatazione/blur della mask gooey (default: 48) 1275 */ 1276function initHeroMaskAnimation(config) { 1277 'use strict'; 1278 1279 const { 1280 wrapperSelector, 1281 svgSelector = '.hero-masked-svg', 1282 blobConfig = { 1283 big: { cx: 500, cy: 500, r: 430 }, 1284 ur: { cx: 780, cy: 240, r: 180 }, 1285 bl: { cx: 240, cy: 780, r: 180 } 1286 }, 1287 initialState = { 1288 cx: 500, 1289 cy: 500, 1290 r: 0 1291 }, 1292 observerThreshold = 0.2, 1293 wanderGooeyPad = 48 1294 } = config; 1295 1296 const heroWrapper = document.querySelector(wrapperSelector); 1297 1298 if (!heroWrapper) return; 1299 1300 const svg = heroWrapper.querySelector(svgSelector); 1301 if (!svg) return; 1302 1303 const cBig = svg.querySelector('.hero-blob-big'); 1304 const cUR = svg.querySelector('.hero-blob-small-ur'); 1305 const cBL = svg.querySelector('.hero-blob-small-bl'); 1306 1307 if (!cBig || !cUR || !cBL) { 1308 //console.log('Hero blob non trovati in', wrapperSelector); 1309 return; 1310 } 1311 1312 const startHeroMaskAnimation = () => { 1313 let layoutFrames = 0; 1314 const runWhenSvgHasLayout = () => { 1315 layoutFrames += 1; 1316 if ((svg.clientWidth > 0 && svg.clientHeight > 0) || layoutFrames >= 72) { 1317 startHeroMaskAnimationAfterLayout(); 1318 } else { 1319 requestAnimationFrame(runWhenSvgHasLayout); 1320 } 1321 }; 1322 requestAnimationFrame(runWhenSvgHasLayout); 1323 }; 1324 1325 const startHeroMaskAnimationAfterLayout = () => { 1326 const finalCircles = [ 1327 { key: 'big', ...blobConfig.big }, 1328 { key: 'ur', ...blobConfig.ur }, 1329 { key: 'bl', ...blobConfig.bl }, 1330 ]; 1331 1332 const targets = [ 1333 { el: cBig, cx: finalCircles[0].cx, cy: finalCircles[0].cy, r: finalCircles[0].r }, 1334 { el: cUR, cx: finalCircles[1].cx, cy: finalCircles[1].cy, r: finalCircles[1].r }, 1335 { el: cBL, cx: finalCircles[2].cx, cy: finalCircles[2].cy, r: finalCircles[2].r }, 1336 ]; 1337 1338 const visIntro = iuadSvgSliceVisibleUserRect(svg); 1339 targets.forEach(function (t) { 1340 const b = iuadHeroMaskCircleCenterBounds(visIntro, t.r, wanderGooeyPad); 1341 const clamped = iuadClampCircleCenterToHeroMaskBounds(t.cx, t.cy, b); 1342 t.cx = clamped.cx; 1343 t.cy = clamped.cy; 1344 }); 1345 1346 // Imposta stato iniziale personalizzabile 1347 targets.forEach(({ el }) => { 1348 gsap.set(el, { 1349 attr: { 1350 cx: initialState.cx, 1351 cy: initialState.cy, 1352 r: initialState.r 1353 }, 1354 clearProps: "transform" 1355 }); 1356 }); 1357 1358 const wander = (el, pad, isBig = false, baseR = 200) => { 1359 const next = () => { 1360 let nextRadius = baseR; 1361 if (isBig) { 1362 nextRadius = gsap.utils.random(baseR * 0.38, baseR * 1); 1363 } 1364 1365 const vis = iuadSvgSliceVisibleUserRect(svg); 1366 const wb = iuadHeroMaskCircleCenterBounds(vis, nextRadius, wanderGooeyPad); 1367 1368 const nx = Math.floor(gsap.utils.random(wb.minCx, wb.maxCx, 1)); 1369 const ny = Math.floor(gsap.utils.random(wb.minCy, wb.maxCy, 1)); 1370 1371 // Durata randomica per tutti i cerchi 1372 const duration = gsap.utils.random(2.5, 4.5); 1373 1374 // Costruisci gli attributi da animare 1375 const attr = { cx: nx, cy: ny }; 1376 if (isBig) { 1377 attr.r = nextRadius; // Usa il raggio già calcolato 1378 } 1379 1380 gsap.to(el, { 1381 duration: duration, 1382 ease: 'sine.inOut', 1383 attr: attr, 1384 onComplete: next 1385 }); 1386 }; 1387 next(); 1388 }; 1389 1390 // Animazioni sequenziali: ur -> bl -> big 1391 const tl = gsap.timeline(); 1392 1393 // 1) UR parte subito: crescita + spostamento verso angolo alto-destra 1394 tl.to(targets[1].el, { 1395 duration: 3, 1396 ease: 'power2.out', 1397 attr: { 1398 cx: targets[1].cx, 1399 cy: targets[1].cy, 1400 r: targets[1].r 1401 } 1402 }, 0); 1403 1404 // 2) BL parte dopo 0.7s: crescita + spostamento verso angolo basso-sinistra 1405 tl.to(targets[2].el, { 1406 duration: 3, 1407 ease: 'power2.out', 1408 attr: { 1409 cx: targets[2].cx, 1410 cy: targets[2].cy, 1411 r: targets[2].r 1412 } 1413 }, 0.7); 1414 1415 // 3) BIG parte dopo 2s: crescita + spostamento verso centro (4 secondi) 1416 tl.to(targets[0].el, { 1417 duration: 4, 1418 ease: 'power2.out', 1419 attr: { 1420 cx: targets[0].cx, 1421 cy: targets[0].cy, 1422 r: targets[0].r 1423 } 1424 }, 2); 1425
1426 // 4) Avvia il movimento continuo di TUTTI i blob contemporaneamente 1427 tl.add(() => { 1428 // Tutti i blob iniziano il movimento continuo insieme 1429 targets.forEach(({ el, r }, i) => { 1430 const isBig = (i === 0); 1431 wander(el, null, isBig, r); 1432 }); 1433 }, 3.7); // Parte a 3.7s, verso la fine delle animazioni iniziali 1434 }; 1435 1436 // Avvia l'animazione quando l'hero entra in viewport 1437 let hasEnteredViewport = false; 1438 if ('IntersectionObserver' in window) { 1439 const observer = new IntersectionObserver((entries, obs) => { 1440 entries.forEach((entry) => { 1441 if (entry.isIntersecting && !hasEnteredViewport) { 1442 hasEnteredViewport = true; 1443 startHeroMaskAnimation(); 1444 obs.disconnect(); 1445 } 1446 }); 1447 }, { root: null, threshold: observerThreshold }); 1448 observer.observe(heroWrapper); 1449 } else { 1450 // Fallback: delay dopo load 1451 setTimeout(() => { 1452 if (!hasEnteredViewport) { 1453 hasEnteredViewport = true; 1454 startHeroMaskAnimation(); 1455 } 1456 }, 300); 1457 } 1458} 1459 1460 1461/** 1462 * Hero Corsi - Animazione maschera SVG 1463 * Inizializza l'animazione per l'hero dei corsi 1464*/ 1465(function() { 1466 'use strict'; 1467 1468 document.addEventListener('DOMContentLoaded', () => { 1469 initHeroMaskAnimation({ 1470 wrapperSelector: '.hero_corsi_wrapper', 1471 blobConfig: { 1472 big: { cx: 500, cy: 500, r: 430 }, 1473 ur: { cx: 780, cy: 240, r: 180 }, 1474 bl: { cx: 240, cy: 780, r: 180 } 1475 }, 1476 initialState: { 1477 cx: 500, 1478 cy: 500, 1479 r: 0 1480 } 1481 }); 1482 }); 1483 1484})(); 1485 1486 1487/** 1488 * Hero Sedi - Animazione maschera SVG 1489 * Inizializza l'animazione per l'hero delle sedi 1490*/ 1491(function() { 1492 'use strict'; 1493 1494 document.addEventListener('DOMContentLoaded', () => { 1495 initHeroMaskAnimation({ 1496 wrapperSelector: '.hero-sedi_wrapper', 1497 blobConfig: { 1498 big: { cx: 500, cy: 500, r: 380 }, 1499 ur: { cx: 730, cy: 230, r: 130 }, 1500 bl: { cx: 230, cy: 730, r: 130 } 1501 }, 1502 initialState: { 1503 cx: 500, 1504 cy: 500, 1505 r: 0 1506 } 1507 }); 1508 }); 1509 1510})(); 1511/** 1512 * Hero News - Animazione maschera SVG 1513 * Inizializza l'animazione per l'hero delle news 1514*/ 1515(function() { 1516 'use strict'; 1517 1518 document.addEventListener('DOMContentLoaded', () => { 1519 initHeroMaskAnimation({ 1520 wrapperSelector: '.hero-news_wrapper', 1521 blobConfig: { 1522 big: { cx: 500, cy: 500, r: 380 }, 1523 ur: { cx: 730, cy: 230, r: 130 }, 1524 bl: { cx: 230, cy: 730, r: 130 } 1525 }, 1526 initialState: { 1527 cx: 500, 1528 cy: 500, 1529 r: 0 1530 } 1531 }); 1532 }); 1533 1534})(); 1535 1536 1537/** 1538 * Page Loader Animation 1539 * Animazione stile Tripletta Pizza: div impilati che appaiono dal basso 1540 * e poi scorrono tutti insieme verso l'alto come un sipario 1541 */ 1542(function() { 1543 'use strict'; 1544 1545 // Avvia appena il DOM è pronto (non aspettare il 'load' completo) 1546 document.addEventListener('DOMContentLoaded', () => { 1547 const loader = document.getElementById('pageLoader'); 1548 1549 if (!loader) return; 1550 1551 const container = loader.querySelector('.loader-container'); 1552 const slides = loader.querySelectorAll('.loader-slide'); 1553 1554 if (slides.length === 0) return; 1555 1556 // Se la URL contiene un anchor, NON blocchiamo lo scroll: 1557 // il jump nativo verso l'anchor deve poter avvenire liberamente. 1558 const shouldLockScroll = !(window.location.hash && window.location.hash.length > 1); 1559 1560 // Blocca lo scroll finché il loader non è scomparso del tutto. 1561 // Nota: usiamo overflow su html/body e fermiamo Lenis se presente. 1562 const prevHtmlOverflow = document.documentElement.style.overflow; 1563 const prevBodyOverflow = document.body.style.overflow; 1564 const prevBodyPosition = document.body.style.position; 1565 const prevBodyWidth = document.body.style.width; 1566 const scrollYAtLock = window.scrollY || window.pageYOffset || 0; 1567 let scrollLocked = false;
1568 1569 function lockScroll() { 1570 if (!shouldLockScroll) return; 1571 if (scrollLocked) return; 1572 scrollLocked = true; 1573 document.body.classList.add('loading'); 1574 document.documentElement.style.overflow = 'hidden'; 1575 document.body.style.overflow = 'hidden'; 1576 // iOS/Safari: prevenzione ulteriore 1577 document.body.style.position = 'fixed'; 1578 document.body.style.top = (-scrollYAtLock) + 'px'; 1579 document.body.style.width = '100%'; 1580 if (window.lenis && typeof window.lenis.stop === 'function') { 1581 window.lenis.stop(); 1582 } 1583 } 1584 1585 function unlockScroll() { 1586 if (!shouldLockScroll) return; 1587 if (!scrollLocked) return; 1588 scrollLocked = false; 1589 document.body.classList.remove('loading'); 1590 document.documentElement.style.overflow = prevHtmlOverflow; 1591 document.body.style.overflow = prevBodyOverflow; 1592 document.body.style.position = prevBodyPosition; 1593 document.body.style.width = prevBodyWidth; 1594 document.body.style.top = ''; 1595 if (window.lenis && typeof window.lenis.start === 'function') { 1596 window.lenis.start(); 1597 } 1598 1599 // Se siamo entrati con un anchor (#...), NON ripristiniamo lo scroll pre-loader, 1600 // altrimenti annulliamo il jump verso l'anchor. In quel caso lasciamo gestire 1601 // lo scroll alla logica anchor post-unlock. 1602 if (window.location.hash && window.location.hash.length > 1) { 1603 if (typeof window.iuadScrollToInitialAnchor === 'function') { 1604 window.iuadScrollToInitialAnchor(); 1605 setTimeout(() => window.iuadScrollToInitialAnchor(), 100); 1606 } 1607 } else { 1608 window.scrollTo(0, scrollYAtLock); 1609 } 1610 } 1611 1612 lockScroll(); 1613 1614 // Imposta i colori di background e testo per ogni slide
1615 slides.forEach(slide => { 1616 const bgColor = slide.getAttribute('data-bg'); 1617 const textColor = slide.getAttribute('data-color'); 1618 slide.style.backgroundColor = bgColor; 1619 slide.style.color = textColor; 1620 }); 1621 1622 const HERO_VIDEO_BREAKPOINT = 767; 1623 1624 function setHeroVideoForViewport() { 1625 const hero = document.getElementById('heroHome'); 1626 const video = document.getElementById('heroVideo'); 1627 if (!hero || !video) return; 1628 const isMobile = window.matchMedia('(max-width: ' + HERO_VIDEO_BREAKPOINT + 'px)').matches; 1629 const url = isMobile ? hero.getAttribute('data-video-mobile') : hero.getAttribute('data-video-desktop'); 1630 const type = isMobile ? (hero.getAttribute('data-video-mobile-type') || 'video/mp4') : (hero.getAttribute('data-video-desktop-type') || 'video/mp4'); 1631 if (!url) return; 1632 video.pause(); 1633 video.innerHTML = '<source src="' + url + '" type="' + type + '">'; 1634 video.load(); 1635 video.classList.add('loaded'); 1636 video.play().catch(err => { 1637 //console.log('Autoplay prevented:', err); 1638 }); 1639 } 1640 1641 let heroVideoViewportMobile = null; 1642 function onHeroVideoResize() { 1643 const isMobile = window.matchMedia('(max-width: ' + HERO_VIDEO_BREAKPOINT + 'px)').matches; 1644 if (heroVideoViewportMobile !== isMobile) { 1645 heroVideoViewportMobile = isMobile; 1646 setHeroVideoForViewport(); 1647 } 1648 } 1649 window.addEventListener('resize', onHeroVideoResize); 1650 1651 const mainTimeline = gsap.timeline({ 1652 delay: 0.2, 1653 onComplete: () => { 1654 loader.classList.add('loaded'); 1655 loader.style.display = 'none'; 1656 unlockScroll(); 1657 1658 setHeroVideoForViewport(); 1659 heroVideoViewportMobile = window.matchMedia('(max-width: ' + HERO_VIDEO_BREAKPOINT + 'px)').matches; 1660 1661 triggerHomeEntryAnimations(); 1662 } 1663 }); 1664 1665 // Animazione di ogni slide che appare (senza movimento, solo opacity) 1666 slides.forEach((slide, index) => { 1667 const isFinal = slide.classList.contains('final'); 1668 const displayDuration = isFinal ? 0.8 : 0.4; // Quanto tempo resta visibile 1669 1670 // Per la prima slide, è già visibile, quindi solo aspetta 1671 if (index === 0) { 1672 mainTimeline.to({}, { duration: displayDuration }); 1673 mainTimeline.add(() => { 1674 slide.classList.remove('active'); 1675 }); 1676 } else { 1677 // Mostra la slide (aggiunge classe active) 1678 mainTimeline.add(() => { 1679 slide.classList.add('active'); 1680 }); 1681 1682 // Pausa mentre la slide è visibile 1683 mainTimeline.to({}, { duration: displayDuration }); 1684 1685 // Nascondi la slide (rimuove classe active) - solo se non è l'ultima 1686 if (index < slides.length - 1) { 1687 mainTimeline.add(() => { 1688 slide.classList.remove('active'); 1689 }); 1690 } 1691 } 1692 }); 1693 1694 // Prima dell'animazione del sipario, mostra il sito dietro 1695 mainTimeline.add(() => { 1696 // Mostra il contenuto dietro il loader 1697 gsap.set('#wrapper-navbar, .site-content, .hero-home', { 1698 opacity: 1, 1699 visibility: 'visible' 1700 }); 1701 }); 1702 1703 // Animazione finale: tutto il container scorre verso l'alto (sipario che si alza) 1704 // Ora mentre sale, si vede il sito sotto 1705 mainTimeline.to(container, { 1706 y: '-100%', 1707 duration: 1.2, 1708 ease: 'power3.inOut' 1709 }); 1710 }); 1711 1712})(); 1713 1714 1715/** 1716 * Home Entry Animations 1717 * Animazioni bounce per header e scroll button dopo il loader 1718 * Solo sulla home page 1719 */ 1720function triggerHomeEntryAnimations() { 1721 'use strict'; 1722 1723 // Verifica se siamo in home page 1724 const isHome = document.body.classList.contains('home') || 1725 document.body.classList.contains('page-template-template-home'); 1726 1727 if (!isHome) return; 1728 1729 const header = document.querySelector('.nd-desktop-header'); 1730 const scrollBtn = document.querySelector('.hero-home__scroll-btn'); 1731 1732 // Animazione header: bounce dall'alto 1733 if (header) { 1734 gsap.fromTo(header, 1735 { 1736 y: -100, 1737 opacity: 0 1738 }, 1739 { 1740 y: 0, 1741 opacity: 1, 1742 duration: 0.8, 1743 ease: 'back.out(1.7)', 1744 delay: 0.3 1745 } 1746 ); 1747 } 1748 1749 // Animazione scroll button: bounce dal basso 1750 if (scrollBtn) { 1751 gsap.fromTo(scrollBtn, 1752 { 1753 y: 100, 1754 opacity: 0 1755 }, 1756 { 1757 y: 0, 1758 opacity: 1, 1759 duration: 0.8, 1760 ease: 'back.out(1.7)', 1761 delay: 0.5 1762 } 1763 ); 1764 } 1765} 1766 1767 1768/**
1769 * Background Change Animation Trigger 1770 * Attiva l'animazione di cambio colore background sincronizzata su tutti gli elementi 1771 * quando il primo elemento entra in viewport 1772 * Uso: aggiungi la classe 'bg-change-trigger' a qualsiasi elemento 1773 */ 1774(function() { 1775 'use strict'; 1776 1777 // Flag globale per tracciare se l'animazione è già stata avviata 1778 let hasAnimated = false; 1779 let observer = null; 1780 1781 /** 1782 * Funzione helper per verificare se un elemento è visibile 1783 * (non dentro un template responsive nascosto) 1784 */ 1785 function isElementVisible(element) { 1786 const computedStyle = window.getComputedStyle(element); 1787 if (computedStyle.display === 'none' || computedStyle.visibility === 'hidden') { 1788 return false; 1789 } 1790 1791 // Verifica se è dentro un template responsive nascosto 1792 const mobileWrapper = element.closest('.responsive-template-mobile'); 1793 if (mobileWrapper && window.getComputedStyle(mobileWrapper).display === 'none') { 1794 return false; 1795 } 1796 1797 const desktopWrapper = element.closest('.responsive-template-desktop'); 1798 if (desktopWrapper && window.getComputedStyle(desktopWrapper).display === 'none') { 1799 return false; 1800 } 1801 1802 return true; 1803 } 1804 1805 /** 1806 * Funzione per inizializzare l'observer 1807 */ 1808 function initBgChangeAnimation() { 1809 // Se l'animazione è già stata avviata, non reinizializzare 1810 if (hasAnimated) return; 1811 1812 // Trova tutti gli elementi con classe bg-change-trigger 1813 const allTriggers = document.querySelectorAll('.bg-change-trigger'); 1814 1815 if (allTriggers.length === 0) return; 1816 1817 // Filtra solo gli elementi visibili (non dentro template nascosti) 1818 const triggers = Array.from(allTriggers).filter(trigger => isElementVisible(trigger)); 1819 1820 if (triggers.length === 0) return; 1821 1822 // Se c'è già un observer attivo, disconnettilo 1823 if (observer) { 1824 observer.disconnect(); 1825 } 1826 1827 // Trova il primo trigger visibile 1828 const firstTrigger = triggers[0]; 1829 1830 // Crea un IntersectionObserver per il primo elemento 1831 observer = new IntersectionObserver((entries) => { 1832 entries.forEach((entry) => { 1833 // Quando il primo elemento entra in viewport e non è ancora stato animato 1834 if (entry.isIntersecting && !hasAnimated) { 1835 // Trova di nuovo tutti i trigger (potrebbero essere cambiati) 1836 const allTriggersNow = document.querySelectorAll('.bg-change-trigger'); 1837 const visibleTriggers = Array.from(allTriggersNow).filter(trigger => isElementVisible(trigger)); 1838 1839 // Aggiungi la classe a TUTTI gli elementi con bg-change-trigger visibili contemporaneamente 1840 visibleTriggers.forEach((trigger) => { 1841 trigger.classList.add('bg-change-animation'); 1842 }); 1843 1844 if (document.body.classList.contains('bg-change-sync-target')) { 1845 document.body.classList.remove('bg-change-animation'); 1846 void document.body.offsetWidth; 1847 document.body.classList.add('bg-change-animation'); 1848 } 1849 1850 hasAnimated = true; 1851 1852 // Disconnetti l'observer dopo aver aggiunto la classe 1853 // L'animazione continuerà in loop infinito su tutti gli elementi 1854 if (observer) { 1855 observer.disconnect(); 1856 } 1857 1858 //console.log(`Background animation started synchronously for ${visibleTriggers.length} elements`); 1859 } 1860 }); 1861 }, { 1862 root: null, // viewport 1863 threshold: 0.2, // 20% dell'elemento deve essere visibile 1864 rootMargin: '0px' 1865 }); 1866 1867 // Osserva solo il primo elemento visibile 1868 observer.observe(firstTrigger); 1869 } 1870 1871 // Inizializza al DOMContentLoaded 1872 document.addEventListener('DOMContentLoaded', () => { 1873 initBgChangeAnimation(); 1874 }); 1875 1876 // Reinizializza quando i template responsive cambiano visibilità 1877 // Usa un MutationObserver per rilevare cambiamenti di display 1878 if (typeof MutationObserver !== 'undefined') { 1879 const mutationObserver = new MutationObserver(() => { 1880 // Se l'animazione non è ancora partita, riprova 1881 if (!hasAnimated) { 1882 setTimeout(() => { 1883 initBgChangeAnimation(); 1884 }, 100); 1885 } 1886 }); 1887 1888 // Osserva cambiamenti nel DOM 1889 mutationObserver.observe(document.body, { 1890 childList: true, 1891 subtree: true, 1892 attributes: true, 1893 attributeFilter: ['style', 'class'] 1894 }); 1895 } 1896 1897 // Reinizializza anche su resize (per template responsive) 1898 let resizeTimeout; 1899 window.addEventListener('resize', () => { 1900 clearTimeout(resizeTimeout); 1901 resizeTimeout = setTimeout(() => { 1902 if (!hasAnimated) { 1903 initBgChangeAnimation(); 1904 } 1905 }, 200); 1906 }); 1907 1908})(); 1909 1910 1911/**
1912 * Background and Text Change Animation Trigger 1913 * Attiva l'animazione di cambio colore background E testo sincronizzata su tutti gli elementi 1914 * quando il primo elemento entra in viewport 1915 * Uso: aggiungi la classe 'bg-text-change-trigger' all'elemento contenitore 1916 * e 'text-color-change' agli elementi di testo al suo interno 1917 */ 1918(function() { 1919 'use strict'; 1920 1921 // Flag globale per tracciare se l'animazione è già stata avviata 1922 let hasAnimated = false; 1923 let observer = null; 1924 1925 /** 1926 * Funzione helper per verificare se un elemento è visibile 1927 * (non dentro un template responsive nascosto) 1928 */ 1929 function isElementVisible(element) { 1930 const computedStyle = window.getComputedStyle(element); 1931 if (computedStyle.display === 'none' || computedStyle.visibility === 'hidden') { 1932 return false; 1933 } 1934 1935 // Verifica se è dentro un template responsive nascosto 1936 const mobileWrapper = element.closest('.responsive-template-mobile'); 1937 if (mobileWrapper && window.getComputedStyle(mobileWrapper).display === 'none') { 1938 return false; 1939 } 1940 1941 const desktopWrapper = element.closest('.responsive-template-desktop'); 1942 if (desktopWrapper && window.getComputedStyle(desktopWrapper).display === 'none') { 1943 return false; 1944 } 1945 1946 return true; 1947 } 1948 1949 /** 1950 * Funzione per inizializzare l'observer 1951 */ 1952 function initBgTextChangeAnimation() { 1953 // Se l'animazione è già stata avviata, non reinizializzare 1954 if (hasAnimated) return; 1955 1956 // Trova tutti gli elementi con classe bg-text-change-trigger 1957 const allTriggers = document.querySelectorAll('.bg-text-change-trigger'); 1958 1959 if (allTriggers.length === 0) return; 1960 1961 // Filtra solo gli elementi visibili (non dentro template nascosti) 1962 const triggers = Array.from(allTriggers).filter(trigger => isElementVisible(trigger)); 1963 1964 if (triggers.length === 0) return; 1965 1966 // Se c'è già un observer attivo, disconnettilo 1967 if (observer) { 1968 observer.disconnect(); 1969 } 1970 1971 // Trova il primo trigger visibile 1972 const firstTrigger = triggers[0]; 1973 1974 // Crea un IntersectionObserver per il primo elemento 1975 observer = new IntersectionObserver((entries) => { 1976 entries.forEach((entry) => { 1977 // Quando il primo elemento entra in viewport e non è ancora stato animato 1978 if (entry.isIntersecting && !hasAnimated) { 1979 // Trova di nuovo tutti i trigger (potrebbero essere cambiati) 1980 const allTriggersNow = document.querySelectorAll('.bg-text-change-trigger'); 1981 const visibleTriggers = Array.from(allTriggersNow).filter(trigger => isElementVisible(trigger)); 1982 1983 // Aggiungi la classe background a TUTTI gli elementi con bg-text-change-trigger visibili 1984 visibleTriggers.forEach((trigger) => { 1985 trigger.classList.add('bg-change-animation'); 1986 1987 // Trova gli elementi di testo all'interno e aggiungi l'animazione del colore 1988 const textElements = trigger.querySelectorAll('.text-color-change'); 1989 textElements.forEach((textElement) => { 1990 textElement.classList.add('text-color-change-animation'); 1991 }); 1992 }); 1993 1994 if (document.body.classList.contains('bg-change-sync-target')) { 1995 document.body.classList.remove('bg-change-animation'); 1996 void document.body.offsetWidth; 1997 document.body.classList.add('bg-change-animation'); 1998 } 1999 2000 hasAnimated = true; 2001 2002 // Disconnetti l'observer dopo aver aggiunto le classi 2003 // Le animazioni continueranno in loop infinito sincronizzate 2004 if (observer) { 2005 observer.disconnect(); 2006 } 2007
2008 //console.log(`Background and text animation started synchronously for ${visibleTriggers.length} elements`); 2009 } 2010 }); 2011 }, { 2012 root: null, // viewport 2013 threshold: 0.2, // 20% dell'elemento deve essere visibile 2014 rootMargin: '0px' 2015 }); 2016 2017 // Osserva solo il primo elemento visibile 2018 observer.observe(firstTrigger); 2019 } 2020 2021 // Inizializza al DOMContentLoaded 2022 document.addEventListener('DOMContentLoaded', () => { 2023 initBgTextChangeAnimation(); 2024 }); 2025 2026 // Reinizializza quando i template responsive cambiano visibilità 2027 // Usa un MutationObserver per rilevare cambiamenti di display 2028 if (typeof MutationObserver !== 'undefined') { 2029 const mutationObserver = new MutationObserver(() => { 2030 // Se l'animazione non è ancora partita, riprova 2031 if (!hasAnimated) { 2032 setTimeout(() => { 2033 initBgTextChangeAnimation(); 2034 }, 100); 2035 } 2036 }); 2037 2038 // Osserva cambiamenti nel DOM 2039 mutationObserver.observe(document.body, { 2040 childList: true, 2041 subtree: true, 2042 attributes: true, 2043 attributeFilter: ['style', 'class'] 2044 }); 2045 } 2046 2047 // Reinizializza anche su resize (per template responsive) 2048 let resizeTimeout; 2049 window.addEventListener('resize', () => { 2050 clearTimeout(resizeTimeout); 2051 resizeTimeout = setTimeout(() => { 2052 if (!hasAnimated) { 2053 initBgTextChangeAnimation(); 2054 } 2055 }, 200); 2056 }); 2057 2058})(); 2059 2060 2061/** 2062 * Dipartimenti Block - Scroll Pinned Animation 2063 * Animazione sincronizzata con lo scroll: i blocchi salgono dal basso mentre la sezione è pinnata 2064 * La pagina riprende a scrollare solo quando tutti i blocchi hanno raggiunto la loro posizione 2065 */ 2066(function() { 2067 'use strict'; 2068 2069 document.addEventListener('DOMContentLoaded', () => { 2070 const dipartimentiBlock = document.querySelector('.dipartimenti-block'); 2071 2072 if (!dipartimentiBlock) return; 2073 2074 // Verifica che l'elemento sia visibile (non nascosto da display:none) 2075 const isVisible = window.getComputedStyle(dipartimentiBlock).display !== 'none'; 2076 if (!isVisible) { 2077 // Se non è visibile, aspetta che diventi visibile (per template responsive dinamici) 2078 const checkVisibility = () => { 2079 const checkVisible = window.getComputedStyle(dipartimentiBlock).display !== 'none'; 2080 if (checkVisible) { 2081 initDipartimentiAnimation(); 2082 } else { 2083 setTimeout(checkVisibility, 100); 2084 } 2085 }; 2086 checkVisibility(); 2087 return; 2088 } 2089 2090 initDipartimentiAnimation(); 2091 }); 2092 2093 function initDipartimentiAnimation() { 2094 const dipartimentiBlock = document.querySelector('.dipartimenti-block'); 2095 if (!dipartimentiBlock) return; 2096 2097 // Verifica nuovamente la visibilità prima di inizializzare 2098 if (window.getComputedStyle(dipartimentiBlock).display === 'none') { 2099 return; 2100 } 2101 2102 const cards = dipartimentiBlock.querySelectorAll('.dipartimento-card'); 2103 2104 if (cards.length === 0) return; 2105 2106 // Registra ScrollTrigger plugin 2107 gsap.registerPlugin(ScrollTrigger); 2108 2109 // Definisci gli offset iniziali per ogni blocco 2110 const initialOffsets = [0, 20, 10, 30]; // px per blocchi 1, 2, 3, 4 2111 2112 // Controlla se siamo su mobile 2113 const isMobile = window.innerWidth <= 1024; 2114 2115 // Timeline principale per l'animazione 2116 const tl = gsap.timeline({ 2117 scrollTrigger: { 2118 trigger: dipartimentiBlock, 2119 start: 'top top', 2120 end: '+=200%', // L'animazione dura per 200% dell'altezza del viewport 2121 pin: true, // Pinna la sezione durante l'animazione 2122 scrub: 1, // Sincronizza l'animazione con lo scroll (1 = smooth) 2123 anticipatePin: 1, 2124 // markers: true, // Decommentare per debug 2125 } 2126 }); 2127 2128 // FASE 1: Card 1 e 3 salgono per prime (partenza anticipata) 2129 [0, 2].forEach((index) => { 2130 const card = cards[index]; 2131 const compensation = isMobile ? 0 : -initialOffsets[index]; 2132 2133 tl.to(card, { 2134 y: compensation, // Arrivano alla posizione finale allineata 2135 duration: 0.6, 2136 ease: 'power2.out' 2137 }, 0); // Partono subito 2138 }); 2139 2140 // FASE 2: Card 2 e 4 salgono dopo (partenza ritardata) 2141 [1, 3].forEach((index) => { 2142 const card = cards[index]; 2143 const compensation = isMobile ? 0 : -initialOffsets[index]; 2144 2145 tl.to(card, { 2146 y: compensation, // Arrivano alla posizione finale allineata 2147 duration: 0.6, 2148 ease: 'power2.out' 2149 }, 0.1); // Partono 0.1s dopo (ridotto da 0.3s) 2150 }); 2151 2152 // Aggiungi classe animated a tutte le card alla fine 2153 tl.call(() => { 2154 cards.forEach(card => card.classList.add('animated')); 2155 }); 2156 2157 // Pausa finale prima di rilasciare il pin 2158 tl.to({}, { duration: 0.4 }); 2159 2160 //console.log('Dipartimenti block scroll animation initialized'); 2161 } 2162 2163})(); 2164 2165 2166/** 2167 * Carousel Sedi - Infinite Loop Marquee 2168 * Animazione infinita continua senza stop per il carousel di sedi e dipartimenti 2169 */ 2170(function() { 2171 'use strict'; 2172 2173 document.addEventListener('DOMContentLoaded', () => { 2174 const carousel = document.querySelector('.carousel-sedi-block'); 2175 2176 if (!carousel) return; 2177 2178 // Verifica che l'elemento sia visibile (non nascosto da display:none) 2179 const isVisible = window.getComputedStyle(carousel).display !== 'none'; 2180 if (!isVisible) { 2181 // Se non è visibile, aspetta che diventi visibile (per template responsive dinamici) 2182 const checkVisibility = () => { 2183 const checkVisible = window.getComputedStyle(carousel).display !== 'none'; 2184 if (checkVisible) { 2185 initCarouselSediAnimation(); 2186 } else { 2187 setTimeout(checkVisibility, 100); 2188 } 2189 }; 2190 checkVisibility(); 2191 return; 2192 } 2193 2194 initCarouselSediAnimation(); 2195 }); 2196 2197 function initCarouselSediAnimation() { 2198 const carousel = document.querySelector('.carousel-sedi-block'); 2199 if (!carousel) return; 2200 2201 // Verifica nuovamente la visibilità prima di inizializzare 2202 if (window.getComputedStyle(carousel).display === 'none') { 2203 return; 2204 } 2205 2206 const track = carousel.querySelector('.carousel-sedi-track'); 2207 2208 if (!track) return; 2209 2210 // Registra GSAP plugin 2211 gsap.registerPlugin(ScrollTrigger); 2212 2213 // Calcola la larghezza totale di un set di item (1/3 del totale perché abbiamo duplicato 3 volte) 2214 const items = track.querySelectorAll('.carousel-sedi-item'); 2215 const totalItems = items.length; 2216 const itemsPerSet = totalItems / 3; 2217 2218 // Calcola la larghezza di un singolo set 2219 let setWidth = 0; 2220 for (let i = 0; i < itemsPerSet; i++) { 2221 setWidth += items[i].offsetWidth; 2222 // Aggiungi anche il gap (tranne per l'ultimo) 2223 if (i < itemsPerSet - 1) { 2224 const gap = parseInt(window.getComputedStyle(track).gap) || 80; 2225 setWidth += gap; 2226 } 2227 } 2228 2229 // Durata dell'animazione (in secondi) - più basso = più veloce 2230 const duration = 40; 2231 2232 // Crea l'animazione infinita con GSAP 2233 gsap.to(track, { 2234 x: -setWidth, 2235 duration: duration, 2236 ease: 'none', // Linear per movimento costante 2237 repeat: -1, // Ripeti all'infinito 2238 modifiers: { 2239 x: gsap.utils.unitize(x => parseFloat(x) % setWidth) // Seamless loop 2240 } 2241 }); 2242 2243 //console.log('Carousel sedi infinite loop initialized'); 2244 } 2245 2246})(); 2247 2248 2249/** 2250 * Exchange Program Block - Card Opening Animation
2251 * Le card si aprono ai lati rivelando il testo centrale durante lo scroll 2252 */ 2253(function() { 2254 'use strict'; 2255 2256 document.addEventListener('DOMContentLoaded', () => { 2257 const exchangeBlock = document.querySelector('.exchange-program-block'); 2258 2259 if (!exchangeBlock) return; 2260 2261 // Verifica che l'elemento sia visibile (non nascosto da display:none) 2262 const isVisible = window.getComputedStyle(exchangeBlock).display !== 'none'; 2263 if (!isVisible) { 2264 // Se non è visibile, aspetta che diventi visibile (per template responsive dinamici) 2265 const checkVisibility = () => { 2266 const checkVisible = window.getComputedStyle(exchangeBlock).display !== 'none'; 2267 if (checkVisible) { 2268 initExchangeProgramAnimation(); 2269 } else { 2270 setTimeout(checkVisibility, 100); 2271 } 2272 }; 2273 checkVisibility(); 2274 return; 2275 } 2276 2277 initExchangeProgramAnimation(); 2278 }); 2279 2280 function initExchangeProgramAnimation() { 2281 const exchangeBlock = document.querySelector('.exchange-program-block'); 2282 if (!exchangeBlock) return; 2283 2284 // Verifica nuovamente la visibilità prima di inizializzare 2285 if (window.getComputedStyle(exchangeBlock).display === 'none') { 2286 return; 2287 } 2288 2289 const cards = exchangeBlock.querySelectorAll('.exchange-card'); 2290 const content = exchangeBlock.querySelector('.exchange-program-content'); 2291 2292 if (cards.length === 0) return; 2293 2294 // Registra ScrollTrigger plugin 2295 gsap.registerPlugin(ScrollTrigger); 2296 2297 // Controlla se siamo su mobile 2298 const isMobile = window.innerWidth <= 1024; 2299 2300 if (isMobile) { 2301 // Su mobile non fare animazione 2302 return; 2303 } 2304 2305 // Il contenuto centrale è sempre visibile (nessuna animazione) 2306 gsap.set(content, { opacity: 1, scale: 1 }); 2307 2308 // Timeline principale per l'animazione 2309 const tl = gsap.timeline({ 2310 scrollTrigger: { 2311 trigger: exchangeBlock, 2312 start: 'top top', 2313 end: '+=150%', 2314 pin: true, 2315 scrub: 1, 2316 anticipatePin: 1, 2317 // markers: true, // Decommentare per debug 2318 } 2319 }); 2320
2321 // Le card si aprono spostandosi verso le loro posizioni finali 2322 cards.forEach((card) => { 2323 const cardNumber = card.getAttribute('data-card'); 2324 const computedStyle = getComputedStyle(card); 2325 2326 // Leggi le posizioni finali dalle CSS variables 2327 const finalTop = computedStyle.getPropertyValue('--final-top').trim(); 2328 const finalLeft = computedStyle.getPropertyValue('--final-left').trim(); 2329 const finalRight = computedStyle.getPropertyValue('--final-right').trim(); 2330 2331 // Prepara l'oggetto di animazione 2332 const animationProps = { 2333 duration: 0.8, 2334 ease: 'power2.out', 2335 force3D: true // Mantiene accelerazione hardware 2336 }; 2337 2338 // Anima verso la posizione finale usando top e left/right 2339 if (finalTop) { 2340 animationProps.top = finalTop; 2341 } 2342 2343 if (finalLeft) { 2344 animationProps.left = finalLeft; 2345 } else if (finalRight) { 2346 animationProps.right = finalRight; 2347 } 2348 2349 tl.to(card, animationProps, 0); // Tutte le card si muovono insieme 2350 }); 2351 2352 // Pausa finale 2353 tl.to({}, { duration: 0.4 }); 2354 2355 //console.log('Exchange Program block animation initialized'); 2356 } 2357 2358})(); 2359 2360 2361/** 2362 * Portfolio Home Block - Vertical Scroll Animation 2363 * Le card appaiono dal basso e salgono durante lo scroll, come se passassero attraverso una finestra 2364 */ 2365(function() { 2366 'use strict'; 2367 2368 document.addEventListener('DOMContentLoaded', () => { 2369 const portfolioBlock = document.querySelector('.portfolio-home-block'); 2370 2371 if (!portfolioBlock) return; 2372 2373 // Verifica che l'elemento sia visibile (non nascosto da display:none) 2374 const isVisible = window.getComputedStyle(portfolioBlock).display !== 'none'; 2375 if (!isVisible) { 2376 // Se non è visibile, aspetta che diventi visibile (per template responsive dinamici) 2377 const checkVisibility = () => { 2378 const checkVisible = window.getComputedStyle(portfolioBlock).display !== 'none'; 2379 if (checkVisible) { 2380 initPortfolioHomeAnimation(); 2381 } else { 2382 setTimeout(checkVisibility, 100); 2383 } 2384 }; 2385 checkVisibility(); 2386 return; 2387 } 2388 2389 initPortfolioHomeAnimation(); 2390 }); 2391 2392 function initPortfolioHomeAnimation() { 2393 const portfolioBlock = document.querySelector('.portfolio-home-block'); 2394 if (!portfolioBlock) return; 2395 2396 // Verifica nuovamente la visibilità prima di inizializzare 2397 if (window.getComputedStyle(portfolioBlock).display === 'none') { 2398 return; 2399 } 2400 2401 const cardsContainer = portfolioBlock.querySelector('.portfolio-cards-container'); 2402 const viewport = portfolioBlock.querySelector('.portfolio-scroll-viewport'); 2403 2404 if (!cardsContainer || !viewport) return; 2405 2406 // Registra ScrollTrigger plugin 2407 gsap.registerPlugin(ScrollTrigger); 2408 2409 // Controlla se siamo su mobile 2410 const isMobile = window.innerWidth <= 1024; 2411 2412 if (isMobile) { 2413 // Su mobile non fare animazione 2414 return; 2415 } 2416 2417 // Calcola l'altezza totale del contenuto 2418 const containerHeight = cardsContainer.offsetHeight; 2419 const viewportHeight = viewport.offsetHeight; 2420 2421 // Distanza totale che le card devono percorrere 2422 // Devono partire da sotto la viewport e uscire completamente sopra 2423 const travelDistance = containerHeight + viewportHeight; 2424 2425 // Imposta la posizione iniziale: card posizionate sotto la viewport 2426 gsap.set(cardsContainer, { 2427 y: viewportHeight 2428 }); 2429 2430 // Timeline principale per l'animazione 2431 const tl = gsap.timeline({ 2432 scrollTrigger: { 2433 trigger: portfolioBlock, 2434 start: 'top top', 2435 end: '+=300%', // L'animazione dura per 300% dell'altezza del viewport 2436 pin: true, // Pinna la sezione durante l'animazione 2437 scrub: 1, // Sincronizza l'animazione con lo scroll (1 = smooth) 2438 anticipatePin: 1, 2439 // markers: true, // Decommentare per debug 2440 } 2441 }); 2442
2443 // Anima il container delle card dal basso verso l'alto 2444 tl.to(cardsContainer, { 2445 y: -containerHeight, // Le card escono completamente dalla viewport 2446 ease: 'none', // Linear per un movimento costante 2447 duration: 1 2448 }); 2449 2450 //console.log('Portfolio Home block vertical scroll animation initialized'); 2451 } 2452 2453})(); 2454 2455 2456/** 2457 * Portfolio Listing Hero - Vertical Scroll Animation 2458 * Le card appaiono dal basso e salgono durante lo scroll, come se passassero attraverso una finestra 2459 */ 2460(function() { 2461 'use strict'; 2462 2463 let portfolioScrollTrigger = null; 2464 let portfolioTimeline = null; 2465 let portfolioCardsContainer = null; 2466 let portfolioViewport = null; 2467 2468 function initPortfolioListingHero() { 2469 const portfolioListingHero = document.querySelector('.portfolio-listing-hero'); 2470 2471 if (!portfolioListingHero) return; 2472 2473 // Verifica che l'elemento sia visibile (non nascosto da display:none) 2474 const isVisible = window.getComputedStyle(portfolioListingHero).display !== 'none'; 2475 if (!isVisible) { 2476 // Se non è visibile, aspetta che diventi visibile (per template responsive dinamici) 2477 const checkVisibility = () => { 2478 const checkVisible = window.getComputedStyle(portfolioListingHero).display !== 'none'; 2479 if (checkVisible) { 2480 initPortfolioListingHero(); 2481 } else { 2482 setTimeout(checkVisibility, 100); 2483 } 2484 }; 2485 checkVisibility(); 2486 return; 2487 } 2488 2489 portfolioCardsContainer = portfolioListingHero.querySelector('.portfolio-listing-cards-container'); 2490 portfolioViewport = portfolioListingHero.querySelector('.portfolio-listing-scroll-viewport'); 2491 2492 if (!portfolioCardsContainer || !portfolioViewport) return; 2493 2494 // Registra ScrollTrigger plugin 2495 gsap.registerPlugin(ScrollTrigger); 2496 2497 // Controlla se siamo su mobile 2498 const isMobile = window.innerWidth <= 1024; 2499 2500 if (isMobile) { 2501 // Su mobile non fare animazione 2502 return; 2503 } 2504 2505 // Se esiste già un ScrollTrigger, killalo 2506 if (portfolioScrollTrigger) { 2507 portfolioScrollTrigger.kill(); 2508 } 2509 2510 // Calcola l'altezza totale del contenuto 2511 const containerHeight = portfolioCardsContainer.offsetHeight; 2512 const viewportHeight = portfolioViewport.offsetHeight; 2513 2514 // Imposta la posizione iniziale: card posizionate sotto la viewport 2515 gsap.set(portfolioCardsContainer, { 2516 y: viewportHeight 2517 }); 2518 2519 // Calcola la distanza di scroll in base all'altezza del contenuto 2520 // La distanza di scroll deve essere proporzionale all'altezza del contenuto 2521 // per creare un effetto parallax fluido 2522 const scrollDistance = containerHeight + viewportHeight; 2523 2524 // Timeline principale per l'animazione 2525 portfolioTimeline = gsap.timeline({ 2526 scrollTrigger: { 2527 trigger: portfolioListingHero, 2528 start: 'top top', 2529 end: `+=${scrollDistance}px`, // Distanza di scroll proporzionale all'altezza del contenuto 2530 pin: portfolioListingHero, // Pinna l'intera section 2531 scrub: true, // Sincronizza l'animazione con lo scroll (true = più fluido) 2532 anticipatePin: 1, 2533 // markers: true, // Decommentare per debug 2534 } 2535 }); 2536 2537 // Salva l'istanza ScrollTrigger 2538 portfolioScrollTrigger = portfolioTimeline.scrollTrigger; 2539 2540 // Anima il container delle card dal basso verso l'alto 2541 portfolioTimeline.to(portfolioCardsContainer, { 2542 y: -containerHeight, // Le card escono completamente dalla viewport 2543 ease: 'none', // Linear per un movimento costante 2544 duration: 1 2545 }); 2546 2547 //console.log('Portfolio Listing Hero vertical scroll animation initialized'); 2548 } 2549 2550 // Funzione per ricalcolare l'animazione dopo il filtro 2551 window.recalculatePortfolioListingHero = function() { 2552 if (!portfolioCardsContainer || !portfolioViewport) { 2553 initPortfolioListingHero(); 2554 return; 2555 } 2556 2557 // Controlla se siamo su mobile 2558 const isMobile = window.innerWidth <= 1024; 2559 if (isMobile) return; 2560 2561 // Kill il vecchio ScrollTrigger 2562 if (portfolioScrollTrigger) { 2563 portfolioScrollTrigger.kill(); 2564 portfolioScrollTrigger = null; 2565 } 2566 2567 // Ricalcola l'animazione 2568 initPortfolioListingHero(); 2569 }; 2570 2571 document.addEventListener('DOMContentLoaded', () => { 2572 initPortfolioListingHero(); 2573 }); 2574 2575 // Reinizializza anche su resize (per template responsive) 2576 let resizeTimeout; 2577 window.addEventListener('resize', () => { 2578 clearTimeout(resizeTimeout); 2579 resizeTimeout = setTimeout(() => { 2580 // Kill il vecchio ScrollTrigger se esiste 2581 if (portfolioScrollTrigger) { 2582 portfolioScrollTrigger.kill(); 2583 portfolioScrollTrigger = null; 2584 portfolioTimeline = null; 2585 portfolioCardsContainer = null; 2586 portfolioViewport = null; 2587 } 2588 // Reinizializza 2589 initPortfolioListingHero(); 2590 }, 200); 2591 }); 2592 2593})(); 2594 2595 2596/** 2597 * Docenti Listing Hero - Vertical Scroll Animation 2598 * Le card appaiono dal basso e salgono durante lo scroll, come se passassero attraverso una finestra 2599 */ 2600(function() { 2601 'use strict'; 2602 2603 let docentiScrollTrigger = null; 2604 let docentiTimeline = null; 2605 let docentiCardsContainer = null; 2606 let docentiViewport = null; 2607 2608 function initDocentiListingHero() { 2609 const docentiListingHero = document.querySelector('.docenti-listing-hero'); 2610 2611 if (!docentiListingHero) return; 2612 2613 // Verifica che l'elemento sia visibile (non nascosto da display:none) 2614 const isVisible = window.getComputedStyle(docentiListingHero).display !== 'none'; 2615 if (!isVisible) { 2616 // Se non è visibile, aspetta che diventi visibile (per template responsive dinamici) 2617 const checkVisibility = () => { 2618 const checkVisible = window.getComputedStyle(docentiListingHero).display !== 'none'; 2619 if (checkVisible) { 2620 initDocentiListingHero(); 2621 } else { 2622 setTimeout(checkVisibility, 100); 2623 } 2624 }; 2625 checkVisibility(); 2626 return; 2627 } 2628 2629 docentiCardsContainer = docentiListingHero.querySelector('.docenti-listing-cards-container'); 2630 docentiViewport = docentiListingHero.querySelector('.docenti-listing-scroll-viewport'); 2631 2632 if (!docentiCardsContainer || !docentiViewport) return; 2633 2634 // Registra ScrollTrigger plugin 2635 gsap.registerPlugin(ScrollTrigger); 2636 2637 // Controlla se siamo su mobile 2638 const isMobile = window.innerWidth <= 1024; 2639 2640 if (isMobile) { 2641 // Su mobile non fare animazione 2642 return; 2643 } 2644 2645 // Se esiste già un ScrollTrigger, killalo 2646 if (docentiScrollTrigger) { 2647 docentiScrollTrigger.kill(); 2648 } 2649
2650 // Calcola l'altezza totale del contenuto 2651 const containerHeight = docentiCardsContainer.offsetHeight; 2652 const viewportHeight = docentiViewport.offsetHeight; 2653 2654 // Imposta la posizione iniziale: card posizionate sotto la viewport 2655 gsap.set(docentiCardsContainer, { 2656 y: viewportHeight 2657 }); 2658 2659 // Calcola la distanza di scroll in base all'altezza del contenuto 2660 // La distanza di scroll deve essere proporzionale all'altezza del contenuto 2661 // per creare un effetto parallax fluido 2662 const scrollDistance = containerHeight + viewportHeight; 2663 2664 // Timeline principale per l'animazione 2665 docentiTimeline = gsap.timeline({ 2666 scrollTrigger: { 2667 trigger: docentiListingHero, 2668 start: 'top top', 2669 end: `+=${scrollDistance}px`, // Distanza di scroll proporzionale all'altezza del contenuto 2670 pin: docentiListingHero, // Pinna l'intera section 2671 scrub: true, // Sincronizza l'animazione con lo scroll (true = più fluido) 2672 anticipatePin: 1, 2673 // markers: true, // Decommentare per debug 2674 } 2675 }); 2676 2677 // Salva l'istanza ScrollTrigger 2678 docentiScrollTrigger = docentiTimeline.scrollTrigger; 2679 2680 // Anima il container delle card dal basso verso l'alto 2681 docentiTimeline.to(docentiCardsContainer, { 2682 y: -containerHeight, // Le card escono completamente dalla viewport 2683 ease: 'none', // Linear per un movimento costante 2684 duration: 1 2685 }); 2686 2687 //console.log('Docenti Listing Hero vertical scroll animation initialized'); 2688 } 2689 2690 // Funzione per ricalcolare l'animazione dopo il filtro 2691 window.recalculateDocentiListingHero = function() { 2692 if (!docentiCardsContainer || !docentiViewport) { 2693 initDocentiListingHero(); 2694 return; 2695 } 2696 2697 // Controlla se siamo su mobile 2698 const isMobile = window.innerWidth <= 1024; 2699 if (isMobile) return; 2700 2701 // Kill il vecchio ScrollTrigger 2702 if (docentiScrollTrigger) { 2703 docentiScrollTrigger.kill(); 2704 docentiScrollTrigger = null; 2705 } 2706 2707 // Ricalcola l'animazione 2708 initDocentiListingHero(); 2709 }; 2710 2711 document.addEventListener('DOMContentLoaded', () => { 2712 initDocentiListingHero(); 2713 }); 2714 2715 // Reinizializza anche su resize (per template responsive) 2716 let resizeTimeout; 2717 window.addEventListener('resize', () => { 2718 clearTimeout(resizeTimeout); 2719 resizeTimeout = setTimeout(() => { 2720 // Kill il vecchio ScrollTrigger se esiste 2721 if (docentiScrollTrigger) { 2722 docentiScrollTrigger.kill(); 2723 docentiScrollTrigger = null; 2724 docentiTimeline = null; 2725 docentiCardsContainer = null; 2726 docentiViewport = null; 2727 } 2728 // Reinizializza 2729 initDocentiListingHero(); 2730 }, 200); 2731 }); 2732 2733})(); 2734 2735 2736/** 2737 * Hero Home Scroll Button 2738 * Scrolla alla sezione successiva quando si clicca sul bottone 2739 */ 2740(function() { 2741 'use strict'; 2742 2743 document.addEventListener('DOMContentLoaded', () => { 2744 const scrollBtn = document.querySelector('.hero-home__scroll-btn'); 2745 2746 if (!scrollBtn) return; 2747 2748 scrollBtn.addEventListener('click', () => { 2749 const heroHome = document.getElementById('heroHome'); 2750 2751 if (!heroHome) return; 2752 2753 // Trova la sezione successiva 2754 const nextSection = heroHome.nextElementSibling; 2755 2756 if (nextSection) { 2757 // Smooth scroll alla sezione successiva 2758 nextSection.scrollIntoView({ 2759 behavior: 'smooth', 2760 block: 'start' 2761 }); 2762 } 2763 }); 2764 }); 2765 2766})(); 2767 2768 2769/** 2770 * Agevolazioni Hero - Department Icon Click 2771 * Cambia l'id della section in base all'icona del dipartimento cliccata 2772 */ 2773(function() { 2774 'use strict'; 2775 2776 document.addEventListener('DOMContentLoaded', () => { 2777 const agevolazioniHero = document.querySelector('.agevolazioni-hero'); 2778 2779 if (!agevolazioniHero) return; 2780 2781 // Verifica che l'elemento sia visibile (non nascosto da display:none) 2782 const isVisible = window.getComputedStyle(agevolazioniHero).display !== 'none'; 2783 if (!isVisible) { 2784 // Se non è visibile, aspetta che diventi visibile (per template responsive dinamici) 2785 const checkVisibility = () => { 2786 const checkVisible = window.getComputedStyle(agevolazioniHero).display !== 'none'; 2787 if (checkVisible) { 2788 initAgevolazioniHero(); 2789 } else { 2790 setTimeout(checkVisibility, 100); 2791 } 2792 }; 2793 checkVisibility(); 2794 return; 2795 } 2796 2797 initAgevolazioniHero(); 2798 }); 2799 2800 function initAgevolazioniHero() { 2801 const agevolazioniHero = document.querySelector('.agevolazioni-hero'); 2802 if (!agevolazioniHero) return; 2803 2804 // Verifica nuovamente la visibilità prima di inizializzare 2805 if (window.getComputedStyle(agevolazioniHero).display === 'none') { 2806 return; 2807 } 2808 2809 const departmentIcons = agevolazioniHero.querySelectorAll('.department-icon-wrapper'); 2810 2811 if (departmentIcons.length === 0) return; 2812 2813 // Aggiungi event listener a ogni icona 2814 departmentIcons.forEach((iconWrapper) => { 2815 iconWrapper.addEventListener('click', function() { 2816 // Recupera l'id del dipartimento dal wrapper (es: "fashion", "graphic", ecc.) 2817 const departmentId = this.id; 2818 2819 if (!departmentId) return; 2820 2821 // Rimuovi classe active da tutte le icone 2822 departmentIcons.forEach(icon => icon.classList.remove('active')); 2823 2824 // Aggiungi classe active all'icona cliccata 2825 this.classList.add('active'); 2826 2827 // Aggiorna l'id della section con "colore-{department_id}" 2828 agevolazioniHero.id = 'colore-' + departmentId; 2829 2830 //console.log('Agevolazioni hero ID updated to:', agevolazioniHero.id); 2831 }); 2832 }); 2833 2834 // Imposta il primo dipartimento come attivo di default 2835 if (departmentIcons.length > 0) { 2836 const firstIcon = departmentIcons[0]; 2837 const firstDepartmentId = firstIcon.id; 2838 2839 if (firstDepartmentId) { 2840 firstIcon.classList.add('active'); 2841 agevolazioniHero.id = 'colore-' + firstDepartmentId; 2842 } 2843 } 2844 } 2845 2846})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.