xjs.js 61 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826
  1. // xjs.js - combined single-file build (animal + layer)
  2. // Generated from source files in this repo.
  3. // Usage:
  4. // <script src="xjs.js"></script>
  5. // Globals:
  6. // - window.xjs (primary, same as animal)
  7. // - window.animal (compat)
  8. // - window.Layer
  9. // Optional:
  10. // - set window.XJS_GLOBAL_DOLLAR = true before loading to also export window.$ (only if not already defined)
  11. (function(){
  12. var __GLOBAL__ = (typeof window !== 'undefined') ? window : (typeof globalThis !== 'undefined' ? globalThis : this);
  13. /* --- layer.js --- */
  14. (function (global, factory) {
  15. const LayerClass = factory();
  16. // Allow usage as `Layer({...})` or `new Layer()`
  17. // But Layer is a class. We can wrap it in a proxy or factory function.
  18. function LayerFactory(options) {
  19. if (options && typeof options === 'object') {
  20. return LayerClass.$(options);
  21. }
  22. return new LayerClass();
  23. }
  24. // Copy static methods
  25. Object.assign(LayerFactory, LayerClass);
  26. // Also copy prototype for instanceof checks if needed (though tricky with factory)
  27. LayerFactory.prototype = LayerClass.prototype;
  28. typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = LayerFactory :
  29. typeof define === 'function' && define.amd ? define(() => LayerFactory) :
  30. (global.Layer = LayerFactory);
  31. }(__GLOBAL__, (function () {
  32. 'use strict';
  33. const PREFIX = 'layer-';
  34. // Theme & Styles
  35. const css = `
  36. .${PREFIX}overlay {
  37. position: fixed;
  38. top: 0;
  39. left: 0;
  40. width: 100%;
  41. height: 100%;
  42. background: rgba(0, 0, 0, 0.4);
  43. display: flex;
  44. justify-content: center;
  45. align-items: center;
  46. z-index: 1000;
  47. opacity: 0;
  48. transition: opacity 0.3s;
  49. }
  50. .${PREFIX}overlay.show {
  51. opacity: 1;
  52. }
  53. .${PREFIX}popup {
  54. background: #fff;
  55. border-radius: 8px;
  56. box-shadow: 0 4px 12px rgba(0,0,0,0.15);
  57. width: 32em;
  58. max-width: 90%;
  59. padding: 1.5em;
  60. display: flex;
  61. flex-direction: column;
  62. align-items: center;
  63. transform: scale(0.9);
  64. transition: transform 0.3s;
  65. font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  66. }
  67. .${PREFIX}overlay.show .${PREFIX}popup {
  68. transform: scale(1);
  69. }
  70. .${PREFIX}title {
  71. font-size: 1.8em;
  72. font-weight: 600;
  73. color: #333;
  74. margin: 0 0 0.5em;
  75. text-align: center;
  76. }
  77. .${PREFIX}content {
  78. font-size: 1.125em;
  79. color: #545454;
  80. margin-bottom: 1.5em;
  81. text-align: center;
  82. line-height: 1.5;
  83. }
  84. .${PREFIX}actions {
  85. display: flex;
  86. justify-content: center;
  87. gap: 1em;
  88. width: 100%;
  89. }
  90. .${PREFIX}button {
  91. border: none;
  92. border-radius: 4px;
  93. padding: 0.6em 1.2em;
  94. font-size: 1em;
  95. cursor: pointer;
  96. transition: background-color 0.2s, box-shadow 0.2s;
  97. color: #fff;
  98. }
  99. .${PREFIX}confirm {
  100. background-color: #3085d6;
  101. }
  102. .${PREFIX}confirm:hover {
  103. background-color: #2b77c0;
  104. }
  105. .${PREFIX}cancel {
  106. background-color: #aaa;
  107. }
  108. .${PREFIX}cancel:hover {
  109. background-color: #999;
  110. }
  111. .${PREFIX}icon {
  112. width: 5em;
  113. height: 5em;
  114. border: 0.25em solid transparent;
  115. border-radius: 50%;
  116. margin: 1.5em auto 1.2em;
  117. box-sizing: content-box;
  118. position: relative;
  119. }
  120. /* Success Icon */
  121. .${PREFIX}icon.success {
  122. border-color: #a5dc86;
  123. color: #a5dc86;
  124. }
  125. .${PREFIX}icon.success::before, .${PREFIX}icon.success::after {
  126. content: '';
  127. position: absolute;
  128. background: #fff;
  129. border-radius: 50%;
  130. }
  131. .${PREFIX}success-line-tip {
  132. width: 1.5em;
  133. height: 0.3em;
  134. background-color: #a5dc86;
  135. display: block;
  136. position: absolute;
  137. top: 2.85em;
  138. left: 0.85em;
  139. transform: rotate(45deg);
  140. border-radius: 0.15em;
  141. }
  142. .${PREFIX}success-line-long {
  143. width: 2.9em;
  144. height: 0.3em;
  145. background-color: #a5dc86;
  146. display: block;
  147. position: absolute;
  148. top: 2.4em;
  149. right: 0.5em;
  150. transform: rotate(-45deg);
  151. border-radius: 0.15em;
  152. }
  153. /* Error Icon */
  154. .${PREFIX}icon.error {
  155. border-color: #f27474;
  156. color: #f27474;
  157. }
  158. .${PREFIX}error-x-mark {
  159. position: relative;
  160. display: block;
  161. }
  162. .${PREFIX}error-line {
  163. position: absolute;
  164. height: 0.3em;
  165. width: 3em;
  166. background-color: #f27474;
  167. display: block;
  168. top: 2.3em;
  169. left: 1em;
  170. border-radius: 0.15em;
  171. }
  172. .${PREFIX}error-line.left {
  173. transform: rotate(45deg);
  174. }
  175. .${PREFIX}error-line.right {
  176. transform: rotate(-45deg);
  177. }
  178. /* Warning Icon */
  179. .${PREFIX}icon.warning {
  180. border-color: #facea8;
  181. color: #f8bb86;
  182. }
  183. .${PREFIX}warning-body {
  184. position: absolute;
  185. width: 0.3em;
  186. height: 2.9em;
  187. background-color: #f8bb86;
  188. left: 50%;
  189. top: 0.6em;
  190. margin-left: -0.15em;
  191. border-radius: 0.15em;
  192. }
  193. .${PREFIX}warning-dot {
  194. position: absolute;
  195. width: 0.3em;
  196. height: 0.3em;
  197. background-color: #f8bb86;
  198. left: 50%;
  199. bottom: 0.6em;
  200. margin-left: -0.15em;
  201. border-radius: 50%;
  202. }
  203. `;
  204. // Inject Styles
  205. const injectStyles = () => {
  206. if (document.getElementById(`${PREFIX}styles`)) return;
  207. const styleSheet = document.createElement('style');
  208. styleSheet.id = `${PREFIX}styles`;
  209. styleSheet.textContent = css;
  210. document.head.appendChild(styleSheet);
  211. };
  212. class Layer {
  213. constructor() {
  214. injectStyles();
  215. this.params = {};
  216. this.dom = {};
  217. this.promise = null;
  218. this.resolve = null;
  219. this.reject = null;
  220. }
  221. // Constructor helper when called as function: const popup = Layer({...})
  222. static get isProxy() { return true; }
  223. // Static entry point
  224. static fire(options) {
  225. const instance = new Layer();
  226. return instance._fire(options);
  227. }
  228. // Chainable entry point (builder-style)
  229. // Example:
  230. // Layer.$({ title: 'Hi' }).fire().then(...)
  231. // Layer.$().config({ title: 'Hi' }).fire()
  232. static $(options) {
  233. const instance = new Layer();
  234. if (options !== undefined) instance.config(options);
  235. return instance;
  236. }
  237. // Chainable config helper (does not render until `.fire()` is called)
  238. config(options = {}) {
  239. // Support the same shorthand as Layer.fire(title, text, icon)
  240. if (typeof options === 'string') {
  241. options = { title: options };
  242. if (arguments[1]) options.text = arguments[1];
  243. if (arguments[2]) options.icon = arguments[2];
  244. }
  245. this.params = { ...(this.params || {}), ...options };
  246. return this;
  247. }
  248. // Instance entry point (chainable)
  249. fire(options) {
  250. const merged = (options === undefined) ? (this.params || {}) : options;
  251. return this._fire(merged);
  252. }
  253. _fire(options = {}) {
  254. if (typeof options === 'string') {
  255. options = { title: options };
  256. if (arguments[1]) options.text = arguments[1];
  257. if (arguments[2]) options.icon = arguments[2];
  258. }
  259. this.params = {
  260. title: '',
  261. text: '',
  262. icon: null,
  263. confirmButtonText: 'OK',
  264. cancelButtonText: 'Cancel',
  265. showCancelButton: false,
  266. confirmButtonColor: '#3085d6',
  267. cancelButtonColor: '#aaa',
  268. closeOnClickOutside: true,
  269. ...options
  270. };
  271. this.promise = new Promise((resolve, reject) => {
  272. this.resolve = resolve;
  273. this.reject = reject;
  274. });
  275. this._render();
  276. return this.promise;
  277. }
  278. _render() {
  279. // Remove existing if any
  280. const existing = document.querySelector(`.${PREFIX}overlay`);
  281. if (existing) existing.remove();
  282. // Create Overlay
  283. this.dom.overlay = document.createElement('div');
  284. this.dom.overlay.className = `${PREFIX}overlay`;
  285. // Create Popup
  286. this.dom.popup = document.createElement('div');
  287. this.dom.popup.className = `${PREFIX}popup`;
  288. this.dom.overlay.appendChild(this.dom.popup);
  289. // Icon
  290. if (this.params.icon) {
  291. this.dom.icon = this._createIcon(this.params.icon);
  292. this.dom.popup.appendChild(this.dom.icon);
  293. }
  294. // Title
  295. if (this.params.title) {
  296. this.dom.title = document.createElement('h2');
  297. this.dom.title.className = `${PREFIX}title`;
  298. this.dom.title.textContent = this.params.title;
  299. this.dom.popup.appendChild(this.dom.title);
  300. }
  301. // Content (Text / HTML / Element)
  302. if (this.params.text || this.params.html || this.params.content) {
  303. this.dom.content = document.createElement('div');
  304. this.dom.content.className = `${PREFIX}content`;
  305. if (this.params.content) {
  306. // DOM Element or Selector
  307. let el = this.params.content;
  308. if (typeof el === 'string') el = document.querySelector(el);
  309. if (el instanceof Element) this.dom.content.appendChild(el);
  310. } else if (this.params.html) {
  311. this.dom.content.innerHTML = this.params.html;
  312. } else {
  313. this.dom.content.textContent = this.params.text;
  314. }
  315. this.dom.popup.appendChild(this.dom.content);
  316. }
  317. // Actions
  318. this.dom.actions = document.createElement('div');
  319. this.dom.actions.className = `${PREFIX}actions`;
  320. // Cancel Button
  321. if (this.params.showCancelButton) {
  322. this.dom.cancelBtn = document.createElement('button');
  323. this.dom.cancelBtn.className = `${PREFIX}button ${PREFIX}cancel`;
  324. this.dom.cancelBtn.textContent = this.params.cancelButtonText;
  325. this.dom.cancelBtn.style.backgroundColor = this.params.cancelButtonColor;
  326. this.dom.cancelBtn.onclick = () => this._close(false);
  327. this.dom.actions.appendChild(this.dom.cancelBtn);
  328. }
  329. // Confirm Button
  330. this.dom.confirmBtn = document.createElement('button');
  331. this.dom.confirmBtn.className = `${PREFIX}button ${PREFIX}confirm`;
  332. this.dom.confirmBtn.textContent = this.params.confirmButtonText;
  333. this.dom.confirmBtn.style.backgroundColor = this.params.confirmButtonColor;
  334. this.dom.confirmBtn.onclick = () => this._close(true);
  335. this.dom.actions.appendChild(this.dom.confirmBtn);
  336. this.dom.popup.appendChild(this.dom.actions);
  337. // Event Listeners
  338. if (this.params.closeOnClickOutside) {
  339. this.dom.overlay.addEventListener('click', (e) => {
  340. if (e.target === this.dom.overlay) {
  341. this._close(null); // Dismiss
  342. }
  343. });
  344. }
  345. document.body.appendChild(this.dom.overlay);
  346. // Animation
  347. requestAnimationFrame(() => {
  348. this.dom.overlay.classList.add('show');
  349. });
  350. }
  351. _createIcon(type) {
  352. const icon = document.createElement('div');
  353. icon.className = `${PREFIX}icon ${type}`;
  354. if (type === 'success') {
  355. const tip = document.createElement('div');
  356. tip.className = `${PREFIX}success-line-tip`;
  357. const long = document.createElement('div');
  358. long.className = `${PREFIX}success-line-long`;
  359. icon.appendChild(tip);
  360. icon.appendChild(long);
  361. } else if (type === 'error') {
  362. const xMark = document.createElement('span');
  363. xMark.className = `${PREFIX}error-x-mark`;
  364. const left = document.createElement('span');
  365. left.className = `${PREFIX}error-line left`;
  366. const right = document.createElement('span');
  367. right.className = `${PREFIX}error-line right`;
  368. xMark.appendChild(left);
  369. xMark.appendChild(right);
  370. icon.appendChild(xMark);
  371. } else if (type === 'warning') {
  372. const body = document.createElement('div');
  373. body.className = `${PREFIX}warning-body`;
  374. const dot = document.createElement('div');
  375. dot.className = `${PREFIX}warning-dot`;
  376. icon.appendChild(body);
  377. icon.appendChild(dot);
  378. }
  379. return icon;
  380. }
  381. _close(isConfirmed) {
  382. this.dom.overlay.classList.remove('show');
  383. setTimeout(() => {
  384. if (this.dom.overlay && this.dom.overlay.parentNode) {
  385. this.dom.overlay.parentNode.removeChild(this.dom.overlay);
  386. }
  387. }, 300);
  388. if (isConfirmed === true) {
  389. this.resolve({ isConfirmed: true, isDenied: false, isDismissed: false });
  390. } else if (isConfirmed === false) {
  391. this.resolve({ isConfirmed: false, isDenied: false, isDismissed: true, dismiss: 'cancel' });
  392. } else {
  393. this.resolve({ isConfirmed: false, isDenied: false, isDismissed: true, dismiss: 'backdrop' });
  394. }
  395. }
  396. }
  397. return Layer;
  398. })));
  399. /* --- animal.js --- */
  400. (function (global, factory) {
  401. typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
  402. typeof define === 'function' && define.amd ? define(factory) :
  403. (global.animal = factory());
  404. }(__GLOBAL__, (function () {
  405. 'use strict';
  406. // --- Utils ---
  407. const isArr = (a) => Array.isArray(a);
  408. const isStr = (s) => typeof s === 'string';
  409. const isFunc = (f) => typeof f === 'function';
  410. const isNil = (v) => v === undefined || v === null;
  411. const isSVG = (el) => (typeof SVGElement !== 'undefined' && el instanceof SVGElement) || ((typeof Element !== 'undefined') && (el instanceof Element) && el.namespaceURI === 'http://www.w3.org/2000/svg');
  412. const isEl = (v) => (typeof Element !== 'undefined') && (v instanceof Element);
  413. const clamp01 = (n) => Math.max(0, Math.min(1, n));
  414. const toKebab = (s) => s.replace(/[A-Z]/g, (m) => '-' + m.toLowerCase());
  415. const toArray = (targets) => {
  416. if (isArr(targets)) return targets;
  417. if (isStr(targets)) {
  418. if (typeof document === 'undefined') return [];
  419. return Array.from(document.querySelectorAll(targets));
  420. }
  421. if ((typeof NodeList !== 'undefined') && (targets instanceof NodeList)) return Array.from(targets);
  422. if ((typeof Element !== 'undefined') && (targets instanceof Element)) return [targets];
  423. if ((typeof Window !== 'undefined') && (targets instanceof Window)) return [targets];
  424. // Plain objects (for anime.js-style object tweening)
  425. if (!isNil(targets) && (typeof targets === 'object' || isFunc(targets))) return [targets];
  426. return [];
  427. };
  428. // Selection helper (chainable, still Array-compatible)
  429. const _layerHandlerByEl = (typeof WeakMap !== 'undefined') ? new WeakMap() : null;
  430. class Selection extends Array {
  431. animate(params) { return animate(this, params); }
  432. draw(params = {}) { return svgDraw(this, params); }
  433. inViewAnimate(params, options = {}) { return inViewAnimate(this, params, options); }
  434. // Layer.js plugin-style helper:
  435. // const $ = animal.$;
  436. // $('.btn').layer({ title: 'Hi' });
  437. // Clicking the element will open Layer.
  438. layer(options) {
  439. const LayerCtor =
  440. (typeof window !== 'undefined' && window.Layer) ? window.Layer :
  441. (typeof globalThis !== 'undefined' && globalThis.Layer) ? globalThis.Layer :
  442. (typeof Layer !== 'undefined' ? Layer : null);
  443. if (!LayerCtor) return this;
  444. this.forEach((el) => {
  445. if (!isEl(el)) return;
  446. // De-dupe / replace previous binding
  447. if (_layerHandlerByEl) {
  448. const prev = _layerHandlerByEl.get(el);
  449. if (prev) el.removeEventListener('click', prev);
  450. }
  451. const handler = () => {
  452. let opts = options;
  453. if (isFunc(options)) {
  454. opts = options(el);
  455. } else if (isNil(options)) {
  456. opts = null;
  457. }
  458. // If no explicit options, allow data-* configuration
  459. if (!opts || (typeof opts === 'object' && Object.keys(opts).length === 0)) {
  460. const d = el.dataset || {};
  461. opts = {
  462. title: d.layerTitle || el.getAttribute('data-layer-title') || (el.textContent || '').trim(),
  463. text: d.layerText || el.getAttribute('data-layer-text') || '',
  464. icon: d.layerIcon || el.getAttribute('data-layer-icon') || null,
  465. showCancelButton: (d.layerCancel === 'true') || (el.getAttribute('data-layer-cancel') === 'true')
  466. };
  467. }
  468. // Prefer builder API if present, fallback to static fire.
  469. if (LayerCtor.$ && isFunc(LayerCtor.$)) return LayerCtor.$(opts).fire();
  470. if (LayerCtor.fire && isFunc(LayerCtor.fire)) return LayerCtor.fire(opts);
  471. };
  472. if (_layerHandlerByEl) _layerHandlerByEl.set(el, handler);
  473. el.addEventListener('click', handler);
  474. });
  475. return this;
  476. }
  477. // Remove click bindings added by `.layer()`
  478. unlayer() {
  479. this.forEach((el) => {
  480. if (!isEl(el)) return;
  481. if (!_layerHandlerByEl) return;
  482. const prev = _layerHandlerByEl.get(el);
  483. if (prev) el.removeEventListener('click', prev);
  484. _layerHandlerByEl.delete(el);
  485. });
  486. return this;
  487. }
  488. }
  489. const $ = (targets) => Selection.from(toArray(targets));
  490. const UNITLESS_KEYS = ['opacity', 'scale', 'scaleX', 'scaleY', 'scaleZ', 'zIndex', 'fontWeight', 'strokeDashoffset', 'strokeDasharray', 'strokeWidth'];
  491. const getUnit = (val, prop) => {
  492. if (UNITLESS_KEYS.includes(prop)) return '';
  493. 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);
  494. return split ? split[1] : undefined;
  495. };
  496. const isCssVar = (k) => isStr(k) && k.startsWith('--');
  497. // Keep this intentionally broad so "unknown" config keys don't accidentally become animated props.
  498. // Prefer putting non-anim-prop config under `params.options`.
  499. const isAnimOptionKey = (k) => [
  500. 'options',
  501. 'duration', 'delay', 'easing', 'direction', 'fill', 'loop', 'endDelay', 'autoplay',
  502. 'update', 'begin', 'complete',
  503. // WAAPI-ish common keys (ignored unless we explicitly support them)
  504. 'iterations', 'iterationStart', 'iterationComposite', 'composite', 'playbackRate',
  505. // Spring helpers
  506. 'springFrames'
  507. ].includes(k);
  508. const isEasingFn = (e) => typeof e === 'function';
  509. // Minimal cubic-bezier implementation (for JS engine easing)
  510. function cubicBezier(x1, y1, x2, y2) {
  511. // Inspired by https://github.com/gre/bezier-easing (simplified)
  512. const NEWTON_ITERATIONS = 4;
  513. const NEWTON_MIN_SLOPE = 0.001;
  514. const SUBDIVISION_PRECISION = 0.0000001;
  515. const SUBDIVISION_MAX_ITERATIONS = 10;
  516. const kSplineTableSize = 11;
  517. const kSampleStepSize = 1.0 / (kSplineTableSize - 1.0);
  518. const float32ArraySupported = typeof Float32Array === 'function';
  519. function A(aA1, aA2) { return 1.0 - 3.0 * aA2 + 3.0 * aA1; }
  520. function B(aA1, aA2) { return 3.0 * aA2 - 6.0 * aA1; }
  521. function C(aA1) { return 3.0 * aA1; }
  522. function calcBezier(aT, aA1, aA2) {
  523. return ((A(aA1, aA2) * aT + B(aA1, aA2)) * aT + C(aA1)) * aT;
  524. }
  525. function getSlope(aT, aA1, aA2) {
  526. return 3.0 * A(aA1, aA2) * aT * aT + 2.0 * B(aA1, aA2) * aT + C(aA1);
  527. }
  528. function binarySubdivide(aX, aA, aB) {
  529. let currentX, currentT, i = 0;
  530. do {
  531. currentT = aA + (aB - aA) / 2.0;
  532. currentX = calcBezier(currentT, x1, x2) - aX;
  533. if (currentX > 0.0) aB = currentT;
  534. else aA = currentT;
  535. } while (Math.abs(currentX) > SUBDIVISION_PRECISION && ++i < SUBDIVISION_MAX_ITERATIONS);
  536. return currentT;
  537. }
  538. function newtonRaphsonIterate(aX, aGuessT) {
  539. for (let i = 0; i < NEWTON_ITERATIONS; ++i) {
  540. const currentSlope = getSlope(aGuessT, x1, x2);
  541. if (currentSlope === 0.0) return aGuessT;
  542. const currentX = calcBezier(aGuessT, x1, x2) - aX;
  543. aGuessT -= currentX / currentSlope;
  544. }
  545. return aGuessT;
  546. }
  547. const sampleValues = float32ArraySupported ? new Float32Array(kSplineTableSize) : new Array(kSplineTableSize);
  548. for (let i = 0; i < kSplineTableSize; ++i) {
  549. sampleValues[i] = calcBezier(i * kSampleStepSize, x1, x2);
  550. }
  551. function getTForX(aX) {
  552. let intervalStart = 0.0;
  553. let currentSample = 1;
  554. const lastSample = kSplineTableSize - 1;
  555. for (; currentSample !== lastSample && sampleValues[currentSample] <= aX; ++currentSample) {
  556. intervalStart += kSampleStepSize;
  557. }
  558. --currentSample;
  559. const dist = (aX - sampleValues[currentSample]) / (sampleValues[currentSample + 1] - sampleValues[currentSample]);
  560. const guessForT = intervalStart + dist * kSampleStepSize;
  561. const initialSlope = getSlope(guessForT, x1, x2);
  562. if (initialSlope >= NEWTON_MIN_SLOPE) return newtonRaphsonIterate(aX, guessForT);
  563. if (initialSlope === 0.0) return guessForT;
  564. return binarySubdivide(aX, intervalStart, intervalStart + kSampleStepSize);
  565. }
  566. return (x) => {
  567. if (x === 0 || x === 1) return x;
  568. return calcBezier(getTForX(x), y1, y2);
  569. };
  570. }
  571. function resolveJsEasing(easing, springValues) {
  572. if (isEasingFn(easing)) return easing;
  573. if (typeof easing === 'object' && easing && !Array.isArray(easing)) {
  574. // Spring: map linear progress -> simulated spring curve (0..1)
  575. const values = springValues || getSpringValues(easing);
  576. const last = Math.max(0, values.length - 1);
  577. return (t) => {
  578. const p = clamp01(t);
  579. const idx = p * last;
  580. const i0 = Math.floor(idx);
  581. const i1 = Math.min(last, i0 + 1);
  582. const frac = idx - i0;
  583. const v0 = values[i0] ?? p;
  584. const v1 = values[i1] ?? p;
  585. return v0 + (v1 - v0) * frac;
  586. };
  587. }
  588. if (Array.isArray(easing) && easing.length === 4) {
  589. return cubicBezier(easing[0], easing[1], easing[2], easing[3]);
  590. }
  591. // Common named easings
  592. switch (easing) {
  593. case 'linear': return (t) => t;
  594. case 'ease': return cubicBezier(0.25, 0.1, 0.25, 1);
  595. case 'ease-in': return cubicBezier(0.42, 0, 1, 1);
  596. case 'ease-out': return cubicBezier(0, 0, 0.58, 1);
  597. case 'ease-in-out': return cubicBezier(0.42, 0, 0.58, 1);
  598. default: return (t) => t;
  599. }
  600. }
  601. // --- Spring Physics (Simplified) ---
  602. // Returns an array of [time, value] or just value for WAAPI linear easing
  603. function spring({ stiffness = 100, damping = 10, mass = 1, velocity = 0, precision = 0.01 } = {}) {
  604. const values = [];
  605. let t = 0;
  606. const timeStep = 1 / 60; // 60fps simulation
  607. let current = 0;
  608. let v = velocity;
  609. const target = 1;
  610. let running = true;
  611. while (running && t < 10) { // Safety break at 10s
  612. const fSpring = -stiffness * (current - target);
  613. const fDamper = -damping * v;
  614. const a = (fSpring + fDamper) / mass;
  615. v += a * timeStep;
  616. current += v * timeStep;
  617. values.push(current);
  618. t += timeStep;
  619. if (Math.abs(current - target) < precision && Math.abs(v) < precision) {
  620. running = false;
  621. }
  622. }
  623. if (values[values.length - 1] !== 1) values.push(1);
  624. return values;
  625. }
  626. // Cache spring curves by config (perf: avoid recomputing for many targets)
  627. // LRU-ish capped cache to avoid unbounded growth in long-lived apps.
  628. const SPRING_CACHE_MAX = 50;
  629. const springCache = new Map();
  630. function springCacheGet(key) {
  631. const v = springCache.get(key);
  632. if (!v) return v;
  633. // refresh LRU
  634. springCache.delete(key);
  635. springCache.set(key, v);
  636. return v;
  637. }
  638. function springCacheSet(key, values) {
  639. if (springCache.has(key)) springCache.delete(key);
  640. springCache.set(key, values);
  641. if (springCache.size > SPRING_CACHE_MAX) {
  642. const firstKey = springCache.keys().next().value;
  643. if (firstKey !== undefined) springCache.delete(firstKey);
  644. }
  645. }
  646. function getSpringValues(config = {}) {
  647. const {
  648. stiffness = 100,
  649. damping = 10,
  650. mass = 1,
  651. velocity = 0,
  652. precision = 0.01
  653. } = config || {};
  654. const key = `${stiffness}|${damping}|${mass}|${velocity}|${precision}`;
  655. const cached = springCacheGet(key);
  656. if (cached) return cached;
  657. const values = spring({ stiffness, damping, mass, velocity, precision });
  658. springCacheSet(key, values);
  659. return values;
  660. }
  661. function downsample(values, maxFrames = 120) {
  662. if (!values || values.length <= maxFrames) return values || [];
  663. const out = [];
  664. const lastIndex = values.length - 1;
  665. const step = lastIndex / (maxFrames - 1);
  666. for (let i = 0; i < maxFrames; i++) {
  667. const idx = i * step;
  668. const i0 = Math.floor(idx);
  669. const i1 = Math.min(lastIndex, i0 + 1);
  670. const frac = idx - i0;
  671. const v0 = values[i0];
  672. const v1 = values[i1];
  673. out.push(v0 + (v1 - v0) * frac);
  674. }
  675. if (out[out.length - 1] !== 1) out[out.length - 1] = 1;
  676. return out;
  677. }
  678. // --- WAAPI Core ---
  679. const TRANSFORMS = ['translateX', 'translateY', 'translateZ', 'rotate', 'rotateX', 'rotateY', 'rotateZ', 'scale', 'scaleX', 'scaleY', 'scaleZ', 'skew', 'skewX', 'skewY', 'perspective', 'x', 'y'];
  680. const ALIASES = {
  681. x: 'translateX',
  682. y: 'translateY',
  683. z: 'translateZ'
  684. };
  685. // Basic rAF loop for non-WAAPI props or fallback
  686. class RafEngine {
  687. constructor() {
  688. this.animations = [];
  689. this.tick = this.tick.bind(this);
  690. this.running = false;
  691. }
  692. add(anim) {
  693. this.animations.push(anim);
  694. if (!this.running) {
  695. this.running = true;
  696. requestAnimationFrame(this.tick);
  697. }
  698. }
  699. remove(anim) {
  700. this.animations = this.animations.filter(a => a !== anim);
  701. }
  702. tick(t) {
  703. const now = t;
  704. this.animations = this.animations.filter(anim => {
  705. return anim.tick(now); // return true to keep
  706. });
  707. if (this.animations.length) {
  708. requestAnimationFrame(this.tick);
  709. } else {
  710. this.running = false;
  711. }
  712. }
  713. }
  714. const rafEngine = new RafEngine();
  715. // --- WAAPI update sampler (only used when update callback is provided) ---
  716. class WaapiUpdateSampler {
  717. constructor() {
  718. this.items = new Set();
  719. this._running = false;
  720. this._tick = this._tick.bind(this);
  721. }
  722. add(item) {
  723. this.items.add(item);
  724. if (!this._running) {
  725. this._running = true;
  726. requestAnimationFrame(this._tick);
  727. }
  728. }
  729. remove(item) {
  730. this.items.delete(item);
  731. }
  732. _tick(t) {
  733. if (!this.items.size) {
  734. this._running = false;
  735. return;
  736. }
  737. let anyActive = false;
  738. let anyChanged = false;
  739. this.items.forEach((item) => {
  740. const { anim, target, update } = item;
  741. if (!anim || !anim.effect) return;
  742. let dur = item._dur || 0;
  743. if (!dur) {
  744. const timing = readWaapiEffectTiming(anim.effect);
  745. dur = timing.duration || 0;
  746. item._dur = dur;
  747. }
  748. if (!dur) return;
  749. const p = clamp01((anim.currentTime || 0) / dur);
  750. if (anim.playState === 'running') anyActive = true;
  751. if (item._lastP !== p) {
  752. anyChanged = true;
  753. item._lastP = p;
  754. update({ target, progress: p, time: t });
  755. }
  756. if (anim.playState === 'finished' || anim.playState === 'idle') {
  757. this.items.delete(item);
  758. }
  759. });
  760. if (this.items.size && (anyActive || anyChanged)) {
  761. requestAnimationFrame(this._tick);
  762. } else {
  763. this._running = false;
  764. }
  765. }
  766. }
  767. const waapiUpdateSampler = new WaapiUpdateSampler();
  768. function readWaapiEffectTiming(effect) {
  769. // We compute this once per animation and cache it to avoid per-frame getComputedTiming().
  770. // We primarily cache `duration` to preserve existing behavior (progress maps to one iteration).
  771. let duration = 0;
  772. let endTime = 0;
  773. if (!effect) return { duration, endTime };
  774. try {
  775. const ct = effect.getComputedTiming ? effect.getComputedTiming() : null;
  776. if (ct) {
  777. if (typeof ct.duration === 'number' && Number.isFinite(ct.duration)) duration = ct.duration;
  778. if (typeof ct.endTime === 'number' && Number.isFinite(ct.endTime)) endTime = ct.endTime;
  779. }
  780. } catch (e) {
  781. // ignore
  782. }
  783. if (!endTime) endTime = duration || 0;
  784. return { duration, endTime };
  785. }
  786. function getDefaultUnit(prop) {
  787. if (!prop) return '';
  788. if (UNITLESS_KEYS.includes(prop)) return '';
  789. if (prop.startsWith('scale')) return '';
  790. if (prop === 'opacity') return '';
  791. if (prop.startsWith('rotate') || prop.startsWith('skew')) return 'deg';
  792. return 'px';
  793. }
  794. function normalizeTransformPartValue(prop, v) {
  795. if (typeof v !== 'number') return v;
  796. if (prop && prop.startsWith('scale')) return '' + v;
  797. if (prop && (prop.startsWith('rotate') || prop.startsWith('skew'))) return v + 'deg';
  798. return v + 'px';
  799. }
  800. // String number template: interpolate numeric tokens inside a string.
  801. // Useful for SVG path `d`, `points`, and other attributes that contain many numbers.
  802. const _PURE_NUMBER_RE = /^[+-]?(?:\d*\.)?\d+(?:[eE][+-]?\d+)?(?:%|px|pt|em|rem|in|cm|mm|ex|ch|pc|vw|vh|vmin|vmax|deg|rad|turn)?$/;
  803. function isPureNumberLike(str) {
  804. return _PURE_NUMBER_RE.test(String(str || '').trim());
  805. }
  806. function countDecimals(numStr) {
  807. const s = String(numStr || '');
  808. const dot = s.indexOf('.');
  809. if (dot < 0) return 0;
  810. const e = s.search(/[eE]/);
  811. const end = e >= 0 ? e : s.length;
  812. const frac = s.slice(dot + 1, end);
  813. const trimmed = frac.replace(/0+$/, '');
  814. return Math.min(6, trimmed.length);
  815. }
  816. function parseNumberTemplate(str) {
  817. const s = String(str ?? '');
  818. const nums = [];
  819. const parts = [];
  820. let last = 0;
  821. const re = /[+-]?(?:\d*\.)?\d+(?:[eE][+-]?\d+)?/g;
  822. let m;
  823. while ((m = re.exec(s))) {
  824. const start = m.index;
  825. const end = start + m[0].length;
  826. parts.push(s.slice(last, start));
  827. nums.push(parseFloat(m[0]));
  828. last = end;
  829. }
  830. parts.push(s.slice(last));
  831. return { nums, parts };
  832. }
  833. function extractNumberStrings(str) {
  834. return String(str ?? '').match(/[+-]?(?:\d*\.)?\d+(?:[eE][+-]?\d+)?/g) || [];
  835. }
  836. function formatInterpolatedNumber(n, decimals) {
  837. let v = n;
  838. if (!Number.isFinite(v)) v = 0;
  839. if (Math.abs(v) < 1e-12) v = 0;
  840. if (!decimals) return String(Math.round(v));
  841. return v.toFixed(decimals).replace(/\.?0+$/, '');
  842. }
  843. // --- JS Interpolator (anime.js-ish, simplified) ---
  844. class JsAnimation {
  845. constructor(target, propValues, opts = {}, callbacks = {}) {
  846. this.target = target;
  847. this.propValues = propValues;
  848. this.duration = opts.duration ?? 1000;
  849. this.delay = opts.delay ?? 0;
  850. this.direction = opts.direction ?? 'normal';
  851. this.loop = opts.loop ?? 1;
  852. this.endDelay = opts.endDelay ?? 0;
  853. this.easing = opts.easing ?? 'linear';
  854. this.autoplay = opts.autoplay !== false;
  855. this.update = callbacks.update;
  856. this.begin = callbacks.begin;
  857. this.complete = callbacks.complete;
  858. this._resolve = null;
  859. this.finished = new Promise((res) => (this._resolve = res));
  860. this._started = false;
  861. this._running = false;
  862. this._paused = false;
  863. this._cancelled = false;
  864. this._startTime = 0;
  865. this._progress = 0;
  866. this._didBegin = false;
  867. this._ease = resolveJsEasing(this.easing, opts.springValues);
  868. this._tween = this._buildTween();
  869. this.tick = this.tick.bind(this);
  870. if (this.autoplay) this.play();
  871. }
  872. _readCurrentValue(k) {
  873. const t = this.target;
  874. // JS object
  875. if (!isEl(t) && !isSVG(t)) return t[k];
  876. // scroll
  877. if (k === 'scrollTop' || k === 'scrollLeft') return t[k];
  878. // CSS var
  879. if (isEl(t) && isCssVar(k)) {
  880. const cs = getComputedStyle(t);
  881. return (cs.getPropertyValue(k) || t.style.getPropertyValue(k) || '').trim();
  882. }
  883. // style
  884. if (isEl(t)) {
  885. const cs = getComputedStyle(t);
  886. // Prefer computed style (kebab) because many props aren't direct keys on cs
  887. const v = cs.getPropertyValue(toKebab(k));
  888. if (v && v.trim()) return v.trim();
  889. // Fallback to inline style access
  890. if (k in t.style) return t.style[k];
  891. }
  892. // SVG attribute
  893. if (isSVG(t)) {
  894. const attr = toKebab(k);
  895. if (t.hasAttribute(attr)) return t.getAttribute(attr);
  896. if (t.hasAttribute(k)) return t.getAttribute(k);
  897. }
  898. // Generic property
  899. return t[k];
  900. }
  901. _writeValue(k, v) {
  902. const t = this.target;
  903. // JS object
  904. if (!isEl(t) && !isSVG(t)) {
  905. t[k] = v;
  906. return;
  907. }
  908. // SVG attribute
  909. if (isSVG(t)) {
  910. const attr = toKebab(k);
  911. t.setAttribute(attr, v);
  912. // Verify write
  913. // if (k === 'd' && Math.random() < 0.01) console.log('Readback:', t.getAttribute('d')?.slice(0, 10));
  914. return;
  915. }
  916. // Fallback
  917. if ((k === 'd' || k === 'points') && isEl(t)) {
  918. t.setAttribute(k, v);
  919. return;
  920. }
  921. // scroll
  922. if (k === 'scrollTop' || k === 'scrollLeft') {
  923. t[k] = v;
  924. return;
  925. }
  926. // CSS var
  927. if (isEl(t) && isCssVar(k)) {
  928. t.style.setProperty(k, v);
  929. return;
  930. }
  931. // style
  932. if (isEl(t) && (k in t.style)) {
  933. t.style[k] = v;
  934. return;
  935. }
  936. // SVG attribute
  937. if (isSVG(t)) {
  938. const attr = toKebab(k);
  939. t.setAttribute(attr, v);
  940. return;
  941. }
  942. // Fallback for path/d/points even if isSVG check somehow failed
  943. if ((k === 'd' || k === 'points') && isEl(t)) {
  944. t.setAttribute(k, v);
  945. return;
  946. }
  947. // Generic property
  948. t[k] = v;
  949. }
  950. _buildTween() {
  951. const tween = {};
  952. Object.keys(this.propValues).forEach((k) => {
  953. const raw = this.propValues[k];
  954. const fromRaw = isArr(raw) ? raw[0] : this._readCurrentValue(k);
  955. const toRaw = isArr(raw) ? raw[1] : raw;
  956. const fromStr = isNil(fromRaw) ? '0' : ('' + fromRaw).trim();
  957. const toStr = isNil(toRaw) ? '0' : ('' + toRaw).trim();
  958. const fromScalar = isPureNumberLike(fromStr);
  959. const toScalar = isPureNumberLike(toStr);
  960. // numeric tween (with unit preservation) — only when BOTH sides are pure scalars.
  961. if (fromScalar && toScalar) {
  962. const fromNum = parseFloat(fromStr);
  963. const toNum = parseFloat(toStr);
  964. const unit = getUnit(toStr, k) ?? getUnit(fromStr, k) ?? '';
  965. tween[k] = { type: 'number', from: fromNum, to: toNum, unit };
  966. return;
  967. }
  968. // string number-template tween (SVG path `d`, `points`, etc.)
  969. const a = parseNumberTemplate(fromStr);
  970. const b = parseNumberTemplate(toStr);
  971. if (a.nums.length >= 2 && a.nums.length === b.nums.length && a.parts.length === b.parts.length) {
  972. const aStrs = extractNumberStrings(fromStr);
  973. const bStrs = extractNumberStrings(toStr);
  974. const decs = a.nums.map((_, i) => Math.max(countDecimals(aStrs[i]), countDecimals(bStrs[i])));
  975. tween[k] = { type: 'number-template', fromNums: a.nums, toNums: b.nums, parts: b.parts, decimals: decs };
  976. return;
  977. } else if ((k === 'd' || k === 'points') && (a.nums.length > 0 || b.nums.length > 0)) {
  978. console.warn(`[Animal.js] Morph mismatch for property "${k}".\nValues must have matching number count.`,
  979. { from: a.nums.length, to: b.nums.length, fromVal: fromStr, toVal: toStr }
  980. );
  981. }
  982. // Non-numeric: fall back to "switch" (still useful for seek endpoints)
  983. tween[k] = { type: 'discrete', from: fromStr, to: toStr };
  984. });
  985. return tween;
  986. }
  987. _apply(progress, time) {
  988. this._progress = clamp01(progress);
  989. Object.keys(this._tween).forEach((k) => {
  990. const t = this._tween[k];
  991. if (t.type === 'number') {
  992. const eased = this._ease ? this._ease(this._progress) : this._progress;
  993. const val = t.from + (t.to - t.from) * eased;
  994. if (!isEl(this.target) && !isSVG(this.target) && t.unit === '') {
  995. this._writeValue(k, val);
  996. } else {
  997. this._writeValue(k, (val + t.unit));
  998. }
  999. } else if (t.type === 'number-template') {
  1000. const eased = this._ease ? this._ease(this._progress) : this._progress;
  1001. const { fromNums, toNums, parts, decimals } = t;
  1002. let out = parts[0] || '';
  1003. for (let i = 0; i < fromNums.length; i++) {
  1004. const v = fromNums[i] + (toNums[i] - fromNums[i]) * eased;
  1005. out += formatInterpolatedNumber(v, decimals ? decimals[i] : 0);
  1006. out += parts[i + 1] || '';
  1007. }
  1008. this._writeValue(k, out);
  1009. } else {
  1010. const val = this._progress >= 1 ? t.to : t.from;
  1011. this._writeValue(k, val);
  1012. }
  1013. });
  1014. if (this.update) this.update({ target: this.target, progress: this._progress, time });
  1015. }
  1016. seek(progress) {
  1017. // Seek does not auto-play; it's intended for scroll-linked or manual control.
  1018. const t = (typeof performance !== 'undefined' ? performance.now() : 0);
  1019. this._apply(progress, t);
  1020. }
  1021. play() {
  1022. if (this._cancelled) return;
  1023. if (!this._started) {
  1024. this._started = true;
  1025. this._startTime = performance.now() + this.delay - (this._progress * this.duration);
  1026. // begin fired on first active tick to avoid firing during delay.
  1027. }
  1028. this._paused = false;
  1029. if (!this._running) {
  1030. this._running = true;
  1031. rafEngine.add(this);
  1032. }
  1033. }
  1034. pause() {
  1035. this._paused = true;
  1036. }
  1037. cancel() {
  1038. this._cancelled = true;
  1039. this._running = false;
  1040. rafEngine.remove(this);
  1041. // Resolve to avoid hanging awaits
  1042. if (this._resolve) this._resolve();
  1043. }
  1044. finish() {
  1045. this.seek(1);
  1046. this._running = false;
  1047. rafEngine.remove(this);
  1048. if (this.complete) this.complete(this.target);
  1049. if (this._resolve) this._resolve();
  1050. }
  1051. tick(now) {
  1052. if (this._cancelled) return false;
  1053. if (this._paused) return true;
  1054. if (!this._started) {
  1055. this._started = true;
  1056. this._startTime = now + this.delay;
  1057. }
  1058. if (now < this._startTime) return true;
  1059. if (!this._didBegin) {
  1060. this._didBegin = true;
  1061. if (this.begin) this.begin(this.target);
  1062. }
  1063. const totalDur = this.duration + (this.endDelay || 0);
  1064. const elapsed = now - this._startTime;
  1065. const iter = totalDur > 0 ? Math.floor(elapsed / totalDur) : 0;
  1066. const inIter = totalDur > 0 ? (elapsed - iter * totalDur) : elapsed;
  1067. const iterations = this.loop === true ? Infinity : this.loop;
  1068. if (iterations !== Infinity && iter >= iterations) {
  1069. this._apply(this._mapDirection(1, iterations - 1));
  1070. this._running = false;
  1071. if (this.complete) this.complete(this.target);
  1072. if (this._resolve) this._resolve();
  1073. return false;
  1074. }
  1075. // if we're in endDelay portion, hold the end state
  1076. let p = clamp01(inIter / this.duration);
  1077. if (this.duration <= 0) p = 1;
  1078. if (this.endDelay && inIter > this.duration) p = 1;
  1079. this._apply(this._mapDirection(p, iter), now);
  1080. // Keep running until loops exhausted
  1081. return true;
  1082. }
  1083. _mapDirection(p, iterIndex) {
  1084. const dir = this.direction;
  1085. const flip = (dir === 'reverse') || (dir === 'alternate-reverse');
  1086. const isAlt = (dir === 'alternate') || (dir === 'alternate-reverse');
  1087. let t = flip ? (1 - p) : p;
  1088. if (isAlt && (iterIndex % 2 === 1)) t = 1 - t;
  1089. return t;
  1090. }
  1091. }
  1092. // --- Controls (Motion One-ish, chainable / thenable) ---
  1093. class Controls {
  1094. constructor({ waapi = [], js = [], finished }) {
  1095. this.animations = waapi; // backward compat with old `.animations` usage
  1096. this.jsAnimations = js;
  1097. this.finished = finished || Promise.resolve();
  1098. }
  1099. then(onFulfilled, onRejected) { return this.finished.then(onFulfilled, onRejected); }
  1100. catch(onRejected) { return this.finished.catch(onRejected); }
  1101. finally(onFinally) { return this.finished.finally(onFinally); }
  1102. play() {
  1103. if (this._onPlay) this._onPlay.forEach((fn) => fn && fn());
  1104. if (this._ensureWaapiUpdate) this._ensureWaapiUpdate();
  1105. this.animations.forEach((a) => a && a.play && a.play());
  1106. this.jsAnimations.forEach((a) => a && a.play && a.play());
  1107. return this;
  1108. }
  1109. pause() {
  1110. this.animations.forEach((a) => a && a.pause && a.pause());
  1111. this.jsAnimations.forEach((a) => a && a.pause && a.pause());
  1112. return this;
  1113. }
  1114. cancel() {
  1115. this.animations.forEach((a) => a && a.cancel && a.cancel());
  1116. this.jsAnimations.forEach((a) => a && a.cancel && a.cancel());
  1117. return this;
  1118. }
  1119. finish() {
  1120. this.animations.forEach((a) => a && a.finish && a.finish());
  1121. this.jsAnimations.forEach((a) => a && a.finish && a.finish());
  1122. return this;
  1123. }
  1124. seek(progress) {
  1125. const p = clamp01(progress);
  1126. const t = (typeof performance !== 'undefined' ? performance.now() : 0);
  1127. this.animations.forEach((anim) => {
  1128. if (anim && anim.effect) {
  1129. let timing = this._waapiTimingByAnim && this._waapiTimingByAnim.get(anim);
  1130. if (!timing) {
  1131. timing = readWaapiEffectTiming(anim.effect);
  1132. if (this._waapiTimingByAnim) this._waapiTimingByAnim.set(anim, timing);
  1133. }
  1134. const dur = timing.duration || 0;
  1135. if (!dur) return;
  1136. anim.currentTime = dur * p;
  1137. const target = this._waapiTargetByAnim && this._waapiTargetByAnim.get(anim);
  1138. if (this._fireUpdate && target) this._fireUpdate({ target, progress: p, time: t });
  1139. }
  1140. });
  1141. this.jsAnimations.forEach((a) => a && a.seek && a.seek(p));
  1142. return this;
  1143. }
  1144. }
  1145. // --- Main Animation Logic ---
  1146. function animate(targets, params) {
  1147. const elements = toArray(targets);
  1148. const safeParams = params || {};
  1149. const optionsNamespace = (safeParams.options && typeof safeParams.options === 'object') ? safeParams.options : {};
  1150. // `params.options` provides a safe namespace for config keys without risking them being treated as animated props.
  1151. // Top-level keys still win for backward compatibility.
  1152. const merged = { ...optionsNamespace, ...safeParams };
  1153. const {
  1154. duration = 1000,
  1155. delay = 0,
  1156. easing = 'ease-out',
  1157. direction = 'normal',
  1158. fill = 'forwards',
  1159. loop = 1,
  1160. endDelay = 0,
  1161. autoplay = true,
  1162. springFrames = 120,
  1163. update, // callback
  1164. begin, // callback
  1165. complete // callback
  1166. } = merged;
  1167. let isSpring = false;
  1168. let springValuesRaw = null;
  1169. let springValuesSampled = null;
  1170. let springDurationMs = null;
  1171. if (typeof easing === 'object' && !Array.isArray(easing)) {
  1172. isSpring = true;
  1173. springValuesRaw = getSpringValues(easing);
  1174. const frames = (typeof springFrames === 'number' && springFrames > 1) ? Math.floor(springFrames) : 120;
  1175. springValuesSampled = downsample(springValuesRaw, frames);
  1176. springDurationMs = springValuesRaw.length * 1000 / 60;
  1177. }
  1178. // Callback aggregation (avoid double-calling when WAAPI+JS both run)
  1179. const cbState = typeof WeakMap !== 'undefined' ? new WeakMap() : null;
  1180. const getState = (t) => {
  1181. if (!cbState) return { begun: false, completed: false, lastUpdateBucket: -1 };
  1182. let s = cbState.get(t);
  1183. if (!s) {
  1184. s = { begun: false, completed: false, lastUpdateBucket: -1 };
  1185. cbState.set(t, s);
  1186. }
  1187. return s;
  1188. };
  1189. const fireBegin = (t) => {
  1190. if (!begin) return;
  1191. const s = getState(t);
  1192. if (s.begun) return;
  1193. s.begun = true;
  1194. begin(t);
  1195. };
  1196. const fireComplete = (t) => {
  1197. if (!complete) return;
  1198. const s = getState(t);
  1199. if (s.completed) return;
  1200. s.completed = true;
  1201. complete(t);
  1202. };
  1203. const fireUpdate = (payload) => {
  1204. if (!update) return;
  1205. const t = payload && payload.target;
  1206. const time = payload && payload.time;
  1207. if (!t) return update(payload);
  1208. const s = getState(t);
  1209. const bucket = Math.floor(((typeof time === 'number' ? time : (typeof performance !== 'undefined' ? performance.now() : 0))) / 16);
  1210. if (bucket === s.lastUpdateBucket) return;
  1211. s.lastUpdateBucket = bucket;
  1212. update(payload);
  1213. };
  1214. const propEntries = [];
  1215. Object.keys(safeParams).forEach((key) => {
  1216. if (key === 'options') return;
  1217. if (isAnimOptionKey(key)) return;
  1218. propEntries.push({ key, canonical: ALIASES[key] || key, val: safeParams[key] });
  1219. });
  1220. // Create animations but don't play if autoplay is false
  1221. const waapiAnimations = [];
  1222. const jsAnimations = [];
  1223. const engineInfo = {
  1224. isSpring,
  1225. waapiKeys: [],
  1226. jsKeys: []
  1227. };
  1228. const waapiKeySet = new Set();
  1229. const jsKeySet = new Set();
  1230. const onPlayHooks = [];
  1231. const waapiUpdateItems = [];
  1232. const waapiTargetByAnim = (typeof WeakMap !== 'undefined') ? new WeakMap() : null;
  1233. const waapiTimingByAnim = (typeof WeakMap !== 'undefined') ? new WeakMap() : null;
  1234. let waapiUpdateStarted = false;
  1235. const ensureWaapiUpdate = () => {
  1236. if (waapiUpdateStarted) return;
  1237. waapiUpdateStarted = true;
  1238. waapiUpdateItems.forEach((item) => waapiUpdateSampler.add(item));
  1239. };
  1240. const promises = elements.map((el) => {
  1241. // Route props per target (fix mixed HTML/SVG/object target arrays).
  1242. const waapiProps = {};
  1243. const jsProps = {};
  1244. propEntries.forEach(({ key, canonical, val }) => {
  1245. const isTransform = TRANSFORMS.includes(canonical) || TRANSFORMS.includes(key);
  1246. if (!el || (!isEl(el) && !isSVG(el))) {
  1247. jsProps[key] = val;
  1248. jsKeySet.add(key);
  1249. return;
  1250. }
  1251. const isSvgTarget = isSVG(el);
  1252. if (isSvgTarget) {
  1253. if (isTransform || key === 'opacity' || key === 'filter') {
  1254. waapiProps[canonical] = val;
  1255. waapiKeySet.add(canonical);
  1256. } else {
  1257. jsProps[key] = val;
  1258. jsKeySet.add(key);
  1259. }
  1260. return;
  1261. }
  1262. // HTML element
  1263. if (isCssVar(key)) {
  1264. jsProps[key] = val;
  1265. jsKeySet.add(key);
  1266. return;
  1267. }
  1268. const isCssLike = isTransform || key === 'opacity' || key === 'filter' || (isEl(el) && (key in el.style));
  1269. if (isCssLike) {
  1270. waapiProps[canonical] = val;
  1271. waapiKeySet.add(canonical);
  1272. } else {
  1273. jsProps[key] = val;
  1274. jsKeySet.add(key);
  1275. }
  1276. });
  1277. // 1. WAAPI Animation
  1278. let waapiAnim = null;
  1279. let waapiPromise = Promise.resolve();
  1280. if (Object.keys(waapiProps).length > 0) {
  1281. const buildFrames = (propValues) => {
  1282. // Spring: share sampled progress; per-target we only compute start/end once per prop.
  1283. if (isSpring && springValuesSampled && springValuesSampled.length) {
  1284. const cs = (isEl(el) && typeof getComputedStyle !== 'undefined') ? getComputedStyle(el) : null;
  1285. const metas = Object.keys(propValues).map((k) => {
  1286. const raw = propValues[k];
  1287. const rawFrom = Array.isArray(raw) ? raw[0] : undefined;
  1288. const rawTo = Array.isArray(raw) ? raw[1] : raw;
  1289. const toNum = parseFloat(('' + rawTo).trim());
  1290. const fromNumExplicit = Array.isArray(raw) ? parseFloat(('' + rawFrom).trim()) : NaN;
  1291. const isT = TRANSFORMS.includes(ALIASES[k] || k) || TRANSFORMS.includes(k);
  1292. const unit = getUnit(('' + rawTo), k) ?? getUnit(('' + rawFrom), k) ?? getDefaultUnit(k);
  1293. let fromNum = 0;
  1294. if (Number.isFinite(fromNumExplicit)) {
  1295. fromNum = fromNumExplicit;
  1296. } else if (k.startsWith('scale')) {
  1297. fromNum = 1;
  1298. } else if (k === 'opacity' && cs) {
  1299. const n = parseFloat(cs.opacity);
  1300. if (!Number.isNaN(n)) fromNum = n;
  1301. } else if (!isT && cs) {
  1302. const cssVal = cs.getPropertyValue(toKebab(k));
  1303. const n = parseFloat(cssVal);
  1304. if (!Number.isNaN(n)) fromNum = n;
  1305. }
  1306. const to = Number.isFinite(toNum) ? toNum : 0;
  1307. return { k, isT, unit: unit ?? '', from: fromNum, to };
  1308. });
  1309. const frames = new Array(springValuesSampled.length);
  1310. for (let i = 0; i < springValuesSampled.length; i++) {
  1311. const v = springValuesSampled[i];
  1312. const frame = {};
  1313. let transformStr = '';
  1314. for (let j = 0; j < metas.length; j++) {
  1315. const m = metas[j];
  1316. const current = m.from + (m.to - m.from) * v;
  1317. const outVal = (m.unit === '' ? ('' + current) : (current + m.unit));
  1318. if (m.isT) transformStr += `${m.k}(${outVal}) `;
  1319. else frame[m.k] = outVal;
  1320. }
  1321. if (transformStr) frame.transform = transformStr.trim();
  1322. frames[i] = frame;
  1323. }
  1324. if (frames[0] && Object.keys(frames[0]).length === 0) frames.shift();
  1325. return frames;
  1326. }
  1327. // Non-spring: 2-keyframe path
  1328. const frame0 = {};
  1329. const frame1 = {};
  1330. let transform0 = '';
  1331. let transform1 = '';
  1332. Object.keys(propValues).forEach((k) => {
  1333. const val = propValues[k];
  1334. const isT = TRANSFORMS.includes(ALIASES[k] || k) || TRANSFORMS.includes(k);
  1335. if (Array.isArray(val)) {
  1336. const from = isT ? normalizeTransformPartValue(k, val[0]) : val[0];
  1337. const to = isT ? normalizeTransformPartValue(k, val[1]) : val[1];
  1338. if (isT) {
  1339. transform0 += `${k}(${from}) `;
  1340. transform1 += `${k}(${to}) `;
  1341. } else {
  1342. frame0[k] = from;
  1343. frame1[k] = to;
  1344. }
  1345. } else {
  1346. if (isT) transform1 += `${k}(${normalizeTransformPartValue(k, val)}) `;
  1347. else frame1[k] = val;
  1348. }
  1349. });
  1350. if (transform0) frame0.transform = transform0.trim();
  1351. if (transform1) frame1.transform = transform1.trim();
  1352. const out = [frame0, frame1];
  1353. if (Object.keys(out[0]).length === 0) out.shift();
  1354. return out;
  1355. };
  1356. const finalFrames = buildFrames(waapiProps);
  1357. const opts = {
  1358. duration: isSpring ? springDurationMs : duration,
  1359. delay,
  1360. fill,
  1361. iterations: loop,
  1362. easing: isSpring ? 'linear' : easing,
  1363. direction,
  1364. endDelay
  1365. };
  1366. const animation = el.animate(finalFrames, opts);
  1367. if (!autoplay) animation.pause();
  1368. waapiAnim = animation;
  1369. waapiPromise = animation.finished;
  1370. waapiAnimations.push(waapiAnim);
  1371. if (waapiTargetByAnim) waapiTargetByAnim.set(waapiAnim, el);
  1372. if (waapiTimingByAnim) waapiTimingByAnim.set(waapiAnim, readWaapiEffectTiming(waapiAnim.effect));
  1373. if (begin) {
  1374. // Fire begin when play starts (and also for autoplay on next frame)
  1375. onPlayHooks.push(() => fireBegin(el));
  1376. if (autoplay && typeof requestAnimationFrame !== 'undefined') requestAnimationFrame(() => fireBegin(el));
  1377. }
  1378. if (complete) {
  1379. waapiAnim.addEventListener?.('finish', () => fireComplete(el));
  1380. }
  1381. if (update) {
  1382. const timing = (waapiTimingByAnim && waapiTimingByAnim.get(waapiAnim)) || readWaapiEffectTiming(waapiAnim.effect);
  1383. const item = { anim: waapiAnim, target: el, update: fireUpdate, _lastP: null, _dur: timing.duration || 0 };
  1384. waapiUpdateItems.push(item);
  1385. // Ensure removal on finish/cancel
  1386. waapiAnim.addEventListener?.('finish', () => waapiUpdateSampler.remove(item));
  1387. waapiAnim.addEventListener?.('cancel', () => waapiUpdateSampler.remove(item));
  1388. // Start sampler only when needed (autoplay or explicit play)
  1389. if (autoplay) ensureWaapiUpdate();
  1390. }
  1391. }
  1392. // 2. JS Animation (Fallback / Attributes)
  1393. let jsPromise = Promise.resolve();
  1394. if (Object.keys(jsProps).length > 0) {
  1395. const jsAnim = new JsAnimation(
  1396. el,
  1397. jsProps,
  1398. { duration, delay, easing, autoplay, direction, loop, endDelay, springValues: isSpring ? springValuesRaw : null },
  1399. { update: fireUpdate, begin: fireBegin, complete: fireComplete }
  1400. );
  1401. jsAnimations.push(jsAnim);
  1402. jsPromise = jsAnim.finished;
  1403. }
  1404. return Promise.all([waapiPromise, jsPromise]);
  1405. });
  1406. const finished = Promise.all(promises);
  1407. const controls = new Controls({ waapi: waapiAnimations, js: jsAnimations, finished });
  1408. controls.engine = engineInfo;
  1409. controls._onPlay = onPlayHooks;
  1410. controls._fireUpdate = fireUpdate;
  1411. controls._waapiTargetByAnim = waapiTargetByAnim;
  1412. controls._waapiTimingByAnim = waapiTimingByAnim;
  1413. controls._ensureWaapiUpdate = update ? ensureWaapiUpdate : null;
  1414. if (!autoplay) controls.pause();
  1415. engineInfo.waapiKeys = Array.from(waapiKeySet);
  1416. engineInfo.jsKeys = Array.from(jsKeySet);
  1417. return controls;
  1418. }
  1419. // --- SVG Draw ---
  1420. function svgDraw(targets, params = {}) {
  1421. const elements = toArray(targets);
  1422. elements.forEach(el => {
  1423. if (!isSVG(el)) return;
  1424. const len = el.getTotalLength ? el.getTotalLength() : 0;
  1425. el.style.strokeDasharray = len;
  1426. el.style.strokeDashoffset = len;
  1427. animate(el, {
  1428. strokeDashoffset: [len, 0],
  1429. ...params
  1430. });
  1431. });
  1432. }
  1433. // --- In View ---
  1434. function inViewAnimate(targets, params, options = {}) {
  1435. const elements = toArray(targets);
  1436. const observer = new IntersectionObserver((entries) => {
  1437. entries.forEach(entry => {
  1438. if (entry.isIntersecting) {
  1439. animate(entry.target, params);
  1440. if (options.once !== false) observer.unobserve(entry.target);
  1441. }
  1442. });
  1443. }, { threshold: options.threshold || 0.1 });
  1444. elements.forEach(el => observer.observe(el));
  1445. // Return cleanup so callers can disconnect observers in long-lived pages (esp. once:false).
  1446. return () => {
  1447. try {
  1448. elements.forEach((el) => observer.unobserve(el));
  1449. observer.disconnect();
  1450. } catch (e) {
  1451. // ignore
  1452. }
  1453. };
  1454. }
  1455. // --- Scroll Linked ---
  1456. function scroll(animationPromise, options = {}) {
  1457. // options: container (default window), range [start, end] (default viewport logic)
  1458. const container = options.container || window;
  1459. const target = options.target || document.body; // Element to track for progress
  1460. // If passing an animation promise, we control its WAAPI animations
  1461. const controls = animationPromise;
  1462. // Back-compat: old return value was a Promise with `.animations`
  1463. const hasSeek = controls && isFunc(controls.seek);
  1464. const anims = (controls && controls.animations) || (animationPromise && animationPromise.animations) || [];
  1465. const jsAnims = (controls && controls.jsAnimations) || [];
  1466. if (!hasSeek && !anims.length && !jsAnims.length) return;
  1467. // Cache WAAPI timing per animation to avoid repeated getComputedTiming() during scroll.
  1468. const timingCache = (typeof WeakMap !== 'undefined') ? new WeakMap() : null;
  1469. const getEndTime = (anim) => {
  1470. if (!anim || !anim.effect) return 0;
  1471. if (timingCache) {
  1472. const cached = timingCache.get(anim);
  1473. if (cached) return cached;
  1474. }
  1475. const timing = readWaapiEffectTiming(anim.effect);
  1476. const end = timing.duration || 0;
  1477. if (timingCache && end) timingCache.set(anim, end);
  1478. return end;
  1479. };
  1480. const updateScroll = () => {
  1481. let progress = 0;
  1482. if (container === window) {
  1483. const scrollY = window.scrollY;
  1484. const winH = window.innerHeight;
  1485. const docH = document.body.scrollHeight;
  1486. // Simple progress: how far down the page (0 to 1)
  1487. // Or element based?
  1488. // Motion One defaults to element entering view.
  1489. if (options.target) {
  1490. const rect = options.target.getBoundingClientRect();
  1491. const start = winH;
  1492. const end = -rect.height;
  1493. // progress 0 when rect.top == start (just entering)
  1494. // progress 1 when rect.top == end (just left)
  1495. const totalDistance = start - end;
  1496. const currentDistance = start - rect.top;
  1497. progress = currentDistance / totalDistance;
  1498. } else {
  1499. // Whole page scroll
  1500. progress = scrollY / (docH - winH);
  1501. }
  1502. } else if (container && (typeof Element !== 'undefined') && (container instanceof Element)) {
  1503. // Scroll container progress
  1504. const el = container;
  1505. const scrollTop = el.scrollTop;
  1506. const max = (el.scrollHeight - el.clientHeight) || 1;
  1507. if (options.target) {
  1508. const containerRect = el.getBoundingClientRect();
  1509. const rect = options.target.getBoundingClientRect();
  1510. const start = containerRect.height;
  1511. const end = -rect.height;
  1512. const totalDistance = start - end;
  1513. const currentDistance = start - (rect.top - containerRect.top);
  1514. progress = currentDistance / totalDistance;
  1515. } else {
  1516. progress = scrollTop / max;
  1517. }
  1518. }
  1519. // Clamp
  1520. progress = clamp01(progress);
  1521. if (hasSeek) {
  1522. controls.seek(progress);
  1523. return;
  1524. }
  1525. anims.forEach((anim) => {
  1526. if (anim.effect) {
  1527. const end = getEndTime(anim);
  1528. if (!end) return;
  1529. anim.currentTime = end * progress;
  1530. }
  1531. });
  1532. };
  1533. let rafId = 0;
  1534. const onScroll = () => {
  1535. if (rafId) return;
  1536. rafId = requestAnimationFrame(() => {
  1537. rafId = 0;
  1538. updateScroll();
  1539. });
  1540. };
  1541. const eventTarget = (container && container.addEventListener) ? container : window;
  1542. eventTarget.addEventListener('scroll', onScroll, { passive: true });
  1543. updateScroll(); // Initial
  1544. return () => {
  1545. if (rafId) cancelAnimationFrame(rafId);
  1546. eventTarget.removeEventListener('scroll', onScroll);
  1547. };
  1548. }
  1549. // --- Timeline ---
  1550. function timeline(defaults = {}) {
  1551. const steps = [];
  1552. const api = {
  1553. currentTime: 0,
  1554. add: (targets, params, offset) => {
  1555. const animParams = { ...defaults, ...params };
  1556. let start = api.currentTime;
  1557. if (offset !== undefined) {
  1558. if (isStr(offset) && offset.startsWith('-=')) start -= parseFloat(offset.slice(2));
  1559. else if (isStr(offset) && offset.startsWith('+=')) start += parseFloat(offset.slice(2));
  1560. else if (typeof offset === 'number') start = offset;
  1561. }
  1562. const dur = animParams.duration || 1000;
  1563. const step = { targets, animParams, start, _scheduled: false };
  1564. steps.push(step);
  1565. // Backward compatible: schedule immediately (existing docs rely on this)
  1566. if (start <= 0) {
  1567. animate(targets, animParams);
  1568. step._scheduled = true;
  1569. } else {
  1570. setTimeout(() => {
  1571. animate(targets, animParams);
  1572. }, start);
  1573. step._scheduled = true;
  1574. }
  1575. api.currentTime = Math.max(api.currentTime, start + dur);
  1576. return api;
  1577. },
  1578. // Optional: if you create a timeline and want to defer scheduling yourself
  1579. play: () => {
  1580. steps.forEach((s) => {
  1581. if (s._scheduled) return;
  1582. if (s.start <= 0) animate(s.targets, s.animParams);
  1583. else setTimeout(() => animate(s.targets, s.animParams), s.start);
  1584. s._scheduled = true;
  1585. });
  1586. return api;
  1587. }
  1588. };
  1589. return api;
  1590. }
  1591. // --- Export ---
  1592. // transform `$` to be the main export `animal`, with statics attached
  1593. const animal = $;
  1594. // Extend $ behavior to act as a global selector and property accessor
  1595. Object.assign(animal, {
  1596. animate,
  1597. timeline,
  1598. draw: svgDraw,
  1599. svgDraw,
  1600. inViewAnimate,
  1601. spring,
  1602. scroll,
  1603. $: animal // Self-reference for backward compatibility
  1604. });
  1605. // Expose Layer if available or allow lazy loading
  1606. Object.defineProperty(animal, 'Layer', {
  1607. get: () => {
  1608. return (typeof window !== 'undefined' && window.Layer) ? window.Layer :
  1609. (typeof globalThis !== 'undefined' && globalThis.Layer) ? globalThis.Layer :
  1610. (typeof Layer !== 'undefined' ? Layer : null);
  1611. }
  1612. });
  1613. // Shortcut for Layer.fire or new Layer()
  1614. // Allows $.fire({ title: 'Hi' }) or $.layer({ title: 'Hi' })
  1615. animal.fire = (options) => {
  1616. const L = animal.Layer;
  1617. if (L) return (L.fire ? L.fire(options) : new L(options).fire());
  1618. console.warn('Layer module not loaded.');
  1619. return Promise.reject('Layer module not loaded');
  1620. };
  1621. // 'layer' alias for static usage
  1622. animal.layer = animal.fire;
  1623. return animal;
  1624. })));
  1625. // Unified entrypoint
  1626. if (__GLOBAL__ && __GLOBAL__.animal) {
  1627. __GLOBAL__.xjs = __GLOBAL__.animal;
  1628. }
  1629. // Optional jQuery-like alias (opt-in, and won't clobber existing $)
  1630. try {
  1631. if (__GLOBAL__ && __GLOBAL__.XJS_GLOBAL_DOLLAR === true && !__GLOBAL__.$ && __GLOBAL__.xjs) {
  1632. __GLOBAL__.$ = __GLOBAL__.xjs;
  1633. }
  1634. } catch (_) {}
  1635. })();