xjs.js 76 KB

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