xjs.js 78 KB

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