PageSourceSearch

https://www.hubspire.com/wp-content/themes/hubspire-2026/js/home-banner-video-expand.js

js hubspire.com collected 2026-10-02 17:36:31 UTC 6,854 bytes, 237 lines download raw bytes

1/**
2 * Home hero — pin `.home-banner` for scroll distance only: banner copy + row move up while the
3 * video scrubs from card size to 100vw×100vh (fixed to viewport). Extra scroll holds fullscreen
4 * before unpin so `.discovery-cta` follows naturally in document order below.
5 *
6 * Desktop only: wide viewport + fine pointer (mouse). Touch devices (hover: none, pointer: coarse)
7 * including iPad keep the static card size from CSS even at large widths.
8 *
9 * ScrollTrigger is created on `window` `load` (after Lenis + assets) so pin-spacer math matches
10 * layout; a short delayed `ScrollTrigger.refresh()` runs after native scroll restoration settles.
11 */
12(function () {
13  'use strict';
14
15  var section = document.querySelector('[data-home-banner-expand]');
16  if (!section) return;
17
18  var reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
19  var hasGsap = typeof window.gsap !== 'undefined' && typeof window.ScrollTrigger !== 'undefined';
20  var mqDesktop = window.matchMedia('(min-width: 1399px) and (hover: hover) and (pointer: fine)');
21  var ST = hasGsap ? window.ScrollTrigger : null;
22  /** @type {gsap.Context | null} */
23  var scrollCtx = null;
24  var scrollInstalled = false;
25
26  function playHeroVideo() {
27    var videoEl = section.querySelector('.home-banner__video-el');
28    if (videoEl && typeof videoEl.play === 'function') {
29      videoEl.play().catch(function () {});
30    }
31  }
32
33  function scheduleRefresh() {
34    if (!ST || !scrollInstalled) return;
35    if (scheduleRefresh._t) clearTimeout(scheduleRefresh._t);
36    scheduleRefresh._t = setTimeout(function () {
37      scheduleRefresh._t = null;
38      ST.refresh();
39    }, 100);
40  }
41
42  /**
43   * Ancestors with transform/filter/will-change break `position: fixed` (video card → viewport).
44   * `.fade-effect` uses transform; pin also applies transforms — use transform pin + reparent.
45   */
46  function pinOptionsForHero() {
47    return {
48      pinType: 'transform',
49      pinReparent: true
50    };
51  }
52
53  function teardownScrollEffects() {
54    if (scrollCtx) {
55      scrollCtx.revert();
56      scrollCtx = null;
57    }
58    scrollInstalled = false;
59
60    if (!hasGsap) return;
61
62    var videoWrap = section.querySelector('.home-banner__video-wrap');
63    var stack = section.querySelector('.home-banner__stack');
64    var aside = section.querySelector('.home-banner__aside');
65    if (videoWrap) window.gsap.set(videoWrap, { clearProps: 'all' });
66    if (stack) window.gsap.set(stack, { clearProps: 'marginTop' });
67    if (aside) window.gsap.set(aside, { clearProps: 'marginTop' });
68    if (ST) ST.refresh();
69  }
70
71  function initScrollEffects() {
72    if (scrollInstalled || !hasGsap || reducedMotion || !mqDesktop.matches) return;
73
74    var videoSlot = section.querySelector('.home-banner__video-slot');
75    var videoWrap = section.querySelector('.home-banner__video-wrap');
76    var stack = section.querySelector('.home-banner__stack');
77    var aside = section.querySelector('.home-banner__aside');
78    if (!videoSlot || !videoWrap) return;
79
80    playHeroVideo();
81    window.gsap.registerPlugin(ST);
82
83    function slotRect() {
84      return videoSlot.getBoundingClientRect();
85    }
86
87    var startBorderRadius = window.getComputedStyle(videoWrap).borderTopLeftRadius || '0px';
88
89    var expandDur = 1;
90    var holdDur = 0.65;
91    var vh = function () {
92      return window.innerHeight || 800;
93    };
94
95    function viewportHeightPx() {
96      var vv = window.visualViewport;
97      if (vv && typeof vv.height === 'number' && vv.height > 0) return Math.round(vv.height);
98      var h = window.innerHeight;
99      if (typeof h === 'number' && h > 0) return Math.round(h);
100      return 800;
101    }
102
103    var pinOpts = pinOptionsForHero();
104
105    scrollCtx = window.gsap.context(function () {
106      var tl = window.gsap.timeline({
107        defaults: { ease: 'none' }
108      });
109
110      tl.fromTo(
111        videoWrap,
112        {
113          position: 'fixed',
114          zIndex: 1020,
115          top: function () {
116            return slotRect().top;
117          },
118          left: function () {
119            return slotRect().left;
120          },
121          width: function () {
122            return slotRect().width;
123          },
124          height: function () {
125            return slotRect().height;
126          },
127          borderRadius: startBorderRadius,
128          immediateRender: false
129        },
130        {
131          top: 0,
132          left: 0,
133          width: '100vw',
134          height: function () {
135            return viewportHeightPx() + 2 + 'px';
136          },
137          borderRadius: 0,
138          immediateRender: false,
139          duration: expandDur
140        },
141        0
142      );
143
144      if (stack) {
145        tl.to(
146          stack,
147          {
148            marginTop: function () {
149              return -Math.min(vh() * 0.42, stack.offsetHeight + 40);
150            },
151            duration: expandDur
152          },
153          0
154        );
155      }
156
157      if (aside) {
158        tl.to(
159          aside,
160          {
161            marginTop: function () {
162              return -Math.min(vh() * 0.35, aside.offsetHeight + 24);
163            },
164            duration: expandDur
165          },
166          0
167        );
168      }
169
170      tl.to({}, { duration: holdDur });
171
172      ST.create({
173        animation: tl,
174        trigger: section,
175        start: 'top top',
176        end: function () {
177          return '+=' + String(Math.round(vh() * (1.45 + 0.95)));
178        },
179        scrub: true,
180        pin: true,
181        pinSpacing: true,
182        anticipatePin: 1,
183        invalidateOnRefresh: true,
184        pinType: pinOpts.pinType,
185        pinReparent: pinOpts.pinReparent
186      });
187    }, section);
188
189    scrollInstalled = true;
190    if (ST) ST.refresh();
191  }
192
193  function applyLayoutMode() {
194    if (reducedMotion || !hasGsap) {
195      playHeroVideo();
196      return;
197    }
198
199    if (mqDesktop.matches) {
200      initScrollEffects();
201    } else {
202      teardownScrollEffects();
203      playHeroVideo();
204    }
205  }
206
207  window.addEventListener('resize', scheduleRefresh, { passive: true });
208  if (window.visualViewport) {
209    window.visualViewport.addEventListener('resize', scheduleRefresh, { passive: true });
210  }
211  if (typeof ResizeObserver !== 'undefined' && document.body) {
212    var ro = new ResizeObserver(scheduleRefresh);
213    ro.observe(document.body);
214  }
215  if (typeof mqDesktop.addEventListener === 'function') {
216    mqDesktop.addEventListener('change', applyLayoutMode);
217  } else if (typeof mqDesktop.addListener === 'function') {
218    mqDesktop.addListener(applyLayoutMode);
219  }
220
221  function bootOnLoad() {
222    function start() {
223      applyLayoutMode();
224      setTimeout(function () {
225        if (ST && scrollInstalled) ST.refresh();
226      }, 220);
227    }
228
229    if (document.readyState === 'complete') {
230      start();
231    } else {
232      window.addEventListener('load', start, { once: true });
233    }
234  }
235
236  bootOnLoad();
237})();

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.