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