xjs.js 71 KB

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