/** * Scroll Animations using Intersection Observer * Tractatus Framework - Phase 3: Engagement & Interactive Features * * Provides smooth scroll-triggered animations for elements marked with .animate-on-scroll * Supports multiple animation types via data-animation attribute */ class ScrollAnimations { constructor(options = {}) { this.observerOptions = { threshold: options.threshold || 0.1, rootMargin: options.rootMargin || '0px 0px -100px 0px' }; this.animations = { 'fade-in': 'opacity-0 animate-fade-in', 'slide-up': 'opacity-0 translate-y-8 animate-slide-up', 'slide-down': 'opacity-0 -translate-y-8 animate-slide-down', 'slide-left': 'opacity-0 translate-x-8 animate-slide-left', 'slide-right': 'opacity-0 -translate-x-8 animate-slide-right', 'scale-in': 'opacity-0 scale-95 animate-scale-in', 'rotate-in': 'opacity-0 rotate-12 animate-rotate-in' }; this.init(); } init() { // Wait for DOM to be ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => this.observe()); } else { this.observe(); } console.log('[ScrollAnimations] Initialized'); } observe() { const observer = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { this.animateElement(entry.target); // Optional: unobserve after animation to improve performance // Only do this for elements without data-animate-repeat attribute if (!entry.target.hasAttribute('data-animate-repeat')) { observer.unobserve(entry.target); } } else if (entry.target.hasAttribute('data-animate-repeat')) { // Reset animation for repeatable elements this.resetElement(entry.target); } }); }, this.observerOptions); // Find all elements with .animate-on-scroll class const elements = document.querySelectorAll('.animate-on-scroll'); console.log(`[ScrollAnimations] Observing ${elements.length} elements`); elements.forEach((el, index) => { // Add stagger delay if data-stagger attribute is present if (el.hasAttribute('data-stagger')) { const delay = parseInt(el.getAttribute('data-stagger')) || (index * 100); el.style.animationDelay = `${delay}ms`; } // Apply initial animation classes based on data-animation attribute const animationType = el.getAttribute('data-animation') || 'fade-in'; if (this.animations[animationType]) { // Remove any existing animation classes Object.values(this.animations).forEach(classes => { classes.split(' ').forEach(cls => el.classList.remove(cls)); }); // Add initial state classes (will be removed when visible) const initialClasses = this.getInitialClasses(animationType); initialClasses.forEach(cls => el.classList.add(cls)); } observer.observe(el); }); } getInitialClasses(animationType) { // Return classes that represent the "before animation" state const map = { 'fade-in': ['opacity-0'], 'slide-up': ['opacity-0', 'translate-y-8'], 'slide-down': ['opacity-0', '-translate-y-8'], 'slide-left': ['opacity-0', 'translate-x-8'], 'slide-right': ['opacity-0', '-translate-x-8'], 'scale-in': ['opacity-0', 'scale-95'], 'rotate-in': ['opacity-0', 'rotate-12'] }; return map[animationType] || ['opacity-0']; } animateElement(element) { // Remove initial state classes element.classList.remove('opacity-0', 'translate-y-8', '-translate-y-8', 'translate-x-8', '-translate-x-8', 'scale-95', 'rotate-12'); // Add visible state element.classList.add('is-visible'); // Trigger custom event for other components to listen to element.dispatchEvent(new CustomEvent('scroll-animated', { bubbles: true, detail: { element } })); } resetElement(element) { // Remove visible state element.classList.remove('is-visible'); // Re-apply initial animation classes const animationType = element.getAttribute('data-animation') || 'fade-in'; const initialClasses = this.getInitialClasses(animationType); initialClasses.forEach(cls => element.classList.add(cls)); } } // Auto-initialize when script loads // Can be disabled by setting window.DISABLE_AUTO_SCROLL_ANIMATIONS = true before loading this script if (typeof window !== 'undefined' && !window.DISABLE_AUTO_SCROLL_ANIMATIONS) { window.scrollAnimations = new ScrollAnimations(); } // Export for module usage if (typeof module !== 'undefined' && module.exports) { module.exports = ScrollAnimations; }