All files animations.js

92.3% Statements 72/78
84.21% Branches 32/38
90% Functions 18/20
92% Lines 69/75

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176                21x         11x   11x     7x   11x 11x 11x     11x         11x   3x 3x 3x     3x 1x 1x       2x 2x 1x 1x 1x   1x 1x                                   11x 9x             2x           6x 15x 15x     8x 8x 8x   8x 1x 1x               6x 6x 1x 1x 1x 1x 1x           6x 6x 1x 1x 1x 1x 1x                 6x 5x 5x     6x     4x 4x 2x 2x         4x   1x 1x 1x 1x 1x 1x   1x       1x 1x   1x                   4x 4x        
/* ANIMATIONS.JS - Fade-in Animations, Image Loading, Navigation =======================
   Copyright 2025, Mark Forscher */
/* ======================================================================================= */
 
import { Utils } from './utils.js';
 
export class Animations {
  constructor(core) {
    this.core = core;
  }
 
  // Enhanced fade-in animation for content loading sequence
  initEnhancedFadeIn() {
    const fadeElements = Utils.$(".fade-in");
 
    if (fadeElements.length === 0) return;
 
    // Detect mobile for more aggressive loading
    const isMobile = window.innerWidth <= 768 || /Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
 
    let visibleCount = 0; // Track order of appearance for stagger
    const maxStaggerSteps = isMobile ? 2 : 4; // Prevent long delays on long pages
    const staggerUnit = isMobile ? 90 : 140; // Slightly quicker base delay on desktop
 
    // Mobile-optimized settings for smoother UX
    const observerOptions = {
      threshold: 0.02, // Slightly higher so we trigger when a sliver is visible
      rootMargin: isMobile ? '45% 0px' : '35% 0px' // Expand viewport to start animations well before entry
    };
 
    const observer = new IntersectionObserver(
      (entries) => {
        entries.forEach((entry) => {
          Eif (entry.isIntersecting) {
            const element = entry.target;
 
            // Skip if already visible
            if (element.classList.contains('visible')) {
              observer.unobserve(element);
              return;
            }
 
            // Special handling for journal items - only fade in if image is loaded
            if (element.classList.contains('journal-items-item')) {
              if (element.classList.contains('image-loaded')) {
                const delayStep = Math.min(visibleCount, maxStaggerSteps);
                setTimeout(() => {
                  element.classList.add("visible");
                }, delayStep * staggerUnit);
                visibleCount = Math.min(visibleCount + 1, maxStaggerSteps);
                observer.unobserve(element);
              }
              // If image not loaded yet, the image load handler will trigger the fade-in
            } else E{
              // Normal fade-in with device-optimized stagger delay
              const delayStep = Math.min(visibleCount, maxStaggerSteps);
              setTimeout(() => {
                element.classList.add("visible");
              }, delayStep * staggerUnit);
              visibleCount = Math.min(visibleCount + 1, maxStaggerSteps);
              observer.unobserve(element);
            }
          }
        });
      },
      observerOptions
    );
 
    fadeElements.forEach(element => {
      observer.observe(element);
    });
  }
 
  // Legacy fade-in for backwards compatibility (called during init)
  initFadeIn() {
    // Always run enhanced fade-in - the content loading sequence will handle timing
    this.initEnhancedFadeIn();
  }
 
  // Modern navigation handling
  initNavigation() {
    // Handle internal link clicks with preloader
    document.addEventListener('click', (e) => {
      const link = e.target.closest('a');
      if (!link) return;
 
      // Check if it's an internal link
      try {
        const linkUrl = new URL(link.href);
        const currentUrl = new URL(window.location.href);
 
        if (linkUrl.hostname === currentUrl.hostname) {
          e.preventDefault();
          this.core.navigateWithPreloader(link.href);
        }
      } catch (error) {
        // Invalid URL, let it proceed normally
      }
    });
 
    // Handle project list clicks
    const projectListItems = Utils.$(".project-list li");
    projectListItems.forEach(item => {
      item.addEventListener('click', (e) => {
        e.preventDefault();
        const link = item.querySelector('a');
        Eif (link) {
          this.core.navigateWithPreloader(link.href);
        }
      });
    });
 
    // Handle masthead clicks
    const mastheadName = Utils.$1(".masthead-name");
    if (mastheadName) {
      mastheadName.addEventListener('click', (e) => {
        e.preventDefault();
        const link = mastheadName.querySelector('a');
        Eif (link) {
          this.core.navigateWithPreloader(link.href);
        }
      });
    }
  }
 
  // Modern image loading with Intersection Observer (replaces lazy loading)
  initImageLoading() {
    // Get all images that are below the fold
    const images = Array.from(Utils.$('img')).filter(img => {
      const rect = img.getBoundingClientRect();
      return rect.top > window.innerHeight;
    });
 
    if (images.length === 0) return;
 
    // Set up lazy loading for images below the fold
    images.forEach(img => {
      if (img.src && !img.dataset.original) {
        img.dataset.original = img.src;
        img.src = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAIAAAD91JpzAAAAFklEQVQImWP8//8/AwMDEwMDAwMDAwAkBgMBmjCi+wAAAABJRU5ErkJggg==";
      }
    });
 
    // Use Intersection Observer for lazy loading
    const imageObserver = new IntersectionObserver(
      (entries) => {
        entries.forEach(entry => {
          Eif (entry.isIntersecting) {
            const img = entry.target;
            Eif (img.dataset.original) {
              img.style.transition = 'opacity 0.3s ease-in-out';
              img.style.opacity = '0';
 
              img.onload = () => {
                img.style.opacity = '1';
              };
 
              img.src = img.dataset.original;
              delete img.dataset.original;
            }
            imageObserver.unobserve(img);
          }
        });
      },
      {
        threshold: 0.1,
        rootMargin: '200px'
      }
    );
 
    images.forEach(img => {
      imageObserver.observe(img);
    });
  }
}