1/* ============================================ 2 TOM RATH â ANIMATION ENGINE v2.0 3 GSAP-Level Scroll Animations, Text Splitting, 4 Magnetic Cursor, Parallax, Counters 5 ============================================ */ 6 7(function() { 8 'use strict'; 9 10 // âââ SCROLL PROGRESS BAR âââ 11 const progressBar = document.querySelector('.scroll-progress'); 12 if (progressBar) { 13 window.addEventListener('scroll', () => { 14 const scrollTop = window.scrollY; 15 const docHeight = document.documentElement.scrollHeight - window.innerHeight; 16 const progress = docHeight > 0 ? scrollTop / docHeight : 0; 17 progressBar.style.transform = `scaleX(${progress})`; 18 }, { passive: true }); 19 } 20 21 // âââ NAVIGATION âââ 22 const nav = document.querySelector('.nav'); 23 const hamburger = document.querySelector('.nav__hamburger'); 24 const mobileOverlay = document.querySelector('.mobile-overlay'); 25 26 if (nav) { 27 window.addEventListener('scroll', () => { 28 nav.classList.toggle('scrolled', window.scrollY > 60); 29 }, { passive: true }); 30 } 31 32 if (hamburger && mobileOverlay) { 33 function toggleMenu() { 34 hamburger.classList.toggle('active'); 35 mobileOverlay.classList.toggle('active'); 36 const isOpen = mobileOverlay.classList.contains('active'); 37 document.body.style.overflow = isOpen ? 'hidden' : ''; 38 hamburger.setAttribute('aria-expanded', isOpen ? 'true' : 'false'); 39 if (isOpen) { 40 const firstLink = mobileOverlay.querySelector('a'); 41 if (firstLink) firstLink.focus(); 42 } 43 } 44 45 hamburger.addEventListener('click', toggleMenu); 46 47 // Keyboard support: Enter and Space to toggle 48 hamburger.addEventListener('keydown', (e) => { 49 if (e.key === 'Enter' || e.key === ' ') { 50 e.preventDefault(); 51 toggleMenu(); 52 } 53 }); 54 55 // H3: Escape key to close mobile overlay 56 document.addEventListener('keydown', (e) => { 57 if (e.key === 'Escape' && mobileOverlay.classList.contains('active')) { 58 toggleMenu(); 59 hamburger.focus(); 60 } 61 }); 62 63 mobileOverlay.querySelectorAll('a').forEach(link => { 64 link.addEventListener('click', () => { 65 hamburger.classList.remove('active'); 66 mobileOverlay.classList.remove('active'); 67 document.body.style.overflow = ''; 68 }); 69 }); 70 } 71 72 // âââ SCROLL REVEAL (IntersectionObserver) âââ 73 const revealObserver = new IntersectionObserver((entries) => { 74 entries.forEach(entry => { 75 if (entry.isIntersecting) { 76 entry.target.classList.add('revealed'); 77 revealObserver.unobserve(entry.target); 78 } 79 }); 80 }, { 81 threshold: 0.1, 82 rootMargin: '0px 0px -80px 0px' 83 }); 84 85 document.querySelectorAll('[data-reveal], [data-stagger]').forEach(el => { 86 revealObserver.observe(el); 87 }); 88 89 // âââ TEXT SPLITTING ANIMATION âââ 90 document.querySelectorAll('[data-split]').forEach(el => { 91 const text = el.textContent; 92 const words = text.split(' '); 93 el.innerHTML = ''; 94 el.setAttribute('aria-label', text); 95 96 words.forEach((word, wi) => { 97 const wordSpan = document.createElement('span'); 98 wordSpan.className = 'word'; 99 wordSpan.style.display = 'inline-block'; 100 wordSpan.style.overflow = 'hidden'; 101 102 [...word].forEach((char, ci) => { 103 const charSpan = document.createElement('span'); 104 charSpan.className = 'char'; 105 charSpan.textContent = char; 106 charSpan.style.display = 'inline-block'; 107 charSpan.style.opacity = '0'; 108 charSpan.style.transform = 'translateY(100%)'; 109 charSpan.style.animationDelay = `${(wi * 0.08) + (ci * 0.03) + 0.3}s`; 110 wordSpan.appendChild(charSpan); 111 }); 112 113 el.appendChild(wordSpan); 114 if (wi < words.length - 1) { 115 el.appendChild(document.createTextNode(' ')); 116 } 117 }); 118 119 // Trigger animation when in view 120 const splitObserver = new IntersectionObserver((entries) => { 121 entries.forEach(entry => { 122 if (entry.isIntersecting) { 123 entry.target.querySelectorAll('.char').forEach(char => { 124 char.style.animation = `charReveal 0.6s var(--ease-out) forwards`; 125 }); 126 splitObserver.unobserve(entry.target); 127 } 128 }); 129 }, { threshold: 0.3 }); 130 131 splitObserver.observe(el); 132 }); 133 134 // âââ COUNTER ANIMATION âââ 135 document.querySelectorAll('[data-count]').forEach(el => { 136 const target = parseInt(el.dataset.count, 10); 137 const suffix = el.dataset.suffix || ''; 138 const prefix = el.dataset.prefix || ''; 139 const duration = parseInt(el.dataset.duration || '2000', 10); 140 let started = false;
141 142 const countObserver = new IntersectionObserver((entries) => { 143 entries.forEach(entry => { 144 if (entry.isIntersecting && !started) { 145 started = true; 146 animateCount(el, 0, target, duration, prefix, suffix); 147 countObserver.unobserve(entry.target); 148 } 149 }); 150 }, { threshold: 0.5 }); 151 152 countObserver.observe(el); 153 }); 154 155 function animateCount(el, start, end, duration, prefix, suffix) { 156 const startTime = performance.now(); 157 function update(currentTime) { 158 const elapsed = currentTime - startTime; 159 const progress = Math.min(elapsed / duration, 1); 160 // Ease out cubic 161 const eased = 1 - Math.pow(1 - progress, 3); 162 const current = Math.round(start + (end - start) * eased); 163 el.textContent = prefix + current.toLocaleString() + suffix; 164 if (progress < 1) { 165 requestAnimationFrame(update); 166 } 167 } 168 requestAnimationFrame(update); 169 } 170 171 // âââ PARALLAX âââ 172 const parallaxElements = document.querySelectorAll('[data-parallax]'); 173 if (parallaxElements.length > 0) { 174 let ticking = false; 175 window.addEventListener('scroll', () => { 176 if (!ticking) { 177 requestAnimationFrame(() => { 178 const scrollY = window.scrollY; 179 parallaxElements.forEach(el => { 180 const speed = parseFloat(el.dataset.parallax) || 0.3; 181 const rect = el.getBoundingClientRect(); 182 const centerY = rect.top + rect.height / 2; 183 const viewportCenter = window.innerHeight / 2; 184 const offset = (centerY - viewportCenter) * speed; 185 el.style.transform = `translateY(${offset}px)`; 186 }); 187 ticking = false; 188 }); 189 ticking = true; 190 } 191 }, { passive: true }); 192 } 193 194 // âââ MAGNETIC CURSOR EFFECT âââ 195 if (window.matchMedia('(pointer: fine)').matches && window.innerWidth > 768) { 196 document.querySelectorAll('[data-magnetic]').forEach(el => { 197 const strength = parseFloat(el.dataset.magnetic) || 0.3; 198 199 el.addEventListener('mousemove', (e) => { 200 const rect = el.getBoundingClientRect(); 201 const x = e.clientX - rect.left - rect.width / 2; 202 const y = e.clientY - rect.top - rect.height / 2; 203 el.style.transform = `translate(${x * strength}px, ${y * strength}px)`; 204 }); 205 206 el.addEventListener('mouseleave', () => { 207 el.style.transform = ''; 208 }); 209 }); 210 211 } 212 213 // âââ TILT EFFECT ON BOOK COVERS âââ 214 document.querySelectorAll('[data-tilt]').forEach(el => { 215 const maxTilt = parseFloat(el.dataset.tilt) || 15; 216 217 el.addEventListener('mousemove', (e) => { 218 const rect = el.getBoundingClientRect(); 219 const x = (e.clientX - rect.left) / rect.width; 220 const y = (e.clientY - rect.top) / rect.height; 221 const rotateX = (0.5 - y) * maxTilt; 222 const rotateY = (x - 0.5) * maxTilt; 223 el.style.transform = `perspective(800px) rotateX(${rotateX}deg) rotateY(${rotateY}deg) scale(1.02)`; 224 }); 225 226 el.addEventListener('mouseleave', () => { 227 el.style.transform = ''; 228 el.style.transition = 'transform 0.5s var(--ease-out)'; 229 setTimeout(() => { el.style.transition = ''; }, 500); 230 }); 231 }); 232 233 // âââ SMOOTH SECTION TRANSITIONS âââ 234 document.querySelectorAll('a[href^="#"]').forEach(anchor => { 235 anchor.addEventListener('click', function(e) { 236 e.preventDefault(); 237 const target = document.querySelector(this.getAttribute('href')); 238 if (target) { 239 target.scrollIntoView({ behavior: 'smooth', block: 'start' }); 240 } 241 }); 242 }); 243 244 // âââ BACK TO TOP âââ 245 const backToTop = document.querySelector('.back-to-top'); 246 if (backToTop) { 247 window.addEventListener('scroll', () => { 248 backToTop.classList.toggle('visible', window.scrollY > 600); 249 }, { passive: true }); 250 251 backToTop.addEventListener('click', () => { 252 window.scrollTo({ top: 0, behavior: 'smooth' }); 253 }); 254 } 255 256 // âââ STAGGER ANIMATION FOR GRID CHILDREN âââ 257 document.querySelectorAll('[data-stagger-grid]').forEach(grid => { 258 const gridObserver = new IntersectionObserver((entries) => {
259 entries.forEach(entry => { 260 if (entry.isIntersecting) { 261 const children = entry.target.children; 262 Array.from(children).forEach((child, i) => { 263 child.style.transition = `opacity 0.6s var(--ease-out) ${i * 0.1}s, transform 0.6s var(--ease-out) ${i * 0.1}s`; 264 child.style.opacity = '1'; 265 child.style.transform = 'translateY(0)'; 266 }); 267 gridObserver.unobserve(entry.target); 268 } 269 }); 270 }, { threshold: 0.1 }); 271 272 // Initially hide children 273 Array.from(grid.children).forEach(child => { 274 child.style.opacity = '0'; 275 child.style.transform = 'translateY(30px)'; 276 }); 277 278 gridObserver.observe(grid); 279 }); 280 281 // âââ HORIZONTAL SCROLL REVEAL FOR LOGOS/PRESS BAR âââ 282 document.querySelectorAll('[data-marquee]').forEach(el => { 283 const inner = el.querySelector('.marquee-inner'); 284 if (inner) { 285 // Clone children for seamless loop 286 const clone = inner.cloneNode(true); 287 clone.setAttribute('aria-hidden', 'true'); 288 el.appendChild(clone); 289 } 290 }); 291 292 // Page transition effect removed â the outgoing opacity fade 293 // caused persistent white screens on back-button navigation 294 // because bfcache preserved the opacity:0 state and no JS 295 // handler reliably fired to restore visibility. 296 297 // âââ LAZY IMAGE BLUR-UP âââ 298 document.querySelectorAll('img[data-src]').forEach(img => { 299 const observer = new IntersectionObserver((entries) => { 300 entries.forEach(entry => { 301 if (entry.isIntersecting) { 302 const image = entry.target; 303 image.src = image.dataset.src; 304 image.onload = () => { 305 image.style.filter = 'none'; 306 image.style.transition = 'filter 0.5s ease-out'; 307 }; 308 observer.unobserve(image); 309 } 310 }); 311 }); 312 img.style.filter = 'blur(10px)'; 313 observer.observe(img); 314 }); 315 316})();
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.