xjs.js 79 KB

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