1/** 2 * Believele Theme - Animations & Interactions (Enhanced) 3 * Vanilla JS, no dependencies. 4 * 5 * Features: 6 * 1. Scroll Progress Bar 7 * 2. Scroll-triggered reveal animations with stagger (IntersectionObserver) 8 * 3. Navbar scroll effect (shrink + blur + shadow) 9 * 4. Mobile hamburger menu 10 * 5. Smooth anchor scroll with navbar offset 11 * 6. Animated counters with easing 12 * 7. Hero mouse-follow glow 13 * 8. Parallax background orbs 14 * 9. Active nav-link highlighting 15 * 10. Typing text effect 16 * 11. FAQ accordion 17 */ 18 19(function () { 20 'use strict'; 21 22 /* ========================================================================== 23 1. SCROLL PROGRESS BAR 24 ========================================================================== */ 25 26 function initScrollProgress() { 27 // Create the progress bar element 28 var bar = document.createElement('div'); 29 bar.className = 'scroll-progress'; 30 document.body.appendChild(bar); 31 32 var ticking = false; 33 34 function updateProgress() { 35 var scrollTop = window.scrollY || document.documentElement.scrollTop; 36 var docHeight = document.documentElement.scrollHeight - document.documentElement.clientHeight; 37 var progress = docHeight > 0 ? (scrollTop / docHeight) * 100 : 0; 38 bar.style.width = progress + '%'; 39 } 40 41 function onScroll() { 42 if (ticking) return; 43 ticking = true; 44 requestAnimationFrame(function () { 45 updateProgress(); 46 ticking = false; 47 }); 48 } 49 50 window.addEventListener('scroll', onScroll, { passive: true }); 51 // Initial state 52 updateProgress(); 53 } 54 55 /* ========================================================================== 56 2. SCROLL FADE-IN ANIMATIONS (IntersectionObserver) WITH STAGGER 57 ========================================================================== */ 58 59 function initScrollAnimations() { 60 var revealSelectors = '.fade-in, .slide-up, .scale-in'; 61 var revealEls = document.querySelectorAll(revealSelectors); 62 if (!revealEls.length) return; 63 64 var observer = new IntersectionObserver( 65 function (entries) { 66 entries.forEach(function (entry) { 67 if (!entry.isIntersecting) return; 68 69 var el = entry.target; 70 71 // If this element has stagger-children class, apply incremental delays to children 72 if (el.classList.contains('stagger-children')) { 73 var children = el.children; 74 for (var i = 0; i < children.length; i++) { 75 children[i].style.transitionDelay = (i * 0.05) + 's'; 76 children[i].style.animationDelay = (i * 0.05) + 's'; 77 } 78 } 79 80 el.classList.add('visible'); 81 observer.unobserve(el); // animate only once 82 }); 83 }, 84 { threshold: 0.05, rootMargin: '0px 0px 50px 0px' } 85 ); 86 87 revealEls.forEach(function (el) { 88 observer.observe(el); 89 }); 90 } 91 92 /* ========================================================================== 93 3. NAVBAR SCROLL EFFECT 94 ========================================================================== */ 95 96 function initNavbarScroll() { 97 var nav = document.getElementById('siteNav'); 98 if (!nav) { 99 // Fallback: try querySelector 100 nav = document.querySelector('.site-nav'); 101 } 102 if (!nav) return; 103 104 var SCROLL_THRESHOLD = 80; 105 var ticking = false; 106 107 function onScroll() { 108 if (ticking) return; 109 ticking = true; 110 111 requestAnimationFrame(function () { 112 if (window.scrollY > SCROLL_THRESHOLD) { 113 nav.classList.add('scrolled'); 114 } else { 115 nav.classList.remove('scrolled'); 116 } 117 ticking = false; 118 }); 119 } 120 121 window.addEventListener('scroll', onScroll, { passive: true }); 122 // Run once on load in case page is already scrolled 123 onScroll(); 124 } 125 126 /* ========================================================================== 127 4. MOBILE HAMBURGER MENU 128 ========================================================================== */ 129 130 function initMobileMenu() { 131 var hamburger = document.getElementById('navHamburger'); 132 var mobileNav = document.getElementById('mobileNav'); 133 if (!hamburger || !mobileNav) return; 134 135 function openMenu() { 136 hamburger.classList.add('open'); 137 mobileNav.classList.add('open'); 138 document.body.style.overflow = 'hidden'; 139 } 140 141 function closeMenu() { 142 hamburger.classList.remove('open'); 143 mobileNav.classList.remove('open'); 144 document.body.style.overflow = ''; 145 } 146 147 function toggleMenu() { 148 if (mobileNav.classList.contains('open')) { 149 closeMenu(); 150 } else { 151 openMenu(); 152 } 153 } 154 155 // Toggle on hamburger click 156 hamburger.addEventListener('click', function (e) { 157 e.stopPropagation(); 158 toggleMenu(); 159 });
160 161 // Close when a mobile nav link is clicked 162 var mobileLinks = mobileNav.querySelectorAll('a'); 163 mobileLinks.forEach(function (link) { 164 link.addEventListener('click', closeMenu); 165 }); 166 167 // Close when clicking outside the menu 168 document.addEventListener('click', function (e) { 169 if ( 170 mobileNav.classList.contains('open') && 171 !mobileNav.contains(e.target) && 172 !hamburger.contains(e.target) 173 ) { 174 closeMenu(); 175 } 176 }); 177 178 // Close on Escape key 179 document.addEventListener('keydown', function (e) { 180 if (e.key === 'Escape' && mobileNav.classList.contains('open')) { 181 closeMenu(); 182 } 183 }); 184 } 185 186 /* ========================================================================== 187 5. SMOOTH SCROLL (anchor links, with navbar offset) 188 ========================================================================== */ 189 190 function initSmoothScroll() { 191 var NAV_HEIGHT = 80; 192 193 document.addEventListener('click', function (e) { 194 var anchor = e.target.closest('a[href^="#"]'); 195 if (!anchor) return; 196 197 var hash = anchor.getAttribute('href'); 198 if (hash === '#' || hash === '') return; 199 200 var target = document.querySelector(hash); 201 if (!target) return; 202 203 e.preventDefault(); 204 205 var top = target.getBoundingClientRect().top + window.scrollY - NAV_HEIGHT; 206 window.scrollTo({ top: top, behavior: 'smooth' }); 207 208 // Update URL hash without jumping 209 if (history.pushState) { 210 history.pushState(null, null, hash); 211 } 212 }); 213 } 214 215 /* ========================================================================== 216 6. ANIMATED COUNTERS (.stat-number) with requestAnimationFrame + easing 217 ========================================================================== */ 218 219 function initCounters() { 220 var counters = document.querySelectorAll('.stat-number[data-target]'); 221 if (!counters.length) return; 222 223 var observer = new IntersectionObserver( 224 function (entries) { 225 entries.forEach(function (entry) { 226 if (!entry.isIntersecting) return; 227 228 var el = entry.target; 229 if (el.dataset.counted) return; // only animate once 230 el.dataset.counted = 'true'; 231 observer.unobserve(el); 232 233 animateCounter(el); 234 }); 235 }, 236 { threshold: 0.1 } 237 ); 238 239 counters.forEach(function (el) { 240 observer.observe(el); 241 }); 242 } 243 244 /** 245 * Animate a single counter element from 0 to data-target. 246 * Uses ease-out cubic timing over 2 seconds. 247 */ 248 function animateCounter(el) { 249 var target = parseFloat(el.dataset.target); 250 var suffix = el.dataset.suffix || ''; 251 var prefix = el.dataset.prefix || ''; 252 var decimals = el.dataset.decimals ? parseInt(el.dataset.decimals, 10) : 0; 253 254 // If no data-target, keep the static text 255 if (isNaN(target)) return; 256 257 var duration = 2000; 258 var start = performance.now(); 259 260 function easeOutCubic(t) { 261 return 1 - Math.pow(1 - t, 3); 262 } 263 264 function tick(now) { 265 var elapsed = now - start; 266 var progress = Math.min(elapsed / duration, 1); 267 var easedProgress = easeOutCubic(progress); 268 var value = easedProgress * target; 269 270 if (decimals > 0) { 271 el.textContent = prefix + value.toFixed(decimals) + suffix; 272 } else { 273 el.textContent = prefix + Math.round(value).toLocaleString() + suffix; 274 } 275 276 if (progress < 1) { 277 requestAnimationFrame(tick); 278 } 279 } 280 281 requestAnimationFrame(tick); 282 } 283 284 /* ========================================================================== 285 7. HERO FLOATING TAGS â MOUSE-REACTIVE PARALLAX 286 ========================================================================== */ 287 288 function initHeroFloatTags() { 289 var container = document.getElementById('heroFloatTags'); 290 if (!container) return; 291 if (window.innerWidth <= 768) return; 292 293 var tags = container.querySelectorAll('.float-tag'); 294 if (!tags.length) return; 295 296 var hero = container.closest('.hero'); 297 var mouseX = 0; 298 var mouseY = 0; 299 var currentX = []; 300 var currentY = []; 301 var rafId = null; 302 303 // Initialize positions
304 tags.forEach(function () { 305 currentX.push(0); 306 currentY.push(0); 307 }); 308 309 hero.addEventListener('mousemove', function (e) { 310 var rect = hero.getBoundingClientRect(); 311 // Normalize mouse position to -1 to 1 from center 312 mouseX = ((e.clientX - rect.left) / rect.width - 0.5) * 2; 313 mouseY = ((e.clientY - rect.top) / rect.height - 0.5) * 2; 314 315 if (!rafId) { 316 rafId = requestAnimationFrame(animateTags); 317 } 318 }); 319 320 hero.addEventListener('mouseleave', function () { 321 mouseX = 0; 322 mouseY = 0; 323 if (!rafId) { 324 rafId = requestAnimationFrame(animateTags); 325 } 326 }); 327 328 function animateTags() { 329 var stillMoving = false; 330 331 tags.forEach(function (tag, i) { 332 var depth = parseFloat(tag.dataset.depth) || 0.03; 333 var maxMove = depth * 800; // pixels of movement 334 335 var targetX = mouseX * maxMove; 336 var targetY = mouseY * maxMove; 337 338 // Smooth lerp (ease toward target) 339 currentX[i] += (targetX - currentX[i]) * 0.08; 340 currentY[i] += (targetY - currentY[i]) * 0.08; 341 342 // Check if we are still moving (to stop the loop) 343 if (Math.abs(targetX - currentX[i]) > 0.1 || Math.abs(targetY - currentY[i]) > 0.1) { 344 stillMoving = true; 345 } 346 347 tag.style.transform = 'translate(' + currentX[i].toFixed(1) + 'px, ' + currentY[i].toFixed(1) + 'px)'; 348 }); 349 350 if (stillMoving) { 351 rafId = requestAnimationFrame(animateTags); 352 } else { 353 rafId = null; 354 } 355 } 356 357 // Also handle resize 358 window.addEventListener('resize', function () { 359 if (window.innerWidth <= 768) {
360 tags.forEach(function (tag) { 361 tag.style.transform = ''; 362 }); 363 rafId = null; 364 } 365 }, { passive: true }); 366 } 367 368 // Keep old function name for init 369 function initHeroMouseGlow() { 370 initHeroFloatTags(); 371 } 372 373 /* ========================================================================== 374 8. PARALLAX BACKGROUND ORBS (subtle scroll parallax) 375 ========================================================================== */ 376 377 function initParallax() { 378 var orbs = document.querySelectorAll('.hero-orb'); 379 if (!orbs.length) return; 380 if (window.innerWidth <= 768) return; 381 382 var speeds = [0.1, 0.08, 0.05]; 383 var ticking = false; 384 385 function onScroll() { 386 if (ticking) return; 387 ticking = true; 388 389 requestAnimationFrame(function () { 390 if (window.innerWidth > 768) { 391 var scrollY = window.scrollY; 392 orbs.forEach(function (orb, index) { 393 var speed = speeds[index] || 0.2; 394 var offset = scrollY * speed; 395 orb.style.transform = 'translateY(' + offset + 'px)'; 396 }); 397 } 398 ticking = false; 399 }); 400 } 401 402 window.addEventListener('scroll', onScroll, { passive: true }); 403 onScroll(); 404 405 window.addEventListener('resize', function () { 406 if (window.innerWidth <= 768) { 407 orbs.forEach(function (orb) { 408 orb.style.transform = ''; 409 }); 410 } 411 }, { passive: true }); 412 } 413 414 /* ========================================================================== 415 9. ACTIVE NAV LINK HIGHLIGHTING 416 ========================================================================== */ 417 418 function initActiveNav() { 419 var sections = document.querySelectorAll('section[id]'); 420 var navLinks = document.querySelectorAll('.site-nav a[href^="#"], .nav-links a[href^="#"], .mobile-nav-link[href^="#"]'); 421 if (!sections.length || !navLinks.length) return; 422 423 var NAV_HEIGHT = 100; // slightly larger than actual nav for better UX 424 var ticking = false; 425 426 function update() { 427 var scrollPos = window.scrollY + NAV_HEIGHT; 428 var currentId = ''; 429 430 sections.forEach(function (section) { 431 var top = section.offsetTop; 432 var bottom = top + section.offsetHeight; 433 if (scrollPos >= top && scrollPos < bottom) { 434 currentId = section.getAttribute('id'); 435 } 436 }); 437 438 navLinks.forEach(function (link) { 439 var href = link.getAttribute('href'); 440 if (href === '#' + currentId) { 441 link.classList.add('active'); 442 } else { 443 link.classList.remove('active'); 444 } 445 }); 446 } 447 448 function onScroll() { 449 if (ticking) return; 450 ticking = true; 451 requestAnimationFrame(function () { 452 update(); 453 ticking = false; 454 }); 455 } 456 457 window.addEventListener('scroll', onScroll, { passive: true }); 458 update(); // run on load 459 } 460 461 /* ========================================================================== 462 10. TYPING EFFECT (.typing-text) 463 ========================================================================== */ 464 465 function initTypingEffect() { 466 var el = document.querySelector('.typing-text'); 467 if (!el) return; 468 469 var rawTexts = el.dataset.texts; 470 if (!rawTexts) return; 471 472 var texts; 473 try { 474 texts = JSON.parse(rawTexts); 475 } catch (e) { 476 // Fallback: treat as comma-separated 477 texts = rawTexts.split(',').map(function (s) { return s.trim(); }); 478 } 479 480 if (!texts.length) return; 481 482 var textIndex = 0; // which string we are on 483 var charIndex = 0; // position within current string 484 var isDeleting = false; 485 var TYPING_SPEED = 80; // ms per character (typing) 486 var DELETING_SPEED = 40; // ms per character (deleting) 487 var PAUSE_AFTER = 1800; // ms to wait after full text typed 488 var PAUSE_BEFORE = 400; // ms to wait after text fully deleted 489 490 function tick() { 491 var current = texts[textIndex]; 492 493 if (isDeleting) { 494 charIndex--; 495 el.textContent = current.substring(0, charIndex); 496 497 if (charIndex === 0) { 498 isDeleting = false; 499 textIndex = (textIndex + 1) % texts.length; 500 setTimeout(tick, PAUSE_BEFORE); 501 return; 502 } 503 504 setTimeout(tick, DELETING_SPEED); 505 } else { 506 charIndex++; 507 el.textContent = current.substring(0, charIndex); 508 509 if (charIndex === current.length) { 510 isDeleting = true; 511 setTimeout(tick, PAUSE_AFTER); 512 return; 513 } 514 515 setTimeout(tick, TYPING_SPEED); 516 } 517 } 518 519 // Clear any static text and start the loop 520 el.textContent = ''; 521 setTimeout(tick, PAUSE_BEFORE); 522 } 523 524 /* ========================================================================== 525 11. FAQ ACCORDION 526 ========================================================================== */ 527 528 function initFaqAccordion() { 529 var faqQuestions = document.querySelectorAll('.faq-question'); 530 if (!faqQuestions.length) return; 531 532 faqQuestions.forEach(function (question) { 533 question.addEventListener('click', function () { 534 var parentItem = this.closest('.faq-item'); 535 if (!parentItem) return; 536 537 var isActive = parentItem.classList.contains('active'); 538 539 // Close all other FAQ items (accordion behavior) 540 var allItems = document.querySelectorAll('.faq-item');
541 allItems.forEach(function (item) { 542 item.classList.remove('active'); 543 }); 544 545 // Toggle current item 546 if (!isActive) { 547 parentItem.classList.add('active'); 548 } 549 }); 550 }); 551 } 552 553 /* ========================================================================== 554 INIT - Run all modules when the DOM is ready 555 ========================================================================== */ 556 557 function init() { 558 initScrollProgress(); 559 initScrollAnimations(); 560 initNavbarScroll(); 561 initMobileMenu(); 562 initSmoothScroll(); 563 initCounters(); 564 initHeroMouseGlow(); 565 initParallax(); 566 initActiveNav(); 567 initTypingEffect(); 568 initFaqAccordion(); 569 } 570 571 if (document.readyState === 'loading') { 572 document.addEventListener('DOMContentLoaded', init); 573 } else { 574 init(); 575 } 576})();
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.