PageSourceSearch

https://believele.com/wp-content/themes/believele-theme/js/animations.js?ver=3.0.0

js believele.com collected 2026-10-02 01:23:59 UTC 17,559 bytes, 576 lines download raw bytes

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.