1import { scrollControl } from './scrollModule.js'; 2 3// Custom eases here 4// https://gsap.com/docs/v3/Eases/ 5CustomEase.create("elastic", ".2, 1.33, .25 ,1"); 6 7// Transition ease vars for text animations 8let transitionEase = "power1.out"; // Fade transition ease 9 10// This gives a delay on the enter animations first time the page loads 11// IMPORTANT: Change below to 0.2 when testing so you dont have to wait for the animations while deving. 12const DEFAULT_FIRST_LOAD_DELAY = 0.2; 13 14// Flag to track first load 15let isFirstLoad = true; 16let firstLoadDelay = DEFAULT_FIRST_LOAD_DELAY; 17 18// This is the enter page transtion first load 19window.scrollTo(0, 0); 20 21export function initLoader() { 22 scrollControl.stop(); 23 24 $('.loader, [loader-overlay]').addClass('active'); 25 26 27 if (document.querySelector('[data-barba-namespace="home"]')) { 28 29 $('body').addClass('doc-loaded'); 30 // Check if this is truly the first visit (your existing function) 31 function isFirstVisit() { 32 if (sessionStorage.getItem('loaderShown')) { 33 return false; 34 } 35 36 const referrer = document.referrer; 37 const currentDomain = window.location.hostname; 38 39 if (referrer && referrer.includes(currentDomain)) { 40 return false; 41 } 42 43 if (performance.navigation && performance.navigation.type === 1) { 44 return false; 45 } 46 47 if (performance.getEntriesByType('navigation')[0]?.type === 'reload') { 48 return false; 49 } 50 51 return true; 52 } 53 54 // Handle overlay and nav classes based on visit type 55 const overlay = document.querySelector('[loader-overlay]'); 56 const navContainer = document.querySelector('.nav-container'); 57 58 if (!isFirstVisit()) { 59 // Remove loader from DOM if it exists 60 $('.loader').remove(); 61 $('.remove-second-visit').remove(); 62 63 64 // SUBSEQUENT VISITS: 65 // Keep d-none on overlay (it stays hidden) 66 // Remove nav-home from nav container 67 if (navContainer && navContainer.classList.contains('nav-home')) { 68 navContainer.classList.remove('nav-home'); 69 } 70 71 // Show nav immediately and trigger page transition 72 gsap.set('.nav', { opacity: 1 }); 73 pageTransitionEnter(); 74 75 76 77 78 return; // Exit early 79 } 80 81 // FIRST VISIT: 82 // Remove d-none from overlay (so it shows) 83 if (overlay && overlay.classList.contains('d-none')) { 84 overlay.classList.remove('d-none'); 85 } 86 87 // Keep nav-home class (it's already there from PHP) 88 // No action needed for nav-container 89 90 // Mark that loader has been shown 91 sessionStorage.setItem('loaderShown', 'true'); 92 console.log('â First visit detected - showing loader and keeping nav-home'); 93 94 // Total number of images to load 95 const allFrameCount = 470; 96 97 // Get bottle canvas animation setup ready 98 const canvas = document.getElementById("hero-lightpass-2"); 99 const context = canvas.getContext("2d"); 100 //let isMobile = window.innerWidth <= 990; 101 102 function setCanvasSize() { 103 const dpr = window.devicePixelRatio || 1; 104 105 // Get window dimensions but enforce minimum width 106 let canvasWidth = Math.max(window.innerWidth, 1200); // Minimum width of 900px 107 let canvasHeight = window.innerHeight; 108 109 // Set internal canvas dimensions (accounting for DPR for sharpness) 110 canvas.width = canvasWidth * dpr; 111 canvas.height = canvasHeight * dpr; 112 113 // Set display size via CSS 114 canvas.style.width = `${canvasWidth}px`; 115 canvas.style.height = `${canvasHeight}px`; 116 117 // Scale the context to account for the DPR 118 const context = canvas.getContext('2d'); 119 context.scale(dpr, dpr); 120 } 121 setCanvasSize(); 122 123 const frameCount = 161; 124 const images = []; 125 const airpods = { frame: 0 }; 126 127 const basePath = canvas.getAttribute('img-path'); 128 129 const currentFrame = (index) => { 130 //const imagePath = isMobile ? 'ethical_linear_croped' : 'ethical_linear_croped'; 131 const imagePath = 'S1_'; 132 return `${basePath}${imagePath}${index.toString().padStart(5, '0')}.webp`; 133 }; 134 135 for (let i = 0; i < frameCount; i++) { 136 const img = new Image(); 137 img.src = currentFrame(i); 138 images.push(img); 139 } 140 window.addEventListener("resize", () => { 141 //isMobile = window.innerWidth <= 990; 142 setCanvasSize(); 143 render(); 144 }); 145 146 function render() { 147 const dpr = window.devicePixelRatio || 1; 148 const canvas = document.getElementById('hero-lightpass-2'); 149 const context = canvas.getContext('2d'); 150 151 // Clear using the CSS dimensions 152 context.clearRect(0, 0, canvas.width/dpr, canvas.height/dpr); 153 154 let img = images[Math.round(airpods.frame)]; 155 156 let imgRatio = img.width / img.height; 157 let canvasRatio = (canvas.width/dpr) / (canvas.height/dpr); 158 159 let drawWidth, drawHeight, offsetX, offsetY; 160 161 if (canvasRatio > imgRatio) { 162 // Canvas is wider than the image (relative to their heights) 163 drawHeight = canvas.height/dpr; 164 drawWidth = drawHeight * imgRatio; 165 offsetX = ((canvas.width/dpr) - drawWidth) / 2; // Center horizontally 166 offsetY = 0; // Top aligned 167 } else { 168 // Canvas is taller than the image (relative to their widths) 169 drawWidth = canvas.width/dpr; 170 drawHeight = drawWidth / imgRatio; 171 offsetX = ((canvas.width/dpr) - drawWidth) / 2; // Center horizontally here too 172 offsetY = 0;
172 // Top aligned - this is the change from your original code 173 } 174 175 context.drawImage(img, offsetX, offsetY, drawWidth, drawHeight); 176 } 177 178 179 180 images[0].onload = () => { 181 setTimeout(render, 500); 182 }; 183 184 // Animate logo and loader first 185 gsap.from(".logo-outer", { 186 duration: 1, 187 width: '0', 188 ease: 'power2.inOut', 189 }); 190 gsap.from(".logo-text path", { 191 duration: 0.6, 192 opacity: 0, 193 stagger: 0.05, 194 ease: 'power1.inOut', 195 onComplete: () => { 196 $(".logo-outer").addClass("logo-animation-complete"); 197 } 198 }); 199 200 gsap.from(".loading, .counter", { 201 duration: 1, 202 opacity: 0, 203 delay: 0.4, 204 stagger: 0.05, 205 ease: 'power1.inOut', 206 }); 207 208 // Simple loader state 209 const loaderState = { 210 imagesLoaded: 0, 211 animationTriggered: false, 212 loadingComplete: false, 213 lastPercent: 0 // Variable to store the last progress percent 214 }; 215 216 // Initialize counter at 0 217 updateCounterDisplay(0); 218 219 // Start image loading process 220 setTimeout(() => { 221 console.log('Starting image preload process for all ' + allFrameCount + ' frames'); 222 223 // Create an array to track all images 224 const allImages = []; 225 let loadedCount = 0; 226 227 // Preload all 525 images and track progress 228 for (let i = 0; i < allFrameCount; i++) { 229 const img = new Image(); 230 img.src = currentFrame(i); 231 232 // Track load events 233 img.onload = img.onerror = (event) => { 234 loadedCount++; 235 236 // Calculate percentage based on actual loaded images 237 const percent = Math.floor((loadedCount / allFrameCount) * 100); 238 239 // Log every 5% or so 240 if (percent % 5 === 0 && percent !== loaderState.lastPercent) { 241 //console.log(`Loading progress: ${loadedCount}/${allFrameCount} (${percent}%)`); 242 loaderState.lastPercent = percent; 243 } 244 245 // Log errors 246 if (event.type === 'error') { 247 console.log('Image failed to load:', img.src); 248 } 249 250 // Update the counter display 251 updateCounterDisplay(percent); 252 253 // When all images are loaded 254 if (loadedCount >= allFrameCount) { 255 console.log('â All ' + allFrameCount + ' frames loaded successfully'); 256 loaderState.loadingComplete = true; 257 // Initialize final transition once everything is loaded 258 loaderInit(); 259 } 260 }; 261 262 allImages.push(img); 263 } 264 265 // Also monitor any other images in the page content 266 $('#imagesLoaded').imagesLoaded() 267 .always(function(instance) { 268 //console.log('Content images loaded.'); 269 }) 270 .done(function(instance) { 271 //console.log('All content images successfully loaded.'); 272 }) 273 .fail(function(instance) { 274 console.log('At least one content image failed to load.'); 275 }) 276 .progress(function(instance, image) { 277 let total = instance.images.length; 278 let loaded = instance.progressedCount; 279 280 // Log more details for debugging 281 //console.log('Content image loaded:', image.img.src); 282 //console.log('Content loaded:', loaded, 'Total:', total); 283 if (!image.isLoaded) { 284 console.log('Content image failed to load:', image.img.src); 285 } 286 }); 287 288 }, 500); 289 290 // Function to update the counter display 291 function updateCounterDisplay(percent) { 292 percent = Math.floor(Math.max(0, Math.min(100, percent))); 293 294 // Format counter with individual spans for animation 295 let formattedPercentWithSpans = percent.toString() 296 .padStart(2, '0') 297 .split('') 298 .map(char => `<span>${char}</span>`) 299 .join(''); 300 301 $('.counter').html(formattedPercentWithSpans); 302 document.body.style.setProperty('--loader-counter', `${percent}`); 303 304 //console.log('Counter updated:', percent); 305 306 // Trigger SVG animation at 40% 307 if (percent >= 70 && !loaderState.animationTriggered) { 308 loaderState.animationTriggered = true; 309 //console.log('Triggering SVG animation at 40%'); 310 311 gsap.to("[loader-svg-out] img", { 312 duration: 1.5, 313 filter: 'blur(0px)', 314 opacity: 1, 315 stagger: 1, 316 ease: 'power1.out', 317 }); 318 319 gsap.to("[loader-bottle-outer]", { 320 opacity: 1, 321 duration: 1, 322 ease: 'power1.out' 323 }); 324 } 325 326 // Update counter width - use a fresh tween for each update 327 gsap.to(".counter-width", { 328 width: `${percent * 0.4}vw`, 329 duration: 0.3, 330 ease: "power1.out" 331 }); 332 } 333 334 // Final animation sequence 335 function loaderInit() { 336 console.log('â Starting loaderInit animation sequence'); 337 338 function startAnimation() { 339 gsap.to(airpods, { 340 frame: frameCount - 1, 341 duration: 1, 342 ease: "power2.inOut", 343 repeat: 0, 344 onUpdate: () => { 345 airpods.frame = Math.min(frameCount - 1, Math.max(0, airpods.frame)); 346 render(); 347 }, 348 }); 349 } 350 351 // Start a timeline 352 var tl = gsap.timeline(); 353 354 // First functions to run 355 tl.call(function() { 356 setTimeout(() => { 357 window.scrollTo(0, 0); 358 }, 300); 359 scrollControl.stop(); 360 }); 361 362 // Change cursor to wait from the get go. We will remove after the animation 363 tl.set("html", { 364 cursor: "wait"
365 }); 366 367 /** START CANVAS SCRUB **/ 368 tl.call(function() { 369 console.log('Starting bottle animation'); 370 startAnimation(); 371 }, null, 1.6); 372 373 tl.to("[loader-bottle]", { 374 duration: 2.5, 375 scale: 1, 376 ease: 'power3.inOut' 377 }, 1); 378 379 tl.from("[loader-bottle-outer]", { 380 //xPercent: -6, 381 rotate: 10, 382 duration: 1, 383 ease: "expo.inOut", 384 }, 1.5); 385 386 tl.to("[loader-bottle-outer-secondary]", { 387 opacity: 0, 388 filter: 'blur(20px)', 389 scale: 3, 390 duration: 1, 391 ease: 'power4.out' 392 }, 2.4); 393 394 395 // Spinning text 396 tl.to("[loader-svg-out]", { 397 scale: 3, 398 opacity: 0, 399 duration: 2, 400 rotate: 100, 401 filter: 'blur(30px)', 402 ease: 'expo.inOut' 403 }, 1); 404 405 tl.to("[counter-out]", { 406 scale: 2, 407 filter: 'blur(30px)', 408 opacity: 0, 409 duration: 2, 410 ease: 'expo.inOut' 411 }, 1); 412 413 414 415 416 tl.to("[loader-bg]", { 417 opacity: 0, 418 duration: 2, 419 ease: 'power1.out' 420 }, 1.6); 421 422 423 tl.to(".nav", { 424 opacity: 1, 425 ease: 'power3.inOut' 426 }, 3); 427 428 /** END OF INNER ANIMATION ELEMENTS **/ 429 430 // This is the end of the loader animation 431 const transitionOffset = 3.1; 432 tl.to(".loader", { 433 opacity: 0, 434 duration: 0, 435 ease: 'power1.out', 436 }, transitionOffset); 437 438 // This is the end of the loader animation and changes the cursor to normal 439 tl.set("html", { 440 cursor: "auto", 441 }, transitionOffset); 442 443 // And this brings in the first page transition 444 tl.call(function() { 445 console.log('Loader complete, starting page transition'); 446 pageTransitionEnter(); 447 $('.loader').remove(); 448 }, null, transitionOffset); 449 } 450 } 451 452 if (!document.querySelector('[data-barba-namespace="home"]')) { 453 454 $('body').addClass('doc-loaded'); 455 456 var tl = gsap.timeline(); 457 458 tl.to("[loader-bg]", { 459 opacity: 0, 460 duration: 0, 461 ease: 'power1.out' 462 }, 0); 463 464 /** END OF INNER ANIMATION ELEMENTS **/ 465 tl.to(".loader", { 466 opacity: 0, 467 duration: 0.6, 468 ease: 'power1.out', 469 }, 0.6); 470 471 // This is the end of the loader animation and changes the cursor to normal 472 tl.set("html", { 473 cursor: "auto", 474 }, 0.6); 475 476 // And this brings in the first page transition 477 tl.call(function() { 478 console.log('Loader complete, starting page transition'); 479 pageTransitionEnter(); 480 //$('.loader').remove(); 481 }, null, 0.6); 482 } 483} 484 485// This is the enter page transition - also enter after first loader 486export function pageTransitionEnter(data) { 487 $('main').addClass('pageTransitionEnter'); 488 489 490 // Change bodu color wthis this attribute data-bg="rgb(0, 50, 50)" in main element. 491 let bgColor = document.querySelector('main').getAttribute('data-bg'); 492 document.body.style.backgroundColor = bgColor; 493 494 // Start timeline for below animations 495 var tl = gsap.timeline(); 496 497 // Reason for this lenis stop is to bring in the page animations first 498 tl.call(function() { 499 scrollControl.stop(); 500 });
501 502 //Etical life paths 503 tl.from(".pathAnim1", { 504 duration: 0.6, 505 opacity: 0, 506 yPercent: 100, 507 ease: "back.out(1.3)", 508 delay: 0.5 509 }, 0); 510 tl.from(".pathAnim2", { 511 duration: 0.6, 512 opacity: 0, 513 yPercent: 100, 514 ease: "back.out(1.3)", 515 delay: 0.5 516 }, 0.07); 517 tl.from(".pathAnim3", { 518 duration: 0.6, 519 opacity: 0, 520 yPercent: 100, 521 ease: "back.out(1.3)", 522 delay: 0.5 523 }, 0.14); 524 tl.from(".pathAnim4", { 525 duration: 0.6, 526 opacity: 0, 527 yPercent: 100, 528 ease: "back.out(1.3)", 529 delay: 0.5 530 }, 0.21); 531 tl.from(".pathAnim5", { 532 duration: 0.6, 533 opacity: 0, 534 yPercent: 100, 535 ease: "back.out(1.3)", 536 delay: 0.5 537 }, 0.28); 538 539 540 // Chars 541 if ($('[transition-chars]').length) { 542 if(document.querySelector('[transition-chars]')) { 543 tl.set("[transition-chars] .single-char-inner", { 544 opacity: 0, 545 rotate: 0.001, 546 scale: 1.2, 547 filter: 'blur(20px)', 548 }, 0); 549 } 550 $('[transition-chars]').each(function () { 551 let delay = parseFloat($(this).attr('transition-chars')) || 0.4; 552 let element = $(this).find('.single-char-inner'); 553 tl.to(element, { 554 opacity: 1, 555 filter: 'blur(0px)', 556 scale: 1, 557 stagger: 0.04, 558 duration: 0.8, 559 ease: transitionEase, 560 }, delay + firstLoadDelay); 561 }); 562 } 563 564 565 // Words 566 if ($('[transition-words]').length) { 567 if(document.querySelector('[transition-words]')) { 568 tl.set("[transition-words] .single-word-inner", { 569 yPercent: 120, 570 rotate: 6, 571 }, 0); 572 } 573 $('[transition-words]').each(function () { 574 let delay = parseFloat($(this).attr('transition-words')) || 0; 575 let element = $(this).find('.single-word-inner'); 576 tl.to(element, { 577 yPercent: 0, 578 rotate: 0.001, 579 stagger: 0.05, 580 duration: 1, 581 ease: transitionEase, 582 }, delay + firstLoadDelay); 583 }); 584 } 585 586 587 588 // Fade 589 if ($('[transition-fade]').length) { 590 $('[transition-fade]').each(function () { 591 let delay = parseFloat($(this).attr('transition-fade')) || 0.4; 592 let element = $(this); 593 594 tl.from(element, { 595 opacity: 0, 596 duration: 0.6, 597 ease: transitionEase, 598 }, delay + firstLoadDelay); 599 }); 600 } 601 602 // Fade out 603 if ($('[transition-fade-out]').length) { 604 $('[transition-fade-out]').each(function () { 605 let delay = parseFloat($(this).attr('transition-fade-out')) || 0.4; 606 let element = $(this); 607 608 tl.to(element, { 609 opacity: 0, 610 duration: 0.6, 611 ease: transitionEase, 612 }, delay + firstLoadDelay); 613 }); 614 } 615 616 // Fade blur 617 if ($('[transition-fade-blur]').length) { 618 $('[transition-fade-blur]').each(function () { 619 let delay = parseFloat($(this).attr('transition-fade-blur')) || 0; 620 let element = $(this); 621 622 tl.from(element, { 623 opacity: 0, 624 filter: 'blur(20px)', 625 duration: 0.4, 626 ease: transitionEase, 627 }, delay + firstLoadDelay); 628 }); 629 } 630 631 632 if ($('[transition-fade-gummies]').length) { 633 // Get all elements at once instead of looping through them individually 634 let elements = $('[transition-fade-gummies]'); 635 636 tl.from(elements, { 637 opacity: 0, 638 scale: 3, 639 filter: 'blur(20px)', 640 duration: 0.8, 641 delay:0, 642 ease: "power3.inOut", 643 stagger: { 644 amount: 0.5, 645 from: 'random' 646 }, 647 }, 0); 648 } 649 650 // Fade blur stagger 651 if ($('[transition-fade-blur-stagger]').length) { 652 $('[transition-fade-blur-stagger]').each(function () { 653 let delay = parseFloat($(this).attr('transition-fade')) || 0; 654 let element = $(this).find('[stagger-item]'); 655 tl.from(element, { 656 opacity: 0, 657 stagger: { 658 amount: 0.5, 659 from: 'random' 660 }, 661 filter: 'blur(20px)', 662 duration: 0.4, 663 scale:0, 664 ease: transitionEase, 665 }, delay + firstLoadDelay); 666 }); 667 } 668 669 670 tl.call(function() { 671 // Now enable lenis again 672 console.log('Page transition complete, enabling scroll'); 673 scrollControl.start(); 674 ScrollTrigger.refresh();
675 $('body').removeClass('first-load'); 676 677 678 }, null, "0.4"); 679 680 // Change firstLoadDelay after the first load 681 firstLoadDelay = isFirstLoad ? 0 : DEFAULT_FIRST_LOAD_DELAY; 682 683} 684 685// This is the leave page transition 686export function pageTransitionLeave(data) { 687 688 $('main').removeClass('pageTransitionEnter'); 689 $('main').addClass('pageTransitionLeave'); 690 691 scrollControl.stop(); 692 693 // Start timeline for below animations 694 var tl = gsap.timeline(); 695 tl.to(".content", { 696 duration: 0.6, 697 filter: 'blur(80px)', 698 scaleX: 1.5, 699 opacity: 0, 700 ease: 'expo.inOut', 701 delay: 0 702 }, 0); 703}
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.