| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447 |
- (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,
- $
- };
- })));
|