1/** 2 * Animated Hero Block - JavaScript 3 * 4 * State-driven hero animations. CSS owns all motion; JS only toggles 5 * classes, shuffles candy reveal order, and runs desktop parallax. 6 * 7 * @package StudioSwift 8 */ 9 10(function () { 11 'use strict'; 12 13 const DESKTOP_BREAKPOINT = '(min-width: 1199px)'; 14 const TABLET_BREAKPOINT = '(min-width: 768px) and (max-width: 1198px)'; 15 const LERP_FACTOR = 0.08; 16 const PARALLAX_RANGE = 20; 17 // Matches CSS entrance end: candy base delay + last stagger + pop settle (+ buffer). 18 const ENTRANCE_COMPLETE_MS = 2600; 19 20 /** 21 * Fisher-Yates shuffle (in-place). 22 * 23 * @param {Array} array 24 * @return {Array} 25 */ 26 function fisherYatesShuffle(array) { 27 for (let i = array.length - 1; i > 0; i -= 1) { 28 const j = Math.floor(Math.random() * (i + 1)); 29 [array[i], array[j]] = [array[j], array[i]]; 30 } 31 32 return array; 33 } 34 35 /** 36 * @param {HTMLElement} hero 37 */ 38 function AnimatedHero(hero) { 39 this.hero = hero; 40 this.reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; 41 this.desktopMedia = window.matchMedia(DESKTOP_BREAKPOINT); 42 this.tabletMedia = window.matchMedia(TABLET_BREAKPOINT); 43 44 this.candyGroups = []; 45 this.parallaxLayers = []; 46 this.products = []; 47 48 this.targetX = 0; 49 this.targetY = 0; 50 this.currentX = 0; 51 this.currentY = 0; 52 this.rafId = 0; 53 this.isParallaxActive = false; 54 this.completeTimer = 0; 55 56 this.onMouseMove = this.onMouseMove.bind(this); 57 this.onMediaChange = this.onMediaChange.bind(this); 58 this.onReducedMotionChange = this.onReducedMotionChange.bind(this); 59 this.updateParallax = this.updateParallax.bind(this); 60 } 61 62 AnimatedHero.prototype.cacheDom = function cacheDom() { 63 this.candyGroups = Array.from( 64 this.hero.querySelectorAll('.animated-hero__candy-group') 65 ); 66 this.parallaxLayers = Array.from( 67 this.hero.querySelectorAll('.js-animated-hero-parallax') 68 ); 69 this.products = Array.from( 70 this.hero.querySelectorAll('.js-animated-hero-product') 71 ); 72 }; 73 74 AnimatedHero.prototype.revealCandies = function revealCandies() { 75 const groupIndexes = [0, 1, 2, 3]; 76 const revealOrder = fisherYatesShuffle(groupIndexes.slice()); 77 78 revealOrder.forEach((groupIndex, revealIndex) => { 79 const selector = `.animated-hero__candy-group[data-group="${groupIndex + 1}"]`; 80 const groups = this.hero.querySelectorAll(selector); 81 82 groups.forEach((group) => { 83 group.classList.remove( 84 'animated-hero__candy-group--reveal-0', 85 'animated-hero__candy-group--reveal-1', 86 'animated-hero__candy-group--reveal-2', 87 'animated-hero__candy-group--reveal-3' 88 ); 89 group.classList.add(`animated-hero__candy-group--reveal-${revealIndex}`); 90 }); 91 }); 92 }; 93 94 AnimatedHero.prototype.revealProducts = function revealProducts() { 95 this.hero.classList.add('animated-hero--loaded'); 96 }; 97 98 /** 99 * Freeze entrance animations in their final state so breakpoint 100 * media-query swaps (768 / 992 / etc.) cannot replay them. 101 */ 102 AnimatedHero.prototype.markEntranceComplete = function markEntranceComplete() { 103 this.hero.classList.add('animated-hero--complete'); 104 }; 105 106 AnimatedHero.prototype.scheduleEntranceComplete = function scheduleEntranceComplete(delayMs) { 107 if (this.completeTimer) { 108 window.clearTimeout(this.completeTimer); 109 this.completeTimer = 0; 110 } 111 112 if (delayMs <= 0) { 113 this.markEntranceComplete(); 114 return; 115 } 116 117 this.completeTimer = window.setTimeout(() => { 118 this.completeTimer = 0; 119 this.markEntranceComplete(); 120 }, delayMs); 121 }; 122 123 AnimatedHero.prototype.onMouseMove = function onMouseMove(event) { 124 const rect = this.hero.getBoundingClientRect(); 125 const relativeX = (event.clientX - rect.left) / rect.width - 0.5; 126 const relativeY = (event.clientY - rect.top) / rect.height - 0.5; 127 128 this.targetX = relativeX * PARALLAX_RANGE; 129 this.targetY = relativeY * PARALLAX_RANGE; 130 }; 131 132 AnimatedHero.prototype.updateParallax = function updateParallax() { 133 this.rafId = 0; 134 135 this.currentX += (this.targetX - this.currentX) * LERP_FACTOR; 136 this.currentY += (this.targetY - this.currentY) * LERP_FACTOR; 137 138 this.products.forEach((product, index) => { 139 const layer = this.parallaxLayers[index]; 140 141 if (!layer) { 142 return; 143 } 144 145 const multiplier = parseFloat(product.dataset.parallaxMultiplier || '0.45'); 146 const x = this.currentX * multiplier; 147 const y = this.currentY * multiplier; 148 149 layer.style.setProperty('--parallax-x', `${x.toFixed(2)}px`); 150 layer.style.setProperty('--parallax-y', `${y.toFixed(2)}px`); 151 }); 152 153 if (this.isParallaxActive) { 154 this.rafId = window.requestAnimationFrame(this.updateParallax); 155 } 156 }; 157 158 AnimatedHero.prototype.startParallaxLoop = function startParallaxLoop() { 159 if (this.rafId) { 160 return; 161 } 162 163 this.rafId = window.requestAnimationFrame(this.updateParallax); 164 }; 165 166 AnimatedHero.prototype.setupMouseTracking = function setupMouseTracking() { 167 if (this.reducedMotion || !this.desktopMedia.matches) { 168 return; 169 } 170 171 this.isParallaxActive = true; 172 this.hero.addEventListener('mousemove', this.onMouseMove, { passive: true }); 173 this.startParallaxLoop(); 174 }; 175 176 AnimatedHero.prototype.teardownMouseTracking = function teardownMouseTracking() { 177 this.isParallaxActive = false;
178 this.hero.removeEventListener('mousemove', this.onMouseMove); 179 180 if (this.rafId) { 181 window.cancelAnimationFrame(this.rafId); 182 this.rafId = 0; 183 } 184 185 this.targetX = 0; 186 this.targetY = 0; 187 this.currentX = 0; 188 this.currentY = 0; 189 190 this.parallaxLayers.forEach((layer) => { 191 layer.style.setProperty('--parallax-x', '0px'); 192 layer.style.setProperty('--parallax-y', '0px'); 193 }); 194 }; 195 196 AnimatedHero.prototype.onMediaChange = function onMediaChange() { 197 this.teardownMouseTracking(); 198 199 if (this.desktopMedia.matches && !this.reducedMotion) { 200 this.setupMouseTracking(); 201 } 202 }; 203 204 AnimatedHero.prototype.onReducedMotionChange = function onReducedMotionChange() { 205 this.reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; 206 207 if (this.reducedMotion) { 208 this.teardownMouseTracking(); 209 this.hero.classList.add('animated-hero--loaded'); 210 this.scheduleEntranceComplete(0); 211 return; 212 } 213 214 this.onMediaChange(); 215 }; 216 217 AnimatedHero.prototype.bindEvents = function bindEvents() { 218 this.desktopMedia.addEventListener('change', this.onMediaChange); 219 this.tabletMedia.addEventListener('change', this.onMediaChange); 220 221 const reducedMotionMedia = window.matchMedia('(prefers-reduced-motion: reduce)'); 222 reducedMotionMedia.addEventListener('change', this.onReducedMotionChange); 223 this.reducedMotionMedia = reducedMotionMedia; 224 }; 225 226 AnimatedHero.prototype.init = function init() { 227 this.cacheDom(); 228 this.bindEvents(); 229 this.revealCandies(); 230 this.revealProducts(); 231 232 if (this.reducedMotion) { 233 this.scheduleEntranceComplete(0); 234 return; 235 } 236 237 this.scheduleEntranceComplete(ENTRANCE_COMPLETE_MS); 238 this.setupMouseTracking(); 239 }; 240 241 AnimatedHero.prototype.destroy = function destroy() { 242 this.teardownMouseTracking(); 243 244 if (this.completeTimer) { 245 window.clearTimeout(this.completeTimer); 246 this.completeTimer = 0; 247 } 248 249 if (this.desktopMedia) { 250 this.desktopMedia.removeEventListener('change', this.onMediaChange); 251 } 252 253 if (this.tabletMedia) { 254 this.tabletMedia.removeEventListener('change', this.onMediaChange); 255 } 256 257 if (this.reducedMotionMedia) { 258 this.reducedMotionMedia.removeEventListener('change', this.onReducedMotionChange); 259 } 260 261 this.hero.classList.remove('animated-hero--loaded', 'animated-hero--complete'); 262 263 this.candyGroups.forEach((group) => { 264 group.classList.remove( 265 'animated-hero__candy-group--reveal-0', 266 'animated-hero__candy-group--reveal-1', 267 'animated-hero__candy-group--reveal-2', 268 'animated-hero__candy-group--reveal-3' 269 ); 270 }); 271 272 delete this.hero._animatedHeroInstance; 273 delete this.hero.dataset.animatedHeroInit; 274 }; 275 276 /** 277 * Initialize a single hero instance. 278 * 279 * @param {HTMLElement} hero 280 */ 281 function initAnimatedHeroInstance(hero) { 282 if (!hero) { 283 return; 284 } 285 286 if (hero.dataset.animatedHeroInit === 'true' && hero._animatedHeroInstance) { 287 hero._animatedHeroInstance.destroy(); 288 } 289 290 const instance = new AnimatedHero(hero); 291 hero.dataset.animatedHeroInit = 'true'; 292 hero._animatedHeroInstance = instance; 293 instance.init(); 294 } 295 296 function initAnimatedHero() { 297 document.querySelectorAll('.js-animated-hero').forEach(initAnimatedHeroInstance); 298 } 299 300 if (document.readyState === 'loading') { 301 document.addEventListener('DOMContentLoaded', initAnimatedHero); 302 } else { 303 initAnimatedHero(); 304 } 305 306 if (window.acf) { 307 window.acf.addAction('render_block_preview/type=animated-hero', initAnimatedHero); 308 } 309})();
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.