1/** 2 * @file 3 * GSAP animations for the floating_boxes paragraph. 4 */ 5 6((Drupal, once) => { 7 'use strict'; 8 9 // ======================================================================== 10 // ANIMATION CONFIGURATION 11 // ======================================================================== 12 const config = { 13 wrapperSelector: '.paragraph--type--floating-boxes', 14 boxSelector: '.proof-point-box', 15 scrubSmoothing: 0, 16 17 // Exact pixel delay between boxes. 18 staggerDelayPx: 300, 19 20 // The Unpin Offset. 21 earlyUnpinPx: 200, 22 23 desktopBreakpoint: '(min-width: 1024px)' 24 }; 25 // ======================================================================== 26 27 let isObserverInitialized = false; 28 29 Drupal.behaviors.floatingBoxes = { 30 attach(context, settings) { 31 const wrappers = once('floatingBoxesAnimation', config.wrapperSelector, context); 32 33 if (!wrappers.length || typeof gsap === 'undefined' || typeof ScrollTrigger === 'undefined') { 34 return; 35 } 36 37 gsap.registerPlugin(ScrollTrigger); 38 39 if (!isObserverInitialized) { 40 let refreshTimer; 41 const layoutObserver = new ResizeObserver(() => { 42 clearTimeout(refreshTimer); 43 refreshTimer = setTimeout(() => { 44 ScrollTrigger.refresh(); 45 }, 50); 46 }); 47 layoutObserver.observe(document.body); 48 isObserverInitialized = true; 49 } 50 51 wrappers.forEach((wrapper) => { 52 const boxes = wrapper.querySelectorAll(config.boxSelector); 53 let mm = gsap.matchMedia(); 54 55 mm.add(config.desktopBreakpoint, () => { 56 let pinTrigger; 57 let animTl; 58 59 const buildAnimation = () => { 60 if (pinTrigger) pinTrigger.kill(); 61 if (animTl) animTl.kill(); 62 gsap.set(boxes, { clearProps: "all" }); 63 gsap.set(wrapper, { clearProps: "height" }); 64 65 const windowHeight = window.innerHeight; 66 67 // --- DYNAMIC CENTERING MATH --- 68 const header = wrapper.querySelector('.header-box'); 69 const headerCenterRelative = header.offsetTop + (header.offsetHeight / 2); 70 const wrapperTopPos = (windowHeight / 2) - headerCenterRelative; 71 72 // --- THE GAP ELIMINATOR --- 73 const exactWrapperHeight = windowHeight - wrapperTopPos; 74 gsap.set(wrapper, { height: exactWrapperHeight }); 75 76 // EXACT START POSITION: 77 const startY = windowHeight - wrapperTopPos; 78 79 gsap.set(boxes, { 80 top: 0, 81 y: startY 82 }); 83 84 // 1. Find the distance required for the tallest box to completely clear the screen natively 85 let maxTravelScroll = 0; 86 boxes.forEach((box) => { 87 const offScreenY = -wrapperTopPos - box.offsetHeight; 88 const travelNeeded = startY - offScreenY; 89 maxTravelScroll = Math.max(maxTravelScroll, travelNeeded); 90 }); 91 92 const numBoxes = boxes.length; 93 const maxStaggerDelay = config.staggerDelayPx * (numBoxes - 1); 94 95 // 2. The scroll duration if we wanted to wait for the LAST box to clear the screen 96 const fullScrollDistance = maxStaggerDelay + maxTravelScroll; 97 98 // 3. Subtract the early unpin offset to find out exactly when the pin actually ends 99 // (Math.max ensures we never get a negative scroll distance if earlyUnpinPx is set too high) 100 const actualPinScrollDistance = Math.max(fullScrollDistance - config.earlyUnpinPx, 100); 101 102 pinTrigger = ScrollTrigger.create({ 103 trigger: wrapper, 104 start: `top ${wrapperTopPos}px`, 105 end: `+=${actualPinScrollDistance}`, 106 pin: true, 107 refreshPriority: 10 108 }); 109 110 animTl = gsap.timeline({ 111 scrollTrigger: { 112 trigger: wrapper, 113 start: `top ${wrapperTopPos}px`, 114 end: `+=${actualPinScrollDistance}`, 115 scrub: config.scrubSmoothing === 0 ? true : config.scrubSmoothing, 116 } 117 }); 118
119 boxes.forEach((box, index) => { 120 const startScrollPx = index * config.staggerDelayPx; 121 122 // 4. Calculate exactly how many pixels of scroll are available for THIS box 123 // before the pin suddenly disappears 124 const availableScrollPx = actualPinScrollDistance - startScrollPx; 125 126 if (availableScrollPx > 0) { 127 // 5. To maintain exactly 1:1 speed, it must physically travel that exact same amount of pixels 128 const endY = startY - availableScrollPx; 129 130 animTl.to(box, { 131 y: endY, 132 ease: "none", 133 duration: availableScrollPx // Duration matches distance = 1:1 Speed 134 }, startScrollPx); 135 } 136 }); 137 138 ScrollTrigger.sort(); 139 }; 140 141 buildAnimation(); 142 ScrollTrigger.addEventListener("refreshInit", buildAnimation); 143 144 return () => { 145 ScrollTrigger.removeEventListener("refreshInit", buildAnimation); 146 if (pinTrigger) pinTrigger.kill(); 147 if (animTl) animTl.kill(); 148 gsap.set(boxes, { clearProps: "all" }); 149 gsap.set(wrapper, { clearProps: "height" }); 150 }; 151 }); 152 }); 153 } 154 }; 155})(Drupal, once);
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.