animflow
Version:
A lightweight, high-performance animation library for creating smooth, responsive web animations
252 lines (208 loc) • 8.84 kB
JavaScript
/*!
* SVG Draw Animation v2.0.0
* A simple and lightweight SVG path drawing animation library
* (c) 2025 AnimFlow Team
* MIT License
*/
(function(global) {
'use strict';
class SVGAnimator {
constructor(options = {}) {
this.options = {
duration: 10000, // 10 seconds by default
easing: 'ease-in-out',
delay: 0,
autoStart: true,
...options
};
this.pathCache = new WeakMap();
this.initialized = false;
if (this.options.autoStart) {
this.init();
}
}
// Initialize the animator
init() {
if (this.initialized) return;
// Add CSS for animations if not already added
this.addStyles();
// Wait for the DOM to be ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => this.setup());
} else {
this.setup();
}
this.initialized = true;
}
// Add required styles
addStyles() {
if (document.getElementById('svg-animator-styles')) return;
const style = document.createElement('style');
style.id = 'svg-animator-styles';
style.textContent = `
[data-anim*="draw-path"] {
opacity: 0;
animation: none;
animation-fill-mode: forwards;
}
@keyframes drawPath {
from {
stroke-dashoffset: var(--path-length);
opacity: 1;
}
to {
stroke-dashoffset: 0;
opacity: 1;
}
}
`;
document.head.appendChild(style);
}
// Setup the animator
setup() {
// Find all elements with data-anim="draw-path"
const elements = document.querySelectorAll('[data-anim*="draw-path"]');
// Initialize each element
elements.forEach(element => {
this.initElement(element);
});
// Setup intersection observer for lazy loading
this.setupIntersectionObserver();
// Setup mutation observer to watch for new elements
this.setupMutationObserver();
}
// Setup intersection observer for lazy loading
setupIntersectionObserver() {
if (!('IntersectionObserver' in window)) return;
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const element = entry.target;
if (!this.pathCache.has(element)) {
this.initElement(element);
}
observer.unobserve(element);
}
});
}, {
rootMargin: '50px 0px',
threshold: 0.01
});
// Observe all current and future elements
const observeElements = () => {
document.querySelectorAll('[data-anim*="draw-path"]:not([data-observed])').forEach(element => {
if (!this.pathCache.has(element)) {
element.setAttribute('data-observed', 'true');
observer.observe(element);
}
});
};
observeElements();
// Re-observe when new elements are added
const mutationObserver = new MutationObserver(observeElements);
mutationObserver.observe(document.body, {
childList: true,
subtree: true
});
}
// Setup mutation observer to watch for new elements
setupMutationObserver() {
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
mutation.addedNodes.forEach((node) => {
if (node.nodeType === 1) { // Element node
if (node.matches && node.matches('[data-anim*="draw-path"]')) {
this.initElement(node);
}
const elements = node.querySelectorAll ? node.querySelectorAll('[data-anim*="draw-path"]') : [];
elements.forEach(element => this.initElement(element));
}
});
});
});
observer.observe(document.body, {
childList: true,
subtree: true
});
}
// Initialize a single element
initElement(element) {
if (this.pathCache.has(element)) return;
// Calculate path length if not already set
const length = element.getTotalLength();
this.pathCache.set(element, length);
// Set initial styles
element.style.setProperty('--path-length', length + 'px');
element.style.strokeDasharray = length;
element.style.strokeDashoffset = length;
// Get animation options from data attributes
const duration = element.getAttribute('data-anim-duration') || this.options.duration;
const easing = element.getAttribute('data-anim-easing') || this.options.easing;
const delay = element.getAttribute('data-anim-delay') || this.options.delay;
// Start the animation
this.animatePath(element, {
duration: parseInt(duration, 10),
easing: easing,
delay: parseInt(delay, 10)
});
}
// Animate the path
animatePath(path, options = {}) {
const {
duration = this.options.duration,
easing = this.options.easing,
delay = this.options.delay
} = options;
// Reset any existing animations
path.style.animation = 'none';
// Force reflow
path.offsetHeight;
// Start the animation
path.style.animation = `drawPath ${duration}ms ${easing} ${delay}ms forwards`;
}
// Reset the path to its initial state
resetPath(path) {
if (this.pathCache.has(path)) {
const length = this.pathCache.get(path);
// Reset styles
path.style.animation = 'none';
path.style.strokeDasharray = length;
path.style.strokeDashoffset = length;
path.style.opacity = '0';
// Force reflow
path.offsetHeight;
}
}
// Reset and redraw a path
redrawPath(path, options = {}) {
this.resetPath(path);
// Force reflow
path.offsetHeight;
// Redraw with new options
this.animatePath(path, {
duration: options.duration || path.getAttribute('data-anim-duration') || this.options.duration,
easing: options.easing || path.getAttribute('data-anim-easing') || this.options.easing,
delay: options.delay || path.getAttribute('data-anim-delay') || this.options.delay
});
}
}
// Auto-initialize if not in a module environment
if (typeof window !== 'undefined' && window.document) {
// Create a global instance
window.SVGAnimator = new SVGAnimator();
}
// Export for different module systems
if (typeof define === 'function' && define.amd) {
// AMD
define([], () => SVGAnimator);
} else if (typeof exports !== 'undefined') {
// CommonJS
if (typeof module !== 'undefined' && module.exports) {
module.exports = SVGAnimator;
}
} else if (typeof global !== 'undefined') {
// Browser global
global.SVGAnimator = SVGAnimator;
}
return SVGAnimator;
})(typeof window !== 'undefined' ? window : this);