animal.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378
  1. (function (global, factory) {
  2. typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
  3. typeof define === 'function' && define.amd ? define(factory) :
  4. (global.animal = factory());
  5. }(this, (function () {
  6. 'use strict';
  7. // --- Utils ---
  8. const isArr = (a) => Array.isArray(a);
  9. const isStr = (s) => typeof s === 'string';
  10. const isFunc = (f) => typeof f === 'function';
  11. const isNil = (v) => v === undefined || v === null;
  12. const isSVG = (el) => el instanceof SVGElement;
  13. const toArray = (targets) => {
  14. if (isArr(targets)) return targets;
  15. if (isStr(targets)) return Array.from(document.querySelectorAll(targets));
  16. if (targets instanceof NodeList) return Array.from(targets);
  17. if (targets instanceof Element || targets instanceof Window) return [targets];
  18. return [];
  19. };
  20. const UNITLESS_KEYS = ['opacity', 'scale', 'scaleX', 'scaleY', 'scaleZ', 'zIndex', 'fontWeight', 'strokeDashoffset', 'strokeDasharray', 'strokeWidth'];
  21. const getUnit = (val, prop) => {
  22. if (UNITLESS_KEYS.includes(prop)) return '';
  23. 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);
  24. return split ? split[1] : undefined;
  25. };
  26. // --- Spring Physics (Simplified) ---
  27. // Returns an array of [time, value] or just value for WAAPI linear easing
  28. function spring({ stiffness = 100, damping = 10, mass = 1, velocity = 0, precision = 0.01 } = {}) {
  29. const values = [];
  30. let t = 0;
  31. const timeStep = 1 / 60; // 60fps simulation
  32. let current = 0;
  33. let v = velocity;
  34. const target = 1;
  35. let running = true;
  36. while (running && t < 10) { // Safety break at 10s
  37. const fSpring = -stiffness * (current - target);
  38. const fDamper = -damping * v;
  39. const a = (fSpring + fDamper) / mass;
  40. v += a * timeStep;
  41. current += v * timeStep;
  42. values.push(current);
  43. t += timeStep;
  44. if (Math.abs(current - target) < precision && Math.abs(v) < precision) {
  45. running = false;
  46. }
  47. }
  48. if (values[values.length - 1] !== 1) values.push(1);
  49. return values;
  50. }
  51. // --- WAAPI Core ---
  52. const TRANSFORMS = ['translateX', 'translateY', 'translateZ', 'rotate', 'rotateX', 'rotateY', 'rotateZ', 'scale', 'scaleX', 'scaleY', 'scaleZ', 'skew', 'skewX', 'skewY', 'perspective', 'x', 'y'];
  53. const ALIASES = {
  54. x: 'translateX',
  55. y: 'translateY',
  56. z: 'translateZ'
  57. };
  58. // Basic rAF loop for non-WAAPI props or fallback
  59. class RafEngine {
  60. constructor() {
  61. this.animations = [];
  62. this.tick = this.tick.bind(this);
  63. this.running = false;
  64. }
  65. add(anim) {
  66. this.animations.push(anim);
  67. if (!this.running) {
  68. this.running = true;
  69. requestAnimationFrame(this.tick);
  70. }
  71. }
  72. tick(t) {
  73. const now = t;
  74. this.animations = this.animations.filter(anim => {
  75. return anim.tick(now); // return true to keep
  76. });
  77. if (this.animations.length) {
  78. requestAnimationFrame(this.tick);
  79. } else {
  80. this.running = false;
  81. }
  82. }
  83. }
  84. const rafEngine = new RafEngine();
  85. // --- Main Animation Logic ---
  86. function animate(targets, params) {
  87. const elements = toArray(targets);
  88. const {
  89. duration = 1000,
  90. delay = 0,
  91. easing = 'ease-out',
  92. direction = 'normal',
  93. fill = 'forwards',
  94. loop = 1,
  95. endDelay = 0,
  96. autoplay = true,
  97. update, // callback
  98. begin, // callback
  99. complete // callback
  100. } = params;
  101. const animatableProps = {};
  102. const waapiProps = {};
  103. const jsProps = {};
  104. let isSpring = false;
  105. let springValues = null;
  106. if (typeof easing === 'object' && !Array.isArray(easing)) {
  107. isSpring = true;
  108. springValues = spring(easing);
  109. }
  110. Object.keys(params).forEach(key => {
  111. if (['duration', 'delay', 'easing', 'direction', 'fill', 'loop', 'endDelay', 'autoplay', 'update', 'begin', 'complete'].includes(key)) return;
  112. let prop = ALIASES[key] || key;
  113. const isTransform = TRANSFORMS.includes(prop) || TRANSFORMS.includes(key);
  114. const isCss = !isTransform && (key === 'opacity' || key === 'filter'); // Simplified check
  115. const val = params[key];
  116. if (isTransform || isCss) {
  117. waapiProps[key] = val;
  118. } else {
  119. jsProps[key] = val;
  120. }
  121. });
  122. const promises = elements.map(el => {
  123. // 1. WAAPI Animation
  124. let waapiAnim = null;
  125. let waapiPromise = Promise.resolve();
  126. if (Object.keys(waapiProps).length > 0) {
  127. const generateFrames = (propValues) => {
  128. if (isSpring && springValues) {
  129. return springValues.map(v => {
  130. const frame = {};
  131. Object.keys(propValues).forEach(k => {
  132. const targetVal = parseFloat(propValues[k]);
  133. const unit = getUnit(propValues[k], k) || (k.startsWith('rotate') ? 'deg' : (UNITLESS_KEYS.includes(k) ? '' : 'px'));
  134. let startVal = 0;
  135. if (k.startsWith('scale')) startVal = 1;
  136. if (k === 'opacity') startVal = parseFloat(getComputedStyle(el).opacity);
  137. if (Array.isArray(propValues[k])) {
  138. startVal = parseFloat(propValues[k][0]);
  139. const endVal = parseFloat(propValues[k][1]);
  140. const current = startVal + (endVal - startVal) * v;
  141. frame[k] = current + unit;
  142. } else {
  143. const endVal = targetVal;
  144. const current = startVal + (endVal - startVal) * v;
  145. frame[k] = current + unit;
  146. }
  147. });
  148. return frame;
  149. });
  150. } else {
  151. const frame0 = {};
  152. const frame1 = {};
  153. Object.keys(propValues).forEach(k => {
  154. let val = propValues[k];
  155. if (Array.isArray(val)) {
  156. frame0[k] = val[0];
  157. frame1[k] = val[1];
  158. } else {
  159. frame1[k] = val;
  160. }
  161. });
  162. return [frame0, frame1];
  163. }
  164. };
  165. let rawFrames = generateFrames(waapiProps);
  166. const finalFrames = rawFrames.map(f => {
  167. const final = {};
  168. let transformStr = '';
  169. Object.keys(f).forEach(k => {
  170. const isT = TRANSFORMS.includes(ALIASES[k] || k);
  171. if (isT) {
  172. let key = ALIASES[k] || k;
  173. if (key === 'x') key = 'translateX';
  174. if (key === 'y') key = 'translateY';
  175. if (key === 'z') key = 'translateZ';
  176. let val = f[k];
  177. if (typeof val === 'number') {
  178. if (key.startsWith('scale')) {
  179. val = val + '';
  180. } else if (key.startsWith('rotate') || key.startsWith('skew')) {
  181. val = val + 'deg';
  182. } else {
  183. val = val + 'px';
  184. }
  185. }
  186. transformStr += `${key}(${val}) `;
  187. } else {
  188. final[k] = f[k];
  189. }
  190. });
  191. if (transformStr) final.transform = transformStr.trim();
  192. return final;
  193. });
  194. if (Object.keys(finalFrames[0]).length === 0) finalFrames.shift();
  195. const opts = {
  196. duration: isSpring ? (springValues.length * 1000 / 60) : duration,
  197. delay,
  198. fill,
  199. iterations: loop,
  200. easing: isSpring ? 'linear' : easing
  201. };
  202. const animation = el.animate(finalFrames, opts);
  203. waapiAnim = animation;
  204. waapiPromise = animation.finished;
  205. }
  206. // 2. JS Animation (Fallback / Attributes)
  207. let jsPromise = Promise.resolve();
  208. if (Object.keys(jsProps).length > 0) {
  209. jsPromise = new Promise(resolve => {
  210. const startTime = performance.now() + delay;
  211. const initialValues = {};
  212. // Setup
  213. Object.keys(jsProps).forEach(k => {
  214. if (k === 'scrollTop' || k === 'scrollLeft') {
  215. initialValues[k] = el[k];
  216. } else if (k in el.style) {
  217. // For style props not in WAAPI whitelist (like strokeDashoffset)
  218. initialValues[k] = parseFloat(el.style[k]) || 0; // simplistic
  219. } else if (isSVG(el) && el.hasAttribute(k)) {
  220. initialValues[k] = parseFloat(el.getAttribute(k)) || 0;
  221. } else {
  222. initialValues[k] = 0;
  223. }
  224. });
  225. const tick = (now) => {
  226. if (now < startTime) return true;
  227. const elapsed = now - startTime;
  228. let progress = Math.min(elapsed / duration, 1);
  229. Object.keys(jsProps).forEach(k => {
  230. const target = jsProps[k];
  231. let start = initialValues[k];
  232. let end = isArr(target) ? target[1] : target;
  233. if (isArr(target)) start = target[0];
  234. const val = start + (end - start) * progress;
  235. if (k === 'scrollTop' || k === 'scrollLeft') {
  236. el[k] = val;
  237. } else if (k in el.style) {
  238. el.style[k] = val; // Set as style
  239. } else if (isSVG(el)) {
  240. // Kebab-case conversion for setAttribute
  241. const attr = k.replace(/[A-Z]/g, m => '-' + m.toLowerCase());
  242. el.setAttribute(attr, val);
  243. } else {
  244. el[k] = val;
  245. }
  246. if (update) update({ target: el, progress, value: val });
  247. });
  248. if (progress < 1) return true;
  249. if (complete) complete(el);
  250. resolve();
  251. return false;
  252. };
  253. rafEngine.add({ tick });
  254. });
  255. }
  256. return Promise.all([waapiPromise, jsPromise]);
  257. });
  258. const masterPromise = Promise.all(promises);
  259. masterPromise.then = (fn) => Promise.all(promises).then(fn);
  260. return masterPromise;
  261. }
  262. // --- SVG Draw ---
  263. function svgDraw(targets, params = {}) {
  264. const elements = toArray(targets);
  265. elements.forEach(el => {
  266. if (!isSVG(el)) return;
  267. const len = el.getTotalLength ? el.getTotalLength() : 0;
  268. el.style.strokeDasharray = len;
  269. el.style.strokeDashoffset = len;
  270. // Animate offset to 0
  271. animate(el, {
  272. strokeDashoffset: [len, 0],
  273. ...params
  274. });
  275. });
  276. }
  277. // --- In View ---
  278. function inViewAnimate(targets, params, options = {}) {
  279. const elements = toArray(targets);
  280. const observer = new IntersectionObserver((entries) => {
  281. entries.forEach(entry => {
  282. if (entry.isIntersecting) {
  283. animate(entry.target, params);
  284. if (options.once !== false) observer.unobserve(entry.target);
  285. }
  286. });
  287. }, { threshold: options.threshold || 0.1 });
  288. elements.forEach(el => observer.observe(el));
  289. }
  290. // --- Timeline ---
  291. function timeline(defaults = {}) {
  292. const realTimeApi = {
  293. currentTime: 0,
  294. add: (targets, params, offset) => {
  295. const animParams = { ...defaults, ...params };
  296. let start = realTimeApi.currentTime;
  297. if (offset !== undefined) {
  298. if (isStr(offset) && offset.startsWith('-=')) start -= parseFloat(offset.slice(2));
  299. else if (isStr(offset) && offset.startsWith('+=')) start += parseFloat(offset.slice(2));
  300. else if (typeof offset === 'number') start = offset;
  301. }
  302. // Use setTimeout to defer EXECUTION until the exact start time
  303. // This ensures that "implicit from" values are read correctly from the DOM
  304. // at the moment the animation is supposed to start.
  305. if (start <= 0) {
  306. animate(targets, animParams);
  307. } else {
  308. setTimeout(() => {
  309. animate(targets, animParams);
  310. }, start);
  311. }
  312. const dur = animParams.duration || 1000;
  313. realTimeApi.currentTime = Math.max(realTimeApi.currentTime, start + dur);
  314. return realTimeApi;
  315. }
  316. };
  317. return realTimeApi;
  318. }
  319. return {
  320. animate,
  321. timeline,
  322. svgDraw,
  323. inViewAnimate,
  324. spring
  325. };
  326. })));