1/** 2 * Enhanced Homepage Interactions 3 * Handles animations, particles, and interactive elements 4 */ 5 6class HomepageEnhancer { 7 constructor() { 8 this.init(); 9 } 10 11 init() { 12 this.initParticles(); 13 this.initCounterAnimation(); 14 this.initScrollEffects(); 15 this.initHeroInteractions(); 16 this.initPerformanceOptimizations(); 17 } 18 19 // Create dynamic particles background 20 initParticles() { 21 const particlesContainer = document.getElementById('hero-particles'); 22 if (!particlesContainer) return; 23 24 const particleCount = window.innerWidth > 768 ? 60 : 25; 25 particlesContainer.innerHTML = ''; // Clear existing particles 26 27 for (let i = 0; i < particleCount; i++) { 28 const particle = document.createElement('div'); 29 particle.className = 'particle'; 30 31 // Random positioning and timing 32 particle.style.left = Math.random() * 100 + '%'; 33 particle.style.animationDelay = Math.random() * 8 + 's'; 34 particle.style.animationDuration = (Math.random() * 6 + 6) + 's'; 35 36 // Random size and opacity variation 37 const size = Math.random() * 2 + 2; 38 particle.style.width = size + 'px'; 39 particle.style.height = size + 'px'; 40 particle.style.opacity = Math.random() * 0.5 + 0.3; 41 42 particlesContainer.appendChild(particle); 43 } 44 } 45 46 // Enhanced counter animation with easing 47 initCounterAnimation() { 48 const counters = document.querySelectorAll('[data-count]'); 49 50 counters.forEach(counter => { 51 const targetCount = parseInt(counter.dataset.count); 52 let currentCount = 0; 53 let hasAnimated = false; 54 55 // Intersection Observer for triggering animation when in view 56 const observer = new IntersectionObserver((entries) => { 57 entries.forEach(entry => { 58 if (entry.isIntersecting && !hasAnimated) { 59 this.animateCounter(counter, targetCount); 60 hasAnimated = true; 61 } 62 }); 63 }, { threshold: 0.3 }); 64 65 observer.observe(counter); 66 }); 67 } 68 69 animateCounter(element, target) { 70 let start = 0; 71 const duration = 2500; 72 const startTime = performance.now(); 73 74 const animate = (currentTime) => { 75 const elapsed = currentTime - startTime; 76 const progress = Math.min(elapsed / duration, 1); 77 78 // Easing function for smooth animation 79 const easeOutQuart = 1 - Math.pow(1 - progress, 4); 80 const current = Math.floor(easeOutQuart * target); 81 82 element.textContent = current.toLocaleString(); 83 element.classList.add('counting'); 84 85 setTimeout(() => { 86 element.classList.remove('counting'); 87 }, 100); 88 89 if (progress < 1) { 90 requestAnimationFrame(animate); 91 } 92 }; 93 94 requestAnimationFrame(animate); 95 } 96 97 // Scroll effects and parallax 98 initScrollEffects() { 99 let ticking = false; 100 101 const handleScroll = () => { 102 if (!ticking) { 103 requestAnimationFrame(() => { 104 this.updateScrollEffects(); 105 ticking = false; 106 }); 107 ticking = true; 108 } 109 }; 110 111 window.addEventListener('scroll', handleScroll, { passive: true }); 112 } 113 114 updateScrollEffects() { 115 const scrolled = window.pageYOffset; 116 const hero = document.querySelector('.page-banner.home'); 117 118 if (hero && window.innerWidth > 768) { 119 // Parallax effect 120 const parallax = scrolled * 0.5; 121 hero.style.transform = `translateY(${parallax}px)`; 122 123 // Fade effect on scroll 124 const heroHeight = hero.offsetHeight; 125 const opacity = Math.max(0, 1 - (scrolled / heroHeight) * 1.5); 126 hero.style.opacity = opacity; 127 } 128 129 // Fade in elements on scroll 130 const elements = document.querySelectorAll('.fade-in-on-scroll');
131 elements.forEach(element => { 132 const elementTop = element.getBoundingClientRect().top; 133 const elementVisible = 150; 134 135 if (elementTop < window.innerHeight - elementVisible) { 136 element.classList.add('active'); 137 } 138 }); 139 } 140 141 // Hero section interactions 142 initHeroInteractions() { 143 // Smooth scroll to next section 144 window.scrollToNextSection = () => { 145 const nextSection = document.querySelector('.counter-section') || 146 document.querySelector('section:nth-of-type(2)'); 147 if (nextSection) { 148 nextSection.scrollIntoView({ 149 behavior: 'smooth', 150 block: 'start' 151 }); 152 } 153 }; 154 155 // Hero button hover effects 156 const heroButtons = document.querySelectorAll('.btn-hero-primary, .btn-hero-secondary'); 157 heroButtons.forEach(button => { 158 button.addEventListener('mouseenter', () => { 159 button.style.transform = 'translateY(-3px) scale(1.05)'; 160 }); 161 162 button.addEventListener('mouseleave', () => { 163 button.style.transform = 'translateY(0) scale(1)'; 164 }); 165 }); 166 167 // Interactive particle effects on mouse move 168 const hero = document.querySelector('.page-banner.home'); 169 if (hero && window.innerWidth > 768) { 170 hero.addEventListener('mousemove', (e) => { 171 const particles = document.querySelectorAll('.particle'); 172 const mouseX = e.clientX / window.innerWidth; 173 const mouseY = e.clientY / window.innerHeight; 174 175 particles.forEach((particle, index) => { 176 const speed = (index % 3 + 1) * 0.5; 177 const x = (mouseX - 0.5) * speed; 178 const y = (mouseY - 0.5) * speed; 179 180 particle.style.transform = `translate(${x}px, ${y}px)`; 181 }); 182 }); 183 } 184 } 185 186 // Performance optimizations 187 initPerformanceOptimizations() { 188 // Lazy loading for images 189 if ('IntersectionObserver' in window) { 190 const imageObserver = new IntersectionObserver((entries, observer) => { 191 entries.forEach(entry => { 192 if (entry.isIntersecting) { 193 const img = entry.target; 194 img.src = img.dataset.src; 195 img.classList.remove('lazy'); 196 imageObserver.unobserve(img); 197 } 198 }); 199 }); 200 201 document.querySelectorAll('img[data-src]').forEach(img => { 202 imageObserver.observe(img); 203 }); 204 } 205 206 // Reduce animations on low-end devices 207 if (navigator.hardwareConcurrency && navigator.hardwareConcurrency < 4) { 208 document.documentElement.classList.add('reduced-motion'); 209 } 210 211 // Pause animations when tab is not visible 212 document.addEventListener('visibilitychange', () => { 213 const animations = document.querySelectorAll('.particle, .counter-section::before'); 214 animations.forEach(element => { 215 if (document.hidden) { 216 element.style.animationPlayState = 'paused'; 217 } else { 218 element.style.animationPlayState = 'running'; 219 } 220 }); 221 }); 222 } 223 224 // Responsive particle management 225 handleResize() { 226 this.initParticles(); 227 } 228} 229 230// Initialize when DOM is loaded 231document.addEventListener('DOMContentLoaded', () => { 232 const enhancer = new HomepageEnhancer(); 233 234 // Handle window resize 235 window.addEventListener('resize', () => { 236 enhancer.handleResize(); 237 }); 238}); 239 240// Smooth scroll polyfill for older browsers 241if (!Element.prototype.scrollIntoView) { 242 Element.prototype.scrollIntoView = function(options) { 243 const element = this; 244 const targetPosition = element.offsetTop; 245 246 if (options && options.behavior === 'smooth') { 247 const startPosition = window.pageYOffset; 248 const distance = targetPosition - startPosition; 249 const duration = 800; 250 let start = null; 251 252 function animation(currentTime) { 253 if (start === null) start = currentTime; 254 const timeElapsed = currentTime - start; 255 const progress = Math.min(timeElapsed / duration, 1); 256 257 window.scrollTo(0, startPosition + distance * progress); 258 259 if (timeElapsed < duration) { 260 requestAnimationFrame(animation); 261 } 262 } 263 264 requestAnimationFrame(animation); 265 } else { 266 window.scrollTo(0, targetPosition); 267 } 268 }; 269}
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.