xjs.js 65 KB

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