/** * Unified Animation System * * Consolidates all scroll animation modules into a single, unified system. * Replaces: scrollfx, parallax, scroll-timeline, scroll-loop, scroll-dependent, sticky-fade, sticky-steps */ import { Logger } from '../../core/logger.js'; import { ScrollAnimation } from './ScrollAnimation.js'; import { TimelineAnimation } from './TimelineAnimation.js'; /** * AnimationSystem - Unified animation system */ export class AnimationSystem { constructor(config = {}) { this.config = { enabled: config.enabled ?? true, useIntersectionObserver: config.useIntersectionObserver ?? true, throttleDelay: config.throttleDelay || 16, // ~60fps ...config }; this.animations = new Map(); // Map this.observers = new Map(); // Map this.scrollHandler = null; this.isScrolling = false; // Initialize if (this.config.enabled) { this.init(); } Logger.info('[AnimationSystem] Initialized', { enabled: this.config.enabled, useIntersectionObserver: this.config.useIntersectionObserver }); } /** * Create a new AnimationSystem instance */ static create(config = {}) { return new AnimationSystem(config); } /** * Initialize animation system */ init() { // Set up scroll handler if (!this.config.useIntersectionObserver) { this.setupScrollHandler(); } // Auto-initialize elements with data attributes this.autoInitialize(); } /** * Set up scroll handler */ setupScrollHandler() { let ticking = false; this.scrollHandler = () => { if (!ticking) { window.requestAnimationFrame(() => { this.updateAnimations(); ticking = false; }); ticking = true; } }; window.addEventListener('scroll', this.scrollHandler, { passive: true }); } /** * Auto-initialize elements with data attributes */ autoInitialize() { // Fade in on scroll (scrollfx) this.initializeFadeIn(); // Parallax this.initializeParallax(); // Scroll timeline this.initializeTimeline(); // Sticky fade this.initializeStickyFade(); // Sticky steps this.initializeStickySteps(); } /** * Initialize fade-in animations (scrollfx) */ initializeFadeIn() { const elements = document.querySelectorAll('.fade-in-on-scroll, .zoom-in, [data-animate="fade-in"]'); elements.forEach(element => { this.registerAnimation(element, { type: 'fade-in', offset: parseFloat(element.dataset.offset) || 0.85, delay: parseFloat(element.dataset.delay) || 0, once: element.dataset.once !== 'false' }); }); } /** * Initialize parallax animations */ initializeParallax() { const elements = document.querySelectorAll('[data-parallax], .parallax'); elements.forEach(element => { const speed = parseFloat(element.dataset.parallax || element.dataset.speed) || 0.5; this.registerAnimation(element, { type: 'parallax', speed }); }); } /** * Initialize timeline animations (scroll-timeline) */ initializeTimeline() { const elements = document.querySelectorAll('[data-scroll-timeline], [data-scroll-step]'); elements.forEach(element => { this.registerAnimation(element, { type: 'timeline', steps: element.dataset.scrollSteps ? parseInt(element.dataset.scrollSteps) : null, triggerPoint: parseFloat(element.dataset.triggerPoint) || 0.4 }); }); } /** * Initialize sticky fade animations */ initializeStickyFade() { const elements = document.querySelectorAll('[data-sticky-fade], .sticky-fade'); elements.forEach(element => { this.registerAnimation(element, { type: 'sticky-fade', fadeStart: parseFloat(element.dataset.fadeStart) || 0, fadeEnd: parseFloat(element.dataset.fadeEnd) || 1 }); }); } /** * Initialize sticky steps animations */ initializeStickySteps() { const elements = document.querySelectorAll('[data-sticky-steps], .sticky-steps'); elements.forEach(element => { const steps = element.dataset.stickySteps ? parseInt(element.dataset.stickySteps) : 3; this.registerAnimation(element, { type: 'sticky-steps', steps }); }); } /** * Register an animation */ registerAnimation(element, config) { if (this.animations.has(element)) { Logger.warn('[AnimationSystem] Animation already registered for element', element); return; } let animation; switch (config.type) { case 'fade-in': case 'zoom-in': animation = new ScrollAnimation(element, { type: config.type, offset: config.offset || 0.85, delay: config.delay || 0, once: config.once !== false }); break; case 'parallax': animation = new ScrollAnimation(element, { type: 'parallax', speed: config.speed || 0.5 }); break; case 'timeline': animation = new TimelineAnimation(element, { steps: config.steps, triggerPoint: config.triggerPoint || 0.4 }); break; case 'sticky-fade': animation = new ScrollAnimation(element, { type: 'sticky-fade', fadeStart: config.fadeStart || 0, fadeEnd: config.fadeEnd || 1 }); break; case 'sticky-steps': animation = new ScrollAnimation(element, { type: 'sticky-steps', steps: config.steps || 3 }); break; default: Logger.warn('[AnimationSystem] Unknown animation type', config.type); return; } this.animations.set(element, animation); // Set up observer if using IntersectionObserver if (this.config.useIntersectionObserver) { this.setupObserver(element, animation); } Logger.debug('[AnimationSystem] Animation registered', { element, type: config.type }); } /** * Set up IntersectionObserver for element */ setupObserver(element, animation) { const observer = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { animation.enter(); } else if (!animation.config.once) { animation.exit(); } }); }, { threshold: animation.config.offset || 0.85, rootMargin: '0px' }); observer.observe(element); this.observers.set(element, observer); } /** * Update all animations (for scroll-based updates) */ updateAnimations() { this.animations.forEach((animation, element) => { if (animation.needsUpdate) { animation.update(); } }); } /** * Remove animation */ removeAnimation(element) { const animation = this.animations.get(element); if (animation) { animation.destroy(); this.animations.delete(element); } const observer = this.observers.get(element); if (observer) { observer.disconnect(); this.observers.delete(element); } } /** * Destroy animation system */ destroy() { // Remove all animations this.animations.forEach((animation, element) => { this.removeAnimation(element); }); // Remove scroll handler if (this.scrollHandler) { window.removeEventListener('scroll', this.scrollHandler); } Logger.info('[AnimationSystem] Destroyed'); } }