(function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : typeof define === 'function' && define.amd ? define(factory) : (global.animal = factory()); }(this, (function () { 'use strict'; // --- Utils --- const isArr = (a) => Array.isArray(a); const isStr = (s) => typeof s === 'string'; const isFunc = (f) => typeof f === 'function'; const isNil = (v) => v === undefined || v === null; const isSVG = (el) => el instanceof SVGElement; const toArray = (targets) => { if (isArr(targets)) return targets; if (isStr(targets)) return Array.from(document.querySelectorAll(targets)); if (targets instanceof NodeList) return Array.from(targets); if (targets instanceof Element || targets instanceof Window) return [targets]; return []; }; const $ = (targets) => toArray(targets); const UNITLESS_KEYS = ['opacity', 'scale', 'scaleX', 'scaleY', 'scaleZ', 'zIndex', 'fontWeight', 'strokeDashoffset', 'strokeDasharray', 'strokeWidth']; const getUnit = (val, prop) => { if (UNITLESS_KEYS.includes(prop)) return ''; const split = /[+-]?\d*\.?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?(%|px|pt|em|rem|in|cm|mm|ex|ch|pc|vw|vh|vmin|vmax|deg|rad|turn)?$/.exec(val); return split ? split[1] : undefined; }; // --- Spring Physics (Simplified) --- // Returns an array of [time, value] or just value for WAAPI linear easing function spring({ stiffness = 100, damping = 10, mass = 1, velocity = 0, precision = 0.01 } = {}) { const values = []; let t = 0; const timeStep = 1 / 60; // 60fps simulation let current = 0; let v = velocity; const target = 1; let running = true; while (running && t < 10) { // Safety break at 10s const fSpring = -stiffness * (current - target); const fDamper = -damping * v; const a = (fSpring + fDamper) / mass; v += a * timeStep; current += v * timeStep; values.push(current); t += timeStep; if (Math.abs(current - target) < precision && Math.abs(v) < precision) { running = false; } } if (values[values.length - 1] !== 1) values.push(1); return values; } // --- WAAPI Core --- const TRANSFORMS = ['translateX', 'translateY', 'translateZ', 'rotate', 'rotateX', 'rotateY', 'rotateZ', 'scale', 'scaleX', 'scaleY', 'scaleZ', 'skew', 'skewX', 'skewY', 'perspective', 'x', 'y']; const ALIASES = { x: 'translateX', y: 'translateY', z: 'translateZ' }; // Basic rAF loop for non-WAAPI props or fallback class RafEngine { constructor() { this.animations = []; this.tick = this.tick.bind(this); this.running = false; } add(anim) { this.animations.push(anim); if (!this.running) { this.running = true; requestAnimationFrame(this.tick); } } remove(anim) { this.animations = this.animations.filter(a => a !== anim); } tick(t) { const now = t; this.animations = this.animations.filter(anim => { return anim.tick(now); // return true to keep }); if (this.animations.length) { requestAnimationFrame(this.tick); } else { this.running = false; } } } const rafEngine = new RafEngine(); // --- Main Animation Logic --- function animate(targets, params) { const elements = toArray(targets); const { duration = 1000, delay = 0, easing = 'ease-out', direction = 'normal', fill = 'forwards', loop = 1, endDelay = 0, autoplay = true, update, // callback begin, // callback complete // callback } = params; const animatableProps = {}; const waapiProps = {}; const jsProps = {}; let isSpring = false; let springValues = null; if (typeof easing === 'object' && !Array.isArray(easing)) { isSpring = true; springValues = spring(easing); } Object.keys(params).forEach(key => { if (['duration', 'delay', 'easing', 'direction', 'fill', 'loop', 'endDelay', 'autoplay', 'update', 'begin', 'complete'].includes(key)) return; let prop = ALIASES[key] || key; const isTransform = TRANSFORMS.includes(prop) || TRANSFORMS.includes(key); const isCss = !isTransform && (key === 'opacity' || key === 'filter'); // Simplified check const val = params[key]; if (isTransform || isCss) { waapiProps[key] = val; } else { jsProps[key] = val; } }); // Create animations but don't play if autoplay is false const animations = []; const promises = elements.map(el => { // 1. WAAPI Animation let waapiAnim = null; let waapiPromise = Promise.resolve(); if (Object.keys(waapiProps).length > 0) { const generateFrames = (propValues) => { if (isSpring && springValues) { return springValues.map(v => { const frame = {}; Object.keys(propValues).forEach(k => { const targetVal = parseFloat(propValues[k]); const unit = getUnit(propValues[k], k) || (k.startsWith('rotate') ? 'deg' : (UNITLESS_KEYS.includes(k) ? '' : 'px')); let startVal = 0; if (k.startsWith('scale')) startVal = 1; if (k === 'opacity') startVal = parseFloat(getComputedStyle(el).opacity); if (Array.isArray(propValues[k])) { startVal = parseFloat(propValues[k][0]); const endVal = parseFloat(propValues[k][1]); const current = startVal + (endVal - startVal) * v; frame[k] = current + unit; } else { const endVal = targetVal; const current = startVal + (endVal - startVal) * v; frame[k] = current + unit; } }); return frame; }); } else { const frame0 = {}; const frame1 = {}; Object.keys(propValues).forEach(k => { let val = propValues[k]; if (Array.isArray(val)) { frame0[k] = val[0]; frame1[k] = val[1]; } else { frame1[k] = val; } }); return [frame0, frame1]; } }; let rawFrames = generateFrames(waapiProps); const finalFrames = rawFrames.map(f => { const final = {}; let transformStr = ''; Object.keys(f).forEach(k => { const isT = TRANSFORMS.includes(ALIASES[k] || k); if (isT) { let key = ALIASES[k] || k; if (key === 'x') key = 'translateX'; if (key === 'y') key = 'translateY'; if (key === 'z') key = 'translateZ'; let val = f[k]; if (typeof val === 'number') { if (key.startsWith('scale')) { val = val + ''; } else if (key.startsWith('rotate') || key.startsWith('skew')) { val = val + 'deg'; } else { val = val + 'px'; } } transformStr += `${key}(${val}) `; } else { final[k] = f[k]; } }); if (transformStr) final.transform = transformStr.trim(); return final; }); if (Object.keys(finalFrames[0]).length === 0) finalFrames.shift(); const opts = { duration: isSpring ? (springValues.length * 1000 / 60) : duration, delay, fill, iterations: loop, easing: isSpring ? 'linear' : easing }; const animation = el.animate(finalFrames, opts); if (!autoplay) animation.pause(); waapiAnim = animation; waapiPromise = animation.finished; animations.push(waapiAnim); } // 2. JS Animation (Fallback / Attributes) let jsPromise = Promise.resolve(); if (Object.keys(jsProps).length > 0) { jsPromise = new Promise(resolve => { // For JS engine to support manual seek/scroll, we need to structure it differently. // But for this lite version, we focus on WAAPI for scroll linking. // JS fallback will just run normally if autoplay. if (!autoplay) return; // JS engine simplified doesn't support pause/seek easily yet const startTime = performance.now() + delay; const initialValues = {}; // Setup Object.keys(jsProps).forEach(k => { if (k === 'scrollTop' || k === 'scrollLeft') { initialValues[k] = el[k]; } else if (k in el.style) { initialValues[k] = parseFloat(el.style[k]) || 0; } else if (isSVG(el) && el.hasAttribute(k)) { initialValues[k] = parseFloat(el.getAttribute(k)) || 0; } else { initialValues[k] = 0; } }); const tick = (now) => { if (now < startTime) return true; const elapsed = now - startTime; let progress = Math.min(elapsed / duration, 1); Object.keys(jsProps).forEach(k => { const target = jsProps[k]; let start = initialValues[k]; let end = isArr(target) ? target[1] : target; if (isArr(target)) start = target[0]; const val = start + (end - start) * progress; if (k === 'scrollTop' || k === 'scrollLeft') { el[k] = val; } else if (k in el.style) { el.style[k] = val; } else if (isSVG(el)) { const attr = k.replace(/[A-Z]/g, m => '-' + m.toLowerCase()); el.setAttribute(attr, val); } else { el[k] = val; } if (update) update({ target: el, progress, value: val }); }); if (progress < 1) return true; if (complete) complete(el); resolve(); return false; }; rafEngine.add({ tick }); }); } return Promise.all([waapiPromise, jsPromise]); }); const masterPromise = Promise.all(promises); masterPromise.then = (fn) => Promise.all(promises).then(fn); // Attach animations to promise for external control (like scroll) masterPromise.animations = animations; return masterPromise; } // --- SVG Draw --- function svgDraw(targets, params = {}) { const elements = toArray(targets); elements.forEach(el => { if (!isSVG(el)) return; const len = el.getTotalLength ? el.getTotalLength() : 0; el.style.strokeDasharray = len; el.style.strokeDashoffset = len; animate(el, { strokeDashoffset: [len, 0], ...params }); }); } // --- In View --- function inViewAnimate(targets, params, options = {}) { const elements = toArray(targets); const observer = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { animate(entry.target, params); if (options.once !== false) observer.unobserve(entry.target); } }); }, { threshold: options.threshold || 0.1 }); elements.forEach(el => observer.observe(el)); } // --- Scroll Linked --- function scroll(animationPromise, options = {}) { // options: container (default window), range [start, end] (default viewport logic) const container = options.container || window; const target = options.target || document.body; // Element to track for progress // If passing an animation promise, we control its WAAPI animations const anims = animationPromise.animations || []; if (!anims.length) return; const updateScroll = () => { let progress = 0; if (container === window) { const scrollY = window.scrollY; const winH = window.innerHeight; const docH = document.body.scrollHeight; // Simple progress: how far down the page (0 to 1) // Or element based? // Motion One defaults to element entering view. if (options.target) { const rect = options.target.getBoundingClientRect(); const start = winH; const end = -rect.height; // progress 0 when rect.top == start (just entering) // progress 1 when rect.top == end (just left) const totalDistance = start - end; const currentDistance = start - rect.top; progress = currentDistance / totalDistance; } else { // Whole page scroll progress = scrollY / (docH - winH); } } // Clamp progress = Math.max(0, Math.min(1, progress)); anims.forEach(anim => { if (anim.effect) { const dur = anim.effect.getComputedTiming().duration; anim.currentTime = dur * progress; } }); }; window.addEventListener('scroll', updateScroll); updateScroll(); // Initial return () => window.removeEventListener('scroll', updateScroll); } // --- Timeline --- function timeline(defaults = {}) { const realTimeApi = { currentTime: 0, add: (targets, params, offset) => { const animParams = { ...defaults, ...params }; let start = realTimeApi.currentTime; if (offset !== undefined) { if (isStr(offset) && offset.startsWith('-=')) start -= parseFloat(offset.slice(2)); else if (isStr(offset) && offset.startsWith('+=')) start += parseFloat(offset.slice(2)); else if (typeof offset === 'number') start = offset; } if (start <= 0) { animate(targets, animParams); } else { setTimeout(() => { animate(targets, animParams); }, start); } const dur = animParams.duration || 1000; realTimeApi.currentTime = Math.max(realTimeApi.currentTime, start + dur); return realTimeApi; } }; return realTimeApi; } return { animate, timeline, svgDraw, inViewAnimate, spring, scroll, $ }; })));