animflow
Version:
A lightweight, high-performance animation library for creating smooth, responsive web animations
405 lines (340 loc) • 13.7 kB
JavaScript
/*!
* 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);