1/* global gsap, ScrollTrigger, imagesLoaded */ 2(function () { 3 const { 4 mergeObjects, 5 onResize, 6 matchMedia, 7 prefersReducedMotion, 8 isBuilder 9 } = BreakdanceFrontend.utils; 10 11 // ScrollTrigger.refresh(true) uses a "safe" code path that skips the 12 // plugin's lazy self-registration, so it reads `document` off an 13 // uninitialized internal and throws 14 // "Cannot read properties of undefined (reading 'fullscreenElement')" 15 // when it runs before gsap.registerPlugin(ScrollTrigger). This can happen 16 // because gsap and ScrollTrigger are loaded async, so ScrollTrigger may 17 // evaluate before gsap and never auto-register. Only refresh once an 18 // animation instance has registered the plugin; if it hasn't, there are no 19 // ScrollTrigger instances to refresh anyway. gsap.core.globals() with no 20 // args reads the registered globals without mutating them. 21 function isScrollTriggerRegistered() { 22 return ( 23 typeof gsap !== 'undefined' && 24 typeof ScrollTrigger !== 'undefined' && 25 !!gsap.core?.globals?.().ScrollTrigger 26 ); 27 } 28 29 function refreshScrollTrigger() { 30 if (!isScrollTriggerRegistered()) return; 31 ScrollTrigger.refresh(true); 32 } 33 34 class BreakdanceEntrance { 35 enabledClass = 'breakdance-animation-enabled'; 36 beforeClass = 'is-before'; 37 animatingClass = 'is-animating'; 38 completedClass = 'is-animated'; 39 40 defaultOptions = { 41 animation_type: null, 42 duration: { number: 500, unit: 'ms', style: '500ms' }, 43 delay: { number: 0, unit: 'ms', style: '0ms' }, 44 advanced: { 45 distance: { number: 100, unit: "px", style: "100px" }, 46 offset: { number: 0, unit: 'px', style: '0px' }, 47 ease: 'power1.out', 48 anchorPlacement: 'top bottom', 49 once: false, 50 disable_at: null 51 } 52 }; 53 54 delay = 0; 55 initialized = false; 56 57 constructor(selector, options) { 58 gsap.registerPlugin(ScrollTrigger); 59 60 this.cleanup = this.cleanup.bind(this); 61 this.reset = this.reset.bind(this); 62 63 this.selector = selector; 64 this.options = mergeObjects(this.defaultOptions, options); 65 this.rootEl = document.documentElement; 66 67 this.init(); 68 } 69 70 getAnimations() { 71 const distance = this.options.advanced.distance.style; 72 const unit = this.options.advanced.distance.unit; 73 74 return { 75 fade: [ 76 { autoAlpha: 0 }, 77 { autoAlpha: 1 } 78 ], 79 80 slideUp: [ 81 { y: distance }, 82 { y: `0${unit}` } 83 ], 84 slideDown: [ 85 { y: `-=${distance}` }, 86 { y: `0${unit}` } 87 ], 88 slideLeft: [ 89 { x: `-=${distance}` }, 90 { x: `0${unit}` } 91 ], 92 slideRight: [ 93 { x: distance }, 94 { x: `0${unit}` } 95 ], 96 97 flipUp: [ 98 { perspective: 2500, rotateX: `-=${distance}` }, 99 { rotateX: `0${unit}` } 100 ], 101 102 flipDown: [ 103 { perspective: 2500, rotateX: distance }, 104 { rotateX: `0${unit}` } 105 ], 106 flipLeft: [ 107 { perspective: 2500, rotateY: `-=${distance}` }, 108 { rotateY: `0${unit}` } 109 ], 110 flipRight: [ 111 { perspective: 2500, rotateY: distance }, 112 { rotateY: `0${unit}` } 113 ], 114 115 zoomIn: [ 116 { scale: 0.6 }, 117 { scale: 1 } 118 ], 119 zoomOut: [ 120 { scale: 1.2 }, 121 { scale: 1 } 122 ], 123 }; 124 } 125 126 canAnimate() { 127 const breakpoint = this.options.advanced.disable_at; 128 if (!breakpoint) return true; 129 return !matchMedia(breakpoint); 130 } 131 132 getDuration(value) { 133 if (!value) return value; 134 if (value.unit === 's') return value.number; 135 return value.number / 1000; // Convert MS to S 136 } 137 138 cleanup() { 139 // Clear all inline styles, otherwise they will throw off animations for images. 140 gsap.set(this.element, { clearProps: 'all' }); 141 } 142 143 reset(element) { 144 const el = element || this.element; 145 el.classList.add(this.beforeClass); 146 el.classList.remove(this.completedClass); 147 el.classList.remove(this.animatingClass); 148 } 149 150 getOffset(offset, anchor) { 151 const defaultOffset = 120; 152 153 // Top-bottom placement looks terrible if the element is animated with offset zero. 154 if (anchor === 'top bottom' && offset.number === 0) { 155 return defaultOffset; 156 } 157 158 return offset.number; 159 } 160 161 createTween() { 162 const type = this.options.animation_type; 163 const animations = this.getAnimations(); 164 165 if (!animations[type]) { 166 console.error(`[ENTRANCE] The selected ${type}
166 animation is invalid.`); 167 return; 168 } 169 170 const [from, to] = animations[type]; 171 172 const { ease, once, anchorPlacement } = this.options.advanced; 173 const duration = this.getDuration(this.options.duration); 174 const delay = this.getDuration(this.options.delay) + this.delay; 175 const offset = this.getOffset(this.options.advanced.offset, anchorPlacement); 176 const [elementStart, viewportStart] = anchorPlacement.split(' '); 177 178 const anim = gsap.timeline({ 179 delay, 180 paused: true 181 }); 182 183 this.element.classList.add(this.beforeClass); 184 185 if (isBuilder()) { 186 this.hideEl(); 187 } 188 189 this.startTrigger = ScrollTrigger.create({ 190 trigger: this.element, 191 start: `${elementStart}+=${offset} ${viewportStart}`, 192 toggleActions: "play none none none", 193 once, 194 onEnter: () => anim.play() 195 }); 196 197 anim.fromTo(this.element, { 198 ...from, 199 autoAlpha: 0, 200 }, 201 { 202 ...to, 203 duration, 204 delay, 205 ease, 206 autoAlpha: 1, 207 clearProps: 'all', 208 immediateRender: false, 209 onStart: () => { 210 this.element.classList.add(this.animatingClass); 211 this.element.classList.remove(this.beforeClass); 212 }, 213 onComplete: () => { 214 this.element.classList.remove(this.animatingClass); 215 this.element.classList.add(this.completedClass); 216 }, 217 onReverseComplete: () => { 218 this.element.classList.add(this.beforeClass); 219 this.element.classList.remove(this.animatingClass); 220 }, 221 }); 222 223 if (!once) { 224 this.goToBeginningOnReverse(anim); 225 } 226 227 return anim; 228 } 229 230 goToBeginningOnReverse(animation) { 231 // Reset animation to the beginning once its goes offscreen at the bottom. 232 this.endTrigger = ScrollTrigger.create({ 233 trigger: this.element, 234 start: `top-=10 bottom`, 235 onLeaveBack: () => { 236 // Reset the animation to the beginning. 237 animation.pause(0); 238 239 // Clear all inline styles. 240 this.cleanup(); 241 242 // Trick the builder into making the element selectable. 243 // This is needed because activating the element sets its visibility to hidden. 244 if (isBuilder()) { 245 this.hideEl(); 246 } 247 248 this.reset(); 249 } 250 }); 251 } 252 253 hideEl() { 254 gsap.set(this.element, { autoAlpha: 0 }); 255 } 256 257 update(options) { 258 this.options = mergeObjects(this.defaultOptions, options); 259 this.destroy(); 260 this.init(); 261 } 262 263 replay(delay = 0) { 264 this.destroy(); 265 this.reset(); 266 this.delay = delay; 267 this.init(); 268 } 269 270 destroy() { 271 this.initialized = false; 272 273 if (!this.element) return; 274 275 this.element.classList.add(this.completedClass); 276 277 if (!this.tween) return; 278 279 this.tween.kill(); 280 this.tween = null; 281 this.startTrigger?.kill(); 282 this.endTrigger?.kill(); 283 284 // Remove all inline styles 285 gsap.set(this.element, { clearProps: 'all' }); 286 } 287 288 refresh() { 289 ScrollTrigger.refresh(); 290 } 291 292 initTween() { 293 if (this.initialized) return; 294 this.initialized = true; 295 296 this.element.classList.remove(this.completedClass); 297 this.tween = this.createTween(); 298 } 299 300 initOrDestroy() { 301 if (this.canAnimate()) { 302 this.initTween(); 303 } else { 304 this.destroy(); 305 } 306 } 307 308 init() { 309 if (!this.options.animation_type) return; 310 311 this.element = document.querySelector(this.selector); 312 this.element.bdAnim = this; 313 314 if (prefersReducedMotion()) { 315 this.element.classList.add(this.completedClass); 316 return; 317 } 318 319 onResize(() => this.initOrDestroy()); 320 this.rootEl.classList.add(this.enabledClass); 321 } 322 323 static autoload() { 324 const loaded = imagesLoaded(document.body); 325 326 // Listen for images to be loaded. 327 loaded.on("always", () => { 328 // Refresh all instances. 329 const event = new Event("breakdance_refresh_animations", { bubbles: true }); 330 document.dispatchEvent(event); 331 332 // Refresh ScrollTrigger only once. 333 refreshScrollTrigger(); 334 }); 335 336 // Play event 337 addEventListener("breakdance_play_animations", (event) => { 338 const target = event.target === window ? document.body : event.target; 339 340 const nodes = [ 341 target, 342 ...target.querySelectorAll("[data-entrance]") 343 ]; 344
345 nodes.forEach((el) => el.bdAnim?.replay(event?.detail?.delay)); 346 }); 347 348 // Cleanup event 349 addEventListener("breakdance_refresh_animations", (event) => { 350 const target = event.target === window ? document.body : event.target; 351 352 const nodes = [ 353 target, 354 ...target.querySelectorAll("[data-entrance]") 355 ]; 356 357 nodes.forEach((el) => el.bdAnim?.cleanup()); 358 }); 359 360 addEventListener("breakdance_reset_animations", (event) => { 361 const target = event.target === window ? document.body : event.target; 362 363 const nodes = [ 364 target, 365 ...target.querySelectorAll("[data-entrance]") 366 ]; 367 368 nodes.forEach((el) => el.bdAnim?.reset(el)); 369 }); 370 } 371 372 static dontLetScrollTriggerMutateScrollPosition() { 373 window.addEventListener("load", () => { 374 if (!location.hash) return; 375 if (window.bdeAnimationScrolled) return; 376 const scrollElem = document.querySelector(location.hash); 377 if (!scrollElem) return; 378 379 // Safari fix: Scroll to the element after the next repaint. 380 requestAnimationFrame(() => { 381 scrollElem.scrollIntoView({ 382 behavior: "smooth", 383 }); 384 }); 385 386 // Prevent scrolling to the same element twice. 387 window.bdeAnimationScrolled = true; 388 }); 389 } 390 } 391 392 window.BreakdanceEntrance = BreakdanceEntrance; 393 394 BreakdanceEntrance.autoload(); 395 BreakdanceEntrance.dontLetScrollTriggerMutateScrollPosition(); 396}());
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.