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);
});
}
}
|