|
|
@@ -0,0 +1,378 @@
|
|
|
+(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 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);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ 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;
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ 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);
|
|
|
+ waapiAnim = animation;
|
|
|
+ waapiPromise = animation.finished;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 2. JS Animation (Fallback / Attributes)
|
|
|
+ let jsPromise = Promise.resolve();
|
|
|
+ if (Object.keys(jsProps).length > 0) {
|
|
|
+ jsPromise = new Promise(resolve => {
|
|
|
+ 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) {
|
|
|
+ // For style props not in WAAPI whitelist (like strokeDashoffset)
|
|
|
+ initialValues[k] = parseFloat(el.style[k]) || 0; // simplistic
|
|
|
+ } 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; // Set as style
|
|
|
+ } else if (isSVG(el)) {
|
|
|
+ // Kebab-case conversion for setAttribute
|
|
|
+ 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);
|
|
|
+ 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 offset to 0
|
|
|
+ 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));
|
|
|
+ }
|
|
|
+
|
|
|
+ // --- 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;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Use setTimeout to defer EXECUTION until the exact start time
|
|
|
+ // This ensures that "implicit from" values are read correctly from the DOM
|
|
|
+ // at the moment the animation is supposed to start.
|
|
|
+ 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
|
|
|
+ };
|
|
|
+
|
|
|
+})));
|