1/** 2 * åææ¸ä½ç§æ - å°æ¥åç«ææ 3 * Professional Animation Effects 4 * Version: 2.0 5 * 6 * åè½ï¼ 7 * 1. 滾å觸ç¼åç« 8 * 2. è¨æ¸å¨åç« 9 * 3. æåæ©ææ 10 * 4. å°èªæ»¾åææ 11 * 5. å¹³æ»æ»¾å 12 * 6. è¦å·®ææ 13 */ 14 15(function() { 16 'use strict'; 17 18 // ============================================ 19 // åå§å 20 // ============================================ 21 document.addEventListener('DOMContentLoaded', function() { 22 initScrollAnimations(); 23 initCounters(); 24 initTypewriter(); 25 initNavbarScroll(); 26 initSmoothScroll(); 27 initParallax(); 28 initTiltEffect(); 29 initRippleEffect(); 30 initScrollProgress(); 31 }); 32 33 // ============================================ 34 // 滾å觸ç¼åç« 35 // ============================================ 36 function initScrollAnimations() { 37 var animatedElements = document.querySelectorAll('.animate-on-scroll'); 38 var staggerContainers = document.querySelectorAll('.stagger-animation'); 39 40 if (!animatedElements.length && !staggerContainers.length) return; 41 42 // ä½¿ç¨ Intersection Observer API 43 if ('IntersectionObserver' in window) { 44 var observerOptions = { 45 root: null, 46 rootMargin: '0px 0px -50px 0px', 47 threshold: 0.1 48 }; 49 50 var observer = new IntersectionObserver(function(entries) { 51 entries.forEach(function(entry) { 52 if (entry.isIntersecting) { 53 entry.target.classList.add('animated'); 54 // å¯é¸ï¼åªè§¸ç¼ä¸æ¬¡ 55 // observer.unobserve(entry.target); 56 } 57 }); 58 }, observerOptions); 59 60 animatedElements.forEach(function(el) { 61 observer.observe(el); 62 }); 63 64 staggerContainers.forEach(function(el) { 65 observer.observe(el); 66 }); 67 } else { 68 // éç´æ¹æ¡ï¼ç´æ¥é¡¯ç¤º 69 animatedElements.forEach(function(el) { 70 el.classList.add('animated'); 71 }); 72 staggerContainers.forEach(function(el) { 73 el.classList.add('animated'); 74 }); 75 } 76 } 77 78 // ============================================ 79 // è¨æ¸å¨åç« 80 // ============================================ 81 function initCounters() { 82 var counters = document.querySelectorAll('[data-counter]'); 83 if (!counters.length) return; 84 85 var observerOptions = { 86 root: null, 87 rootMargin: '0px', 88 threshold: 0.5 89 }; 90 91 var counterObserver = new IntersectionObserver(function(entries) { 92 entries.forEach(function(entry) { 93 if (entry.isIntersecting && !entry.target.classList.contains('counted')) { 94 animateCounter(entry.target); 95 entry.target.classList.add('counted'); 96 } 97 }); 98 }, observerOptions); 99 100 counters.forEach(function(counter) { 101 counterObserver.observe(counter); 102 }); 103 } 104 105 function animateCounter(element) { 106 // æ¯æ´ data-counter å data-target å ©ç¨®å±¬æ§ 107 var target = parseFloat(element.getAttribute('data-counter')) || parseFloat(element.getAttribute('data-target')) || 0; 108 var suffix = element.getAttribute('data-suffix') || ''; 109 var prefix = element.getAttribute('data-prefix') || ''; 110 var decimals = parseInt(element.getAttribute('data-decimals')) || 0; 111 var duration = parseInt(element.getAttribute('data-duration')) || 2000; 112 113 var startTime = null; 114 var startValue = 0; 115 116 function easeOutExpo(t) { 117 return t === 1 ? 1 : 1 - Math.pow(2, -10 * t); 118 } 119 120 function animate(currentTime) { 121 if (!startTime) startTime = currentTime; 122 var progress = Math.min((currentTime - startTime) / duration, 1); 123 var easedProgress = easeOutExpo(progress); 124 var currentValue = startValue + (target - startValue) * easedProgress; 125 126 element.textContent = prefix + currentValue.toFixed(decimals) + suffix; 127 128 if (progress < 1) { 129 requestAnimationFrame(animate); 130 } 131 } 132 133 requestAnimationFrame(animate); 134 } 135 136 // ============================================ 137 // æåæ©ææ - ç± luxury-animations.js èç 138 // ============================================ 139 function initTypewriter() { 140 // æåæ©ææå·²ç§»è³ luxury-animations.js çµ±ä¸èç 141 // é¿å å¤åè ³æ¬åæèçåä¸å ç´ é æè¡çª 142 return; 143 } 144 145 // ============================================ 146 // å°èªæ»¾åææ 147 // ============================================ 148 function initNavbarScroll() { 149 var navbar = document.querySelector('.navbar'); 150 if (!navbar) return; 151 152 var scrollThreshold = 50; 153 var lastScroll = 0; 154 155 function handleScroll() { 156 var currentScroll = window.pageYOffset || document.documentElement.scrollTop; 157
158 // æ·»å /ç§»é¤æ»¾åé¡ 159 if (currentScroll > scrollThreshold) { 160 navbar.classList.add('scrolled'); 161 } else { 162 navbar.classList.remove('scrolled'); 163 } 164 165 // å¯é¸ï¼æ»¾åæ¹å檢測ï¼é±è/顯示å°èªï¼ 166 // if (currentScroll > lastScroll && currentScroll > 300) { 167 // navbar.style.transform = 'translateY(-100%)'; 168 // } else { 169 // navbar.style.transform = 'translateY(0)'; 170 // } 171 172 lastScroll = currentScroll; 173 } 174 175 // ä½¿ç¨ passive ç£è½å¨æåæ§è½ 176 window.addEventListener('scroll', handleScroll, { passive: true }); 177 178 // åå§æª¢æ¥ 179 handleScroll(); 180 } 181 182 // ============================================ 183 // å¹³æ»æ»¾å 184 // ============================================ 185 function initSmoothScroll() { 186 document.querySelectorAll('a[href^="#"]').forEach(function(anchor) { 187 anchor.addEventListener('click', function(e) { 188 var targetId = this.getAttribute('href'); 189 if (targetId === '#') return; 190 191 // è·³é Bootstrap dropdown å collapse 觸ç¼å¨ 192 if (this.hasAttribute('data-bs-toggle')) return; 193 if (this.classList.contains('dropdown-toggle')) return; 194 if (this.classList.contains('nav-link') && this.closest('.dropdown')) return; 195 196 var target = document.querySelector(targetId); 197 if (target) { 198 e.preventDefault(); 199 200 var navbarHeight = document.querySelector('.navbar') ? 201 document.querySelector('.navbar').offsetHeight : 0; 202 203 var targetPosition = target.getBoundingClientRect().top + window.pageYOffset - navbarHeight; 204 205 window.scrollTo({ 206 top: targetPosition, 207 behavior: 'smooth' 208 }); 209 } 210 }); 211 }); 212 } 213 214 // ============================================ 215 // è¦å·®ææ 216 // ============================================ 217 function initParallax() { 218 var parallaxElements = document.querySelectorAll('[data-parallax]'); 219 if (!parallaxElements.length) return; 220 221 function handleParallax() { 222 var scrollY = window.pageYOffset; 223 224 parallaxElements.forEach(function(element) { 225 var speed = parseFloat(element.getAttribute('data-parallax')) || 0.5; 226 var offset = scrollY * speed; 227 element.style.transform = 'translateY(' + offset + 'px)'; 228 }); 229 } 230 231 window.addEventListener('scroll', handleParallax, { passive: true }); 232 } 233 234 // ============================================ 235 // 3D å¾æææ 236 // ============================================ 237 function initTiltEffect() { 238 var tiltElements = document.querySelectorAll('.tilt-card'); 239 240 tiltElements.forEach(function(element) { 241 element.addEventListener('mousemove', function(e) { 242 var rect = element.getBoundingClientRect(); 243 var x = e.clientX - rect.left; 244 var y = e.clientY - rect.top; 245 246 var centerX = rect.width / 2; 247 var centerY = rect.height / 2; 248 249 var rotateX = (y - centerY) / 20; 250 var rotateY = (centerX - x) / 20; 251 252 element.style.transform = 'perspective(1000px) rotateX(' + rotateX + 'deg) rotateY(' + rotateY + 'deg) translateZ(10px)'; 253 }); 254 255 element.addEventListener('mouseleave', function() { 256 element.style.transform = 'perspective(1000px) rotateX(0) rotateY(0) translateZ(0)'; 257 }); 258 }); 259 } 260 261 // ============================================ 262 // æ¼£æ¼ªææ 263 // ============================================ 264 function initRippleEffect() { 265 document.querySelectorAll('.ripple').forEach(function(button) { 266 button.addEventListener('click', function(e) { 267 var rect = button.getBoundingClientRect(); 268 var size = Math.max(rect.width, rect.height); 269 var x = e.clientX - rect.left - size / 2; 270 var y = e.clientY - rect.top - size / 2; 271 272 var ripple = document.createElement('span'); 273 ripple.style.cssText = 274 'position: absolute;' + 275 'width: ' + size + 'px;' + 276 'height: ' + size + 'px;' + 277 'left: ' + x + 'px;' + 278 'top: ' + y + 'px;' + 279 'background: radial-gradient(circle, rgba(255,255,255,0.4) 0%, transparent 70%);' + 280 'border-radius: 50%;' + 281 'transform: scale(0);' + 282 'animation: rippleEffect 0.6s ease-out;' + 283 'pointer-events: none;'; 284 285 button.style.position = 'relative'; 286 button.style.overflow = 'hidden'; 287 button.appendChild(ripple); 288 289 setTimeout(function() { 290 ripple.remove(); 291 }, 600); 292 }); 293 }); 294 295 // æ·»å CSS åç« 296 if (!document.getElementById('ripple-style')) { 297 var style = document.createElement('style'); 298 style.id = 'ripple-style'; 299 style.textContent = '@keyframes rippleEffect { to { transform: scale(2); opacity: 0; } }'; 300 document.head.appendChild(style); 301 } 302 } 303 304 // ============================================ 305 // 滾åé²åº¦æ¢ 306 // ============================================ 307 function initScrollProgress() { 308 // åµå»ºé²åº¦æ¢å ç´ 309 var progressBar = document.createElement('div'); 310 progressBar.className = 'scroll-progress'; 311 progressBar.style.cssText = 312 'position: fixed;' + 313 'top: 0;' + 314 'left: 0;' + 315 'height: 3px;' + 316 'background: linear-gradient(90deg, #ffc107, #ff9800);' + 317 'z-index: 9999;' + 318 'transition: width 0.1s linear;' + 319 'width: 0%;'; 320 document.body.appendChild(progressBar); 321 322 function updateProgress() { 323 var scrollTop = document.documentElement.scrollTop || document.body.scrollTop; 324 var scrollHeight = document.documentElement.scrollHeight - document.documentElement.clientHeight; 325 var progress = (scrollTop / scrollHeight) * 100; 326 progressBar.style.width = progress + '%'; 327 } 328 329 window.addEventListener('scroll', updateProgress, { passive: true });
330 } 331 332 // ============================================ 333 // æµ®åå¾½ç« åç« 334 // ============================================ 335 var floatingBadges = document.querySelectorAll('.floating-badge'); 336 floatingBadges.forEach(function(badge, index) { 337 badge.style.animation = 'floatBadge 3s ease-in-out infinite'; 338 badge.style.animationDelay = (index * 0.5) + 's'; 339 }); 340 341 // æ·»å æµ®ååç« CSS 342 if (!document.getElementById('float-badge-style')) { 343 var floatStyle = document.createElement('style'); 344 floatStyle.id = 'float-badge-style'; 345 floatStyle.textContent = '@keyframes floatBadge { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-8px); } }'; 346 document.head.appendChild(floatStyle); 347 } 348 349})();
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.