PageSourceSearch

https://www.vivocity.com.sg/wp-content/themes/twentynineteen-child/js/homepage-parallax.js

js vivocity.com.sg collected 2026-10-01 14:32:37 UTC 6,792 bytes, 188 lines download raw bytes

1(function() {
2  'use strict';
3
4  // Tunable values for parallax behavior and safety caps.
5  var config = {
6    desktopMaxTranslate: 80,
7    mobileMaxTranslate: 45,
8    intensity: 1.2,
9    baseScale: 1,
10    kidsVideoScale: 1.12,
11    minDelta: 0.15,
12    maxOverlayBarHeight: 220,
13    overflowSafetyPadding: 1
14  };
15
16  var items = Array.from(document.querySelectorAll('.homepageCategoryGrid .inner, .homepageCategoryGrid .videoParallaxMedia'));
17  // Sticky/fixed bars that can occlude visible viewport area.
18  var topBars = Array.from(document.querySelectorAll('#wpadminbar, #header-outer, header#top, #top, .main-navbar, .mainNavBar'));
19  var bottomBars = Array.from(document.querySelectorAll('#footer .footerMobile, .footerMobile, .cookieBanner'));
20  // Cache last translate to skip tiny paint updates.
21  var lastAppliedTranslate = new WeakMap();
22  // requestAnimationFrame token for scroll/resize throttling.
23  var frame = null;
24
25  // Utility clamp used for normalizing progress.
26  function clamp(value, min, max) {
27    return Math.max(min, Math.min(max, value));
28  }
29
30  // Match existing project mobile breakpoint.
31  function isMobileViewport() {
32    return window.innerWidth < 768;
33  }
34
35  // Use lower translation range on mobile.
36  function getMaxTranslate() {
37    return isMobileViewport() ? config.mobileMaxTranslate : config.desktopMaxTranslate;
38  }
39
40  // Prefer visualViewport height on mobile for better accuracy during browser chrome changes.
41  function getViewportHeight() {
42    if (isMobileViewport() && window.visualViewport) {
43      return Math.round(window.visualViewport.height);
44    }
45    return Math.round(window.innerHeight);
46  }
47
48  function getItemScale(item) {
49    // Kids video gets a slightly larger scale to preserve visible movement with safe overflow.
50    var isKidsVideo = item.classList.contains('videoParallaxMedia') && item.closest('.kidsThumbImg');
51    return isKidsVideo ? config.kidsVideoScale : config.baseScale;
52  }
53
54  // For image layers, move relative to the card frame (.img) rather than the inner media node itself.
55  function getMotionSource(item) {
56    if (item.parentElement && item.parentElement.classList.contains('img')) {
57      return item.parentElement;
58    }
59    return item;
60  }
61
62  function getOverlayOffset(bars, viewportHeight, edge) {
63    // Computes occupied area from fixed/sticky bars so parallax visibility is based on true viewport.
64    var occupied = 0;
65
66    bars.forEach(function(bar) {
67      if (!bar || typeof bar.getBoundingClientRect !== 'function') {
68        return;
69      }
70
71      var rect = bar.getBoundingClientRect();
72      var styles = window.getComputedStyle(bar);
73      var isFixed = styles.position === 'fixed' || styles.position === 'sticky' || bar.id === 'wpadminbar';
74
75      if (!isFixed || rect.height <= 0 || rect.height > config.maxOverlayBarHeight) {
76        return;
77      }
78
79      if (edge === 'top') {
80        if (rect.bottom <= 0 || rect.top > viewportHeight * 0.25) {
81          return;
82        }
83        occupied = Math.max(occupied, rect.bottom);
84        return;
85      }
86
87      if (rect.top >= viewportHeight || rect.bottom <= 0) {
88        return;
89      }
90
91      var overlap = Math.max(0, viewportHeight - Math.max(rect.top, 0));
92      occupied = Math.max(occupied, overlap);
93    });
94
95    return Math.max(0, occupied);
96  }
97
98  function getEffectiveViewport() {
99    // Effective viewport excludes top/bottom overlays that can hide content.
100    var viewportHeight = getViewportHeight() || window.innerHeight;
101    var topOffset = getOverlayOffset(topBars, viewportHeight, 'top');
102    var bottomOffset = getOverlayOffset(bottomBars, viewportHeight, 'bottom');
103    var bottom = Math.max(topOffset, viewportHeight - bottomOffset);
104    var height = Math.max(1, bottom - topOffset);
105
106    return {
107      top: topOffset,
108      bottom: bottom,
109      height: height,
110      center: topOffset + (height / 2)
111    };
112  }
113
114  function updateItem(item, viewport, maxTranslate) {
115    // Skip work when an item is fully outside effective viewport.
116    var motionRect = getMotionSource(item).getBoundingClientRect();
117    if (!motionRect.height || motionRect.bottom <= viewport.top || motionRect.top >= viewport.bottom) {
118      return;
119    }
120
121    var itemScale = getItemScale(item);
122    var itemRect = item.getBoundingClientRect();
123    // Progress is based on item center traversing the full visible travel (enter -> exit).
124    var elementCenter = motionRect.top + (motionRect.height / 2);
125    var travelDistance = (viewport.height + motionRect.height) / 2;
126    var progress = clamp((elementCenter - viewport.center) / Math.max(1, travelDistance), -1, 1);
127    // Clamp movement by available overflow to avoid revealing blank edges.
128    var baseRange = maxTranslate * config.intensity;
129    var renderedOverflow = (itemRect.height - motionRect.height) / 2;
130    var scaleOverflow = (motionRect.height * Math.max(0, itemScale - 1)) / 2;
131    var safeRange = Math.min(baseRange, Math.max(0, renderedOverflow + scaleOverflow - config.overflowSafetyPadding));
132    var translateY = progress * safeRange * -1;
133    var previousTranslate = lastAppliedTranslate.has(item) ? lastAppliedTranslate.get(item) : null;
134
135    // Skip sub-pixel-ish updates to reduce style churn.
136    if (previousTranslate !== null && Math.abs(previousTranslate - translateY) < config.minDelta) {
137      return;
138    }
139
140    lastAppliedTranslate.set(item, translateY);
141    // Final parallax transform.
142    item.style.transform = 'translate3d(0, ' + translateY + 'px, 0) scale(' + itemScale + ')';
143  }
144
145  function renderParallax() {
146    // RAF callback: compute viewport once, then update all parallax items.
147    frame = null;
148
149    var viewport = getEffectiveViewport();
150    var maxTranslate = getMaxTranslate();
151
152    items.forEach(function(item) {
153      updateItem(item, viewport, maxTranslate);
154    });
155  }
156
157  function scheduleParallax() {
158    // Throttle all scroll/resize triggers to one RAF render.
159    if (frame !== null) {
160      return;
161    }
162
163    frame = window.requestAnimationFrame(renderParallax);
164  }
165
166  function initParallax() {
167    // Maintain compatibility with template script that calls window.setTranslate().
168    if (!items.length) {
169      window.setTranslate = function() {};
170      return;
171    }
172
173    // Public hook used by homepage_template.php and any future manual refresh callers.
174    window.setTranslate = scheduleParallax;
175    // Keep parallax synced to scroll and viewport changes.
176    window.addEventListener('scroll', scheduleParallax, { passive: true });
177    window.addEventListener('resize', scheduleParallax);
178    window.addEventListener('orientationchange', scheduleParallax);
179
180    if (window.visualViewport) {
181      window.visualViewport.addEventListener('resize', scheduleParallax);
182    }
183
184    scheduleParallax();
185  }
186
187  initParallax();
188})();

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.