UNPKG

animflow

Version:

A lightweight, high-performance animation library for creating smooth, responsive web animations

405 lines (340 loc) 13.7 kB
/*! * AnimFlow v2.0.0 * A lightweight, high-performance animation library * (c) 2025 AnimFlow Team * MIT License * https://github.com/AnimFlowOrg/animflow */ ;(function(global) { 'use strict'; /** * AnimFlow - A lightweight library for smooth scroll-based animations * @version 1.5.0 */ // Resource Manager for lazy loading and optimization const ResourceManager = { preloadQueue: new Map(), loadedResources: new WeakMap(), imageObserver: null, init() { this.setupImageObserver(); }, setupImageObserver() { this.imageObserver = new IntersectionObserver( (entries) => this.handleImageIntersection(entries), { rootMargin: '50px 0px' } ); }, handleImageIntersection(entries) { entries.forEach(entry => { if (entry.isIntersecting) { const img = entry.target; if (img.dataset.src) { this.loadImage(img); } this.imageObserver.unobserve(img); } }); }, loadImage(img) { const src = img.dataset.src; img.src = src; img.removeAttribute('data-src'); this.loadedResources.set(img, true); }, observeImage(img) { if (!this.loadedResources.has(img)) { this.imageObserver.observe(img); } } }; // SVG Optimizer for better performance (Temporarily Disabled) const SVGOptimizer = { pathCache: new WeakMap(), optimizePath() { // SVG functionality temporarily disabled return 0; }, animatePath() { // SVG functionality temporarily disabled } } // Virtual Scroll Manager const VirtualScroller = { containers: new WeakMap(), init(container, itemHeight) { if (this.containers.has(container)) return; const config = { itemHeight, items: Array.from(container.children), visibleItems: new Set(), totalHeight: 0 }; this.containers.set(container, config); this.setupContainer(container); }, setupContainer(container) { const config = this.containers.get(container); config.totalHeight = config.items.length * config.itemHeight; container.style.height = config.totalHeight + 'px'; this.updateVisibleItems(container); container.addEventListener('scroll', () => this.updateVisibleItems(container)); }, updateVisibleItems(container) { const config = this.containers.get(container); const scrollTop = container.scrollTop; const containerHeight = container.clientHeight; const startIndex = Math.floor(scrollTop / config.itemHeight); const endIndex = Math.ceil((scrollTop + containerHeight) / config.itemHeight); config.items.forEach((item, index) => { if (index >= startIndex && index <= endIndex) { if (!config.visibleItems.has(item)) { item.style.transform = `translateY(${index * config.itemHeight}px)`; container.appendChild(item); config.visibleItems.add(item); } } else { if (config.visibleItems.has(item)) { item.remove(); config.visibleItems.delete(item); } } }); } }; class AnimFlow { constructor(options = {}) { this.options = { ...AnimFlow.defaults, ...options }; this.elements = new Set(); this.observer = null; this.batchQueue = new Set(); this.weakCache = new WeakMap(); this.eventHandlers = new Map(); // Initialize managers ResourceManager.init(); this.setupBatchProcessor(); this.setupEventSystem(); } static init(options = {}) { if (!AnimFlow.instance) { AnimFlow.instance = new AnimFlow(options); } return AnimFlow.instance.init(); } init() { if (this.options.disableOnLowPerformance && this.isLowPerformanceDevice()) { console.info('AnimFlow: Animations disabled due to low performance device detection'); return this; } this.setupIntersectionObserver(); this.setupElements(); return this; } setupEventSystem() { this.eventHandlers.set('animationStart', new Set()); this.eventHandlers.set('animationEnd', new Set()); this.eventHandlers.set('animationCancel', new Set()); } on(event, handler) { if (this.eventHandlers.has(event)) { this.eventHandlers.get(event).add(handler); } return this; } off(event, handler) { if (this.eventHandlers.has(event)) { this.eventHandlers.get(event).delete(handler); } return this; } trigger(event, data) { if (this.eventHandlers.has(event)) { this.eventHandlers.get(event).forEach(handler => handler(data)); } return this; } setupBatchProcessor() { this.processBatch = () => { this.batchQueue.forEach(animation => { if (animation.element && document.body.contains(animation.element)) { this.processAnimation(animation); } }); this.batchQueue.clear(); this.animationFrame = null; }; } setupIntersectionObserver() { this.observer = new IntersectionObserver( (entries) => { entries.forEach(entry => this.handleIntersection(entry)); }, { root: this.options.root, threshold: this.options.threshold, rootMargin: this.options.rootMargin } ); } setupElements() { document.querySelectorAll('[data-anim]').forEach(element => { this.setupElement(element); }); } setupElement(element) { const effect = element.getAttribute('data-anim'); if (!effect) return; // Improve performance using GPU element.style.willChange = 'transform, opacity'; element.style.backfaceVisibility = 'hidden'; // Set initial state element.style.opacity = '0'; // Check element type and apply appropriate optimizations if (element instanceof SVGElement) { this.setupSVGElement(element, effect); } else if (element instanceof HTMLImageElement && !element.complete) { this.setupLazyImage(element, effect); } else { this.setupRegularElement(element, effect); } // Add support for sequential animations const sequence = element.getAttribute('data-anim-sequence'); if (sequence) { element.style.animationDelay = `${parseInt(sequence) * 100}ms`; } this.elements.add(element); this.observer.observe(element); } setupSVGElement(element, effect) { if (effect.includes('draw')) { SVGOptimizer.optimizePath(element); } else { this.setupRegularElement(element, effect); } } setupLazyImage(element, effect) { const onLoad = () => { this.setupRegularElement(element, effect); element.removeEventListener('load', onLoad); }; element.addEventListener('load', onLoad); ResourceManager.observeImage(element); } setupRegularElement(element, effect) { if (effect.includes('slide')) { element.style.transform = 'translateY(30px)'; } else if (effect.includes('scale')) { element.style.transform = 'scale(0.8)'; } const duration = element.getAttribute('data-anim-duration') || this.options.duration; const easing = element.getAttribute('data-anim-easing') || this.options.easing; element.style.transition = `opacity ${duration}ms ${easing}, transform ${duration}ms ${easing}`; } handleIntersection(entry) { const element = entry.target; if (entry.isIntersecting) { this.trigger('animationStart', { element }); requestAnimationFrame(() => { if (element instanceof SVGElement && element.getAttribute('data-anim').includes('draw')) { SVGOptimizer.animatePath(element, this.options.duration, this.options.easing); } else { element.style.opacity = '1'; element.style.transform = 'none'; } }); if (this.options.repeat === 'once' || this.options.once) { this.observer.unobserve(element); this.elements.delete(element); this.trigger('animationEnd', { element }); } } else if (this.options.repeat === 'always') { this.trigger('animationCancel', { element }); element.style.opacity = '0'; const effect = element.getAttribute('data-anim'); if (effect.includes('slide')) { element.style.transform = 'translateY(30px)'; } else if (effect.includes('scale')) { element.style.transform = 'scale(0.8)'; } else if (effect.includes('draw') && element instanceof SVGElement) { SVGOptimizer.optimizePath(element); } } } // Add support for synchronized animation syncAnimate(elements, options = {}) { const group = new Set(elements); const groupOptions = { ...this.options, ...options }; elements.forEach(element => { this.setupElement(element); element.style.transition = `opacity ${groupOptions.duration}ms ${groupOptions.easing}, transform ${groupOptions.duration}ms ${groupOptions.easing}`; }); requestAnimationFrame(() => { elements.forEach(element => { element.style.opacity = '1'; element.style.transform = 'none'; }); }); return { reset: () => { elements.forEach(element => { element.style.opacity = '0'; const effect = element.getAttribute('data-anim'); if (effect.includes('slide')) { element.style.transform = 'translateY(30px)'; } else if (effect.includes('scale')) { element.style.transform = 'scale(0.8)'; } }); } }; } // Add support for sequential animation sequenceAnimate(elements, options = {}) { const delay = options.delay || 100; elements.forEach((element, index) => { element.setAttribute('data-anim-sequence', index.toString()); this.setupElement(element); }); } // Improve performance for older devices isLowPerformanceDevice() { return ( !window.requestAnimationFrame || /Mobile|Android|iPhone|iPad|iPod|Windows Phone/i.test(navigator.userAgent) || (navigator.hardwareConcurrency && navigator.hardwareConcurrency <= 2) ); } processAnimation(animation) { const { element, effect } = animation; element.style.opacity = '1'; element.style.transform = 'none'; } } // Default options AnimFlow.defaults = { root: null, threshold: 0.1, rootMargin: '0px', once: false, repeat: 'always', // 'always', 'once', 'none' duration: 1000, easing: 'cubic-bezier(0.4, 0, 0.2, 1)', disableOnLowPerformance: false, enableLazyLoading: true, enableVirtualScroll: false, virtualScrollItemHeight: 50, sequenceDelay: 100, rtl: false, performance: { useGPU: true, batchSize: 10, debounceTime: 10 } }; // Export to global scope if (typeof module !== 'undefined' && module.exports) { module.exports = AnimFlow; } else { global.AnimFlow = AnimFlow; } })(typeof window !== 'undefined' ? window : this);