1// START GENAI 2class ScrollAnimations { 3 constructor() { 4 this.animationClasses = [ 5 'headerReveal1', 6 'subHeaderReveal', 7 'cardFlipReveal', 8 'cardReveal', 9 'blockReveal', 10 ]; 11 12 // Container attribute to identify parent elements that should be observed 13 this.containerAttribute = 'data-animation-container'; 14 15 this.observer = null; 16 this.dir = 'down'; 17 this.lastY = 0; 18 19 this.init(); 20 } 21 22 // Generate CSS once and cache it 23 generateStaggerCSS() { 24 // Prevent duplicate style injection 25 if (document.getElementById('scroll-animations-styles')) { 26 return; 27 } 28 29 let css = ''; 30 31 // Generate stagger CSS for headerReveal1 32 // We'll use a data attribute to store the total number of words 33 // and calculate delays dynamically in processHeaderRevealElements 34 for (let i = 1; i <= 100; i++) { 35 css += ` 36 .headerReveal1.scroll-down > p:nth-child(${i}) { 37 animation-delay: calc(var(--word-delay, 0ms) * ${i - 1}); 38 } 39 `; 40 } 41 42 // Generate stagger CSS for subHeaderReveal 43 for (let i = 1; i <= 50; i++) { 44 const delay = (i - 1) * 150; 45 css += ` 46 .subHeaderReveal.scroll-down > *:nth-child(${i}) { 47 animation-delay: ${delay}ms; 48 } 49 `; 50 } 51 52 // Generate stagger CSS for blockReveal 53 for (let i = 1; i <= 50; i++) { 54 const delay = (i - 1) * 200; 55 css += ` 56 .blockReveal.scroll-down > *:nth-child(${i}) { 57 animation-delay: ${delay}ms; 58 } 59 `; 60 } 61 62 // Generate stagger CSS for cardFlipReveal 63 for (let i = 1; i <= 20; i++) { 64 const delay = (i - 1) * 200; 65 css += ` 66 .cardFlipReveal.scroll-down:nth-of-type(${i}) { 67 animation-delay: ${delay}ms; 68 } 69 `; 70 } 71 72 // Generate stagger CSS for cardReveal 73 for (let i = 1; i <= 20; i++) { 74 const delay = (i - 1) * 200; 75 css += ` 76 .cardReveal.scroll-down:nth-of-type(${i}) { 77 animation-delay: ${delay}ms; 78 } 79 `; 80 } 81 82 // Create and inject style element with ID to prevent duplicates 83 const style = document.createElement('style'); 84 style.id = 'scroll-animations-styles'; 85 style.textContent = css; 86 document.head.appendChild(style); 87 } 88 89 /** 90 * Initialize the scroll animations 91 */ 92 init() { 93 // Wait for DOM to be ready 94 if (document.readyState === 'loading') { 95 document.addEventListener('DOMContentLoaded', () => this.componentDidLoad()); 96 } else { 97 this.componentDidLoad(); 98 } 99 } 100 101 /** 102 * Observe animation containers and individual animation elements for backward compatibility 103 */ 104 observeAnimationElements() { 105 // First, observe containers with the data-animation-container attribute 106 document.querySelectorAll(`[${this.containerAttribute}]:not(.observed)`).forEach(container => { 107 if (this.observer) { 108 this.observer.observe(container); 109 container.classList.add('observed'); 110 } 111 }); 112 113 // For backward compatibility, also observe individual animation elements 114 // that are not within a container 115 this.animationClasses.forEach(className => { 116 document.querySelectorAll(`.${className}:not(.observed)`).forEach(el => { 117 // Skip if this element is inside a container that's already being observed 118 if (el.closest(`[${this.containerAttribute}]`)) { 119 return; 120 } 121 122 if (this.observer) { 123 this.observer.observe(el); 124 el.classList.add('observed'); 125 } 126 }); 127 }); 128 } 129 130 /** 131 * Component lifecycle method - Called after the component has loaded 132 */ 133 componentDidLoad() { 134 // Generate CSS first 135 this.generateStaggerCSS(); 136 137 // Process elements 138 this.processHeaderRevealElements(); 139 this.processSubHeaderRevealElements(); 140 141 // Create observer once 142 this.observer = new IntersectionObserver( 143 entries => { 144 // First, process containers to ensure sequential animation within each container 145 const containersInView = []; 146 const individualElementsInView = []; 147
148 entries.forEach(entry => { 149 if (entry.isIntersecting) { 150 entry.target.classList.add('in-view'); 151 152 // Check if this is a container or an individual element 153 if (entry.target.hasAttribute(this.containerAttribute)) { 154 // Add container to the list for processing 155 containersInView.push(entry.target); 156 } else { 157 // For backward compatibility, collect individual elements 158 // Only if they're not inside a container that's already being observed 159 if (!entry.target.closest(`[${this.containerAttribute}]`)) { 160 individualElementsInView.push(entry.target); 161 } 162 } 163 } else { 164 entry.target.classList.remove('in-view'); 165 } 166 }); 167 168 // Process containers first 169 if (containersInView.length > 0) { 170 containersInView.forEach(container => { 171 this.processContainerAnimations(container); 172 }); 173 } 174 175 // Then process individual elements (for backward compatibility) 176 if (individualElementsInView.length > 0) { 177 this.processElementsWithPriorityAndOrder(individualElementsInView); 178 } 179 }, 180 { 181 root: null, 182 threshold: 0.1, // Trigger when 10% visible 183 rootMargin: '-10% 0px -20% 0px', 184 } 185 ); 186 187 // Simplified scroll listener - only track direction 188 window.addEventListener('scroll', () => { 189 this.dir = window.scrollY > this.lastY ? 'down' : 'up'; 190 this.lastY = window.scrollY; 191 }); 192 193 // Observe existing ones 194 this.observeAnimationElements(); 195 196 // Animate elements already in viewport on page load 197 setTimeout(() => { 198 this.animateInitialElements(); 199 }, 100); 200 201 // Mutation observer for dynamic content 202 if (typeof window !== 'undefined' && typeof window.MutationObserver !== 'undefined') { 203 const mo = new window.MutationObserver(() => { 204 this.processHeaderRevealElements(); 205 this.processSubHeaderRevealElements(); 206 this.observeAnimationElements(); 207 }); 208 mo.observe(document.body, { childList: true, subtree: true }); 209 } 210 } 211 212 /** 213 * Sorts elements by priority, then position (top, then left) and triggers their animation. 214 */ 215 processElementsWithPriorityAndOrder(elements) { 216 const sortedElements = elements 217 .filter(el => !el.getAttribute('data-animated')) // Process only unanimated elements 218 .sort((a, b) => { 219 const priorityA = this.getAnimationPriority(a); 220 const priorityB = this.getAnimationPriority(b); 221 222 // If priorities differ, sort by priority (higher number first) 223 if (priorityA !== priorityB) { 224 return priorityB - priorityA; 225 } 226 227 // If priorities are the same, sort by position 228 const rectA = a.getBoundingClientRect(); 229 const rectB = b.getBoundingClientRect(); 230 231 // Sort by vertical position first (top-to-bottom) 232 // A small tolerance handles elements that are practically on the same line 233 const verticalDifference = rectA.top - rectB.top; 234 if (Math.abs(verticalDifference) > 10) { 235 return verticalDifference; 236 } 237 238 // For elements on the same line, sort by horizontal position (left-to-right) 239 return rectA.left - rectB.left; 240 }); 241 242 // Trigger animations in the sorted order with a slight delay for a cascade effect 243 sortedElements.forEach((element, index) => { 244 setTimeout(() => { 245 this.triggerElementAnimation(element); 246 }, index * 100); // 100ms delay between each element in the sequence 247 }); 248 } 249 250 /** 251 * Returns a numerical priority for an animation class. Higher numbers animate first. 252 */ 253 getAnimationPriority(element) { 254 const className = this.getAnimationClass(element); 255 switch (className) { 256 case 'headerReveal1': 257 return 5; 258 case 'subHeaderReveal': 259 return 4; 260 case 'cardFlipReveal': 261 return 3; // High priority 262 case 'cardReveal': 263 return 2; 264 case 'blockReveal': 265 return 1; // Low priority 266 default: 267 return 0; 268 } 269 } 270 271 /** 272 * Process headerReveal1 elements to split words 273 */ 274 processHeaderRevealElements() { 275 document.querySelectorAll('.headerReveal1:not([data-processed])').forEach(element => { 276 const originalHTML = element.innerHTML; 277 element.setAttribute('data-original-html', originalHTML); 278 279 const paragraphs = element.querySelectorAll('p'); 280 let allWords = []; 281 282 if (paragraphs.length > 0) { 283 paragraphs.forEach(p => { 284 const words = (p.textContent || '').trim().split(/\s+/).filter(
284Boolean); 285 allWords.push(...words); 286 }); 287 } else { 288 allWords = (element.textContent || '').trim().split(/\s+/).filter(Boolean); 289 } 290 291 if (allWords.length > 0) { 292 // Calculate the delay per word to ensure total animation is 1 second 293 // We use (allWords.length - 1) because the first word has no delay 294 // and the last word should finish animating at 1000ms 295 const wordCount = allWords.length; 296 const totalAnimationDuration = 1000; // 1 second in ms 297 const animationDuration = 400; // Duration of each word's animation in ms 298 299 // Calculate delay between words to fit all animations within 1 second 300 // If there's only one word, delay is 0 301 const delayPerWord = 302 wordCount > 1 ? (totalAnimationDuration - animationDuration) / (wordCount - 1) : 0; 303 304 // Set the delay as a CSS variable on the element 305 element.style.setProperty('--word-delay', `${delayPerWord}ms`); 306 element.style.setProperty('--word-count', wordCount); 307 308 // Create elements for each word 309 element.innerHTML = ''; 310 allWords.forEach(word => { 311 const p = document.createElement('p'); 312 p.textContent = word; 313 element.appendChild(p); 314 }); 315 } 316 element.setAttribute('data-processed', 'true'); 317 }); 318 } 319 320 /** 321 * Process subHeaderReveal elements to add p tags if needed 322 */ 323 processSubHeaderRevealElements() { 324 document.querySelectorAll('.subHeaderReveal:not([data-processed])').forEach(element => { 325 element.setAttribute('data-original-html', element.innerHTML); 326 327 if (element.querySelectorAll('p').length === 0) { 328 const textContent = element.innerHTML.trim(); 329 if (textContent) { 330 const lines = textContent 331 .split(/<br\s*\/?>/i) 332 .map(line => line.trim()) 333 .filter(Boolean); 334 element.innerHTML = ''; 335 if (lines.length > 0) { 336 lines.forEach(line => { 337 const p = document.createElement('p'); 338 p.innerHTML = line; 339 element.appendChild(p); 340 }); 341 } else { 342 element.innerHTML = `<p>${textContent}</p>`; 343 } 344 } 345 } 346 element.setAttribute('data-processed', 'true'); 347 }); 348 } 349 350 /** 351 * Trigger animation for a single element 352 */ 353 triggerElementAnimation(element) { 354 if (element.getAttribute('data-animated')) { 355 return; 356 } 357 358 const className = this.getAnimationClass(element); 359 if (!className) return; 360 361 // const rect = element.getBoundingClientRect(); 362 363 // Always use 'scroll-down' to trigger the forward animation 364 element.classList.add('scroll-down'); 365 element.setAttribute('data-animated', 'true'); 366 } 367 368 /** 369 * Process animations for elements within a container 370 * Animates children in DOM order with sequential delays 371 */ 372 processContainerAnimations(container) { 373 // Skip if already animated 374 if (container.getAttribute('data-animated')) { 375 return; 376 } 377 378 // Find all animation elements within this container 379 const animationElements = []; 380 this.animationClasses.forEach(className => { 381 container.querySelectorAll(`.${className}`).forEach(element => { 382 if (!element.getAttribute('data-animated')) { 383 animationElements.push(element); 384 } 385 }); 386 }); 387 388 if (animationElements.length === 0) { 389 return; 390 } 391 392 // Sort elements by their position in the DOM 393 // This ensures we animate them in the order they appear in the document 394 const sortedElements = Array.from(animationElements).sort((a, b) => { 395 // Use compareDocumentPosition to determine the relative position in the DOM 396 const position = a.compareDocumentPosition(b); 397 398 // If b follows a in the document 399 if (position & window.Node.DOCUMENT_POSITION_FOLLOWING) { 400 return -1; // a comes first 401 } 402 403 // If b precedes a in the document 404 if (position & window.Node.DOCUMENT_POSITION_PRECEDING) { 405 return 1; // b comes first 406 } 407 408 return 0; // Same position (shouldn't happen with different elements) 409 }); 410 411 // Use a longer delay between animations to make the sequence more visible 412 const baseDelay = 1000; // ms between animations 413 414 // Animate elements sequentially with increasing delays
415 sortedElements.forEach((element, index) => { 416 const delay = index * baseDelay; 417 418 setTimeout(() => { 419 this.triggerElementAnimation(element); 420 }, delay); 421 }); 422 423 // Mark container as animated 424 container.setAttribute('data-animated', 'true'); 425 } 426 427 /** 428 * Animate elements that are already in viewport on page load 429 */ 430 animateInitialElements() { 431 // First, check for containers in viewport 432 const containersInView = []; 433 document.querySelectorAll(`[${this.containerAttribute}]`).forEach(container => { 434 const rect = container.getBoundingClientRect(); 435 const viewportHeight = window.innerHeight; 436 437 // Check if container is within the initial viewport 438 if (rect.top < viewportHeight && rect.bottom > 0) { 439 containersInView.push(container); 440 } 441 }); 442 443 // Process containers first 444 if (containersInView.length > 0) { 445 containersInView.forEach(container => { 446 this.processContainerAnimations(container); 447 }); 448 } 449 450 // For backward compatibility, also check individual elements 451 const individualElementsInView = []; 452 this.animationClasses.forEach(className => { 453 document.querySelectorAll(`.${className}`).forEach(element => { 454 // Skip if this element is inside a container 455 if (element.closest(`[${this.containerAttribute}]`)) { 456 return; 457 } 458 459 const rect = element.getBoundingClientRect(); 460 const viewportHeight = window.innerHeight; 461 // Check if element is within the initial viewport 462 if (rect.top < viewportHeight && rect.bottom > 0) { 463 individualElementsInView.push(element); 464 } 465 }); 466 }); 467 468 // Then process individual elements 469 if (individualElementsInView.length > 0) { 470 this.processElementsWithPriorityAndOrder(individualElementsInView); 471 } 472 } 473 474 /** 475 * Get the animation class of an element 476 */ 477 getAnimationClass(element) { 478 for (const className of this.animationClasses) { 479 if (element.classList.contains(className)) { 480 return className; 481 } 482 } 483 return null; 484 } 485 486 /** 487 * Revert element to original structure (optional - currently not used) 488 */ 489 revertToOriginalStructure(element) { 490 const originalHTML = element.getAttribute('data-original-html'); 491 if (originalHTML && !element.getAttribute('data-reverted')) { 492 element.innerHTML = originalHTML; 493 element.classList.remove('scroll-down'); 494 element.setAttribute('data-reverted', 'true'); 495 element.removeAttribute('data-processed'); 496 element.removeAttribute('data-animated'); 497 element.removeAttribute('data-original-html'); 498 } 499 } 500 501 /** 502 * Reset element for re-animation (if needed) 503 */ 504 resetElementForReanimation(element) { 505 element.removeAttribute('data-reverted'); 506 element.removeAttribute('data-processed'); 507 element.removeAttribute('data-animated'); 508 element.removeAttribute('data-last-dir'); 509 element.classList.remove('scroll-down'); 510 } 511} 512 513// Initialize when script loads 514new ScrollAnimations(); 515// END GENAI
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.