1/* global gsap, ScrollTrigger, imagesLoaded */ 2(function () { 3 const { 4 is, 5 matchMedia, 6 toArray, 7 debounce, 8 onResize, 9 mergeObjects, 10 prefersReducedMotion 11 } = BreakdanceFrontend.utils; 12 13 // ScrollTrigger.refresh(true) uses a "safe" code path that skips the 14 // plugin's lazy self-registration, so it reads `document` off an 15 // uninitialized internal and throws 16 // "Cannot read properties of undefined (reading 'fullscreenElement')" 17 // when it runs before gsap.registerPlugin(ScrollTrigger). This can happen 18 // because gsap and ScrollTrigger are loaded async, so ScrollTrigger may 19 // evaluate before gsap and never auto-register. Only refresh once an 20 // animation instance has registered the plugin; if it hasn't, there are no 21 // ScrollTrigger instances to refresh anyway. gsap.core.globals() with no 22 // args reads the registered globals without mutating them. 23 function isScrollTriggerRegistered() { 24 return ( 25 typeof gsap !== 'undefined' && 26 typeof ScrollTrigger !== 'undefined' && 27 !!gsap.core?.globals?.().ScrollTrigger 28 ); 29 } 30 31 function refreshScrollTrigger() { 32 if (!isScrollTriggerRegistered()) return; 33 ScrollTrigger.refresh(true); 34 } 35 36 class BreakdanceParallax { 37 tweens = []; 38 enabledClass = 'breakdance-animation-enabled'; 39 animatingClass = 'is-animating'; 40 41 defaultOptions = { 42 enabled: false, 43 opacity: null, 44 rotation: null, 45 scale: null, 46 x: null, 47 y: null, 48 blur: null, 49 background_color: null, 50 advanced: { 51 ease: 'linear', 52 disable_at: null, 53 relative_to: 'viewport', 54 origin: null, 55 debug: false 56 } 57 } 58 59 initialized = false; 60 61 constructor(selector, options) { 62 gsap.registerPlugin(ScrollTrigger); 63 64 this.createTween = this.createTween.bind(this); 65 this.update = debounce(this._update, 100); 66 67 this.selector = selector; 68 this.options = mergeObjects(this.defaultOptions, options); 69 this.rootEl = document.documentElement; 70 71 this.init(); 72 } 73 74 parseValue(value) { 75 if (!value) return value; 76 if (typeof value === 'string' && value.startsWith('var(')) { 77 // CSS variable 78 const cssVar = value.replace(/var\(([^)]+)\)/, '$1'); 79 return getComputedStyle(this.element).getPropertyValue(cssVar); 80 } 81 // Unit value is an object. 82 return typeof value === 'object' ? value.style : value; 83 } 84 85 parseAnchors(trigger) { 86 const [vBottom, vTop] = trigger || [0, 100]; 87 88 return { 89 bottom: 100 - vBottom, 90 top: 100 - vTop 91 } 92 } 93 94 getTrigger(relativeTo = 'viewport', selector) { 95 if (relativeTo === 'page') { 96 return document.documentElement; 97 } else if (relativeTo === 'custom' && selector) { 98 try { 99 return document.querySelector(selector); 100 } catch (e) {} 101 } 102 103 return this.element; 104 } 105 106 getScrollTriggerObject(values) { 107 const { bottom, top } = this.parseAnchors(values.trigger); 108 const { debug, scrub, relative_to, relative_selector } = this.options.advanced; 109 const speed = this.getDuration(scrub) || true; 110 let delayedCall; 111 112 const trigger = this.getTrigger(relative_to, relative_selector); 113 114 return { 115 trigger: trigger, 116 start: `top ${bottom}%`, // Element x Viewport 117 end: `bottom ${top}%`, // Element x Viewport 118 scrub: speed, 119 markers: debug, 120 toggleClass: 'is-parallax-active', 121 onUpdate: (self) => { 122 if (self.getVelocity() === 0) return; 123 this.element.classList.add(this.animatingClass); 124 125 if (delayedCall) delayedCall.kill(); 126 delayedCall = gsap.delayedCall(0.3, () => { 127 this.element.classList.remove(this.animatingClass); 128 }); 129 } 130 }; 131 } 132 133 createTween([prop, values]) { 134 const cssProp = prop.replace(/_/g, '-'); 135 const ease = this.options.advanced.ease; 136 const scrollTrigger = this.getScrollTriggerObject(values); 137 const startValue = this.parseValue(values.start); 138 const middleValue = this.parseValue(values.middle); 139 const endValue = this.parseValue(values.end); 140 141 const from = { [cssProp]: startValue, ease }; 142 const middle = { [cssProp]: middleValue, ease }; 143 const to = { [cssProp]: endValue, ease }; 144 145 const tl = gsap.timeline({ scrollTrigger }); 146 147 const startEndSteps = !is.nil(startValue) && !is.nil(endValue); 148 const allSteps = startEndSteps && !is.nil(middleValue); 149 150 if (allSteps) { 151 // Start -> Middle -> End 152 tl.fromTo(this.element, from, middle); 153 tl.to(this.element, to); 154 } else if (startEndSteps) { 155 // Start -> End 156 tl.fromTo(this.element, from, to); 157 } else if (!is.nil(startValue)) { 158 // Start -> Original Value 159 tl.from(this.element, from); 160 } else { 161 // Original Value -> End 162 tl.to(this.element, to); 163 } 164 165 return tl; 166 } 167 168 canAnimate() { 169 const breakpoint = this.options.advanced.disable_at; 170 if (!breakpoint) return true; 171 return !matchMedia(breakpoint); 172 } 173 174 getDuration(value) { 175 if (!value) return value; 176 if (value.unit === 's') return value.number; 177 return value.number / 1000; // Convert MS to S 178 } 179 180 initTweens() { 181 if (this.initialized) return; 182 this.initialized = true; 183 184 const { origin } = this.options.advanced; 185 186 if (origin) { 187 gsap.set(this.element, { 188 transformOrigin: `${origin.x}% ${origin.y}%` 189 }); 190 } 191 192 this.tweens = Object.entries(this.options) 193 .filter(([, obj]) => !is.nil(obj?.start) || !is.nil(obj?.end)) 194 .map(this.createTween); 195 } 196 197 removeDebugMarkers() { 198 // Remove ScrollTrigger markers 199 const markers = [ 200 'start', 201 'end', 202 'scroller-start', 203 'scroller-end' 204 ]; 205
206 markers.forEach((suffix) => { 207 const className = `.gsap-marker-${suffix}`; 208 toArray(className).forEach((elem) => elem.remove()); 209 }); 210 } 211 212 _update(options) { 213 this.options = mergeObjects(this.defaultOptions, options); 214 this.destroy(); 215 this.init(); 216 } 217 218 destroyTweens() { 219 if (!this.element) return; 220 if (!this.tweens) return; 221 222 this.tweens.forEach((tween) => { 223 // Killing the tween should automatically kill scrollTrigger, 224 // but that's always not the case for some reason. 225 tween.kill(); 226 tween.scrollTrigger?.kill(); 227 }); 228 229 this.tweens = []; 230 231 // Remove all inline styles 232 gsap.set(this.element, { 233 clearProps: 'all' 234 }); 235 236 this.initialized = false; 237 } 238 239 refresh() { 240 refreshScrollTrigger(); 241 } 242 243 destroy() { 244 this.destroyTweens(); 245 this.removeDebugMarkers(); 246 } 247 248 initOrDestroy() { 249 if (this.canAnimate()) { 250 this.initTweens(); 251 } else { 252 this.destroyTweens(); 253 } 254 } 255 256 init() { 257 if (!this.options.enabled) return; 258 if (prefersReducedMotion()) return; 259 260 this.element = document.querySelector(this.selector); 261 262 onResize(() => this.initOrDestroy()); 263 264 this.rootEl.classList.add(this.enabledClass); 265 } 266 267 static autoload() { 268 const loaded = imagesLoaded(document.body); 269 270 loaded.on('always', () => { 271 // Refresh all instances. 272 const event = new Event("breakdance_refresh_animations", { bubbles: true }); 273 document.dispatchEvent(event); 274 275 // Refresh ScrollTrigger only once. 276 refreshScrollTrigger(); 277 }); 278 } 279 } 280 281 window.BreakdanceParallax = BreakdanceParallax; 282 283 // Autoload 284 BreakdanceParallax.autoload(); 285}());
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.