PageSourceSearch

https://www.vistra.com/themes/custom/vistra2024/js/floating-boxes.js?tm819w

js vistra.com collected 2026-10-02 03:52:41 UTC 5,446 bytes, 155 lines download raw bytes

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.