xjs.js 70 KB

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