| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293 |
- // xjs.js (development loader)
- // - Dev/test: lightweight loader that imports source files (animal.js + layer.js)
- // - Production: use the bundled output from `go run main.go build` (defaults to dist/xjs.js)
- ;(function (global) {
- 'use strict';
- const g = global || (typeof window !== 'undefined' ? window : globalThis);
- const doc = (typeof document !== 'undefined') ? document : null;
- if (!doc) return;
- // Avoid double-loading
- if (g.xjs && g.Layer) return;
- const current = doc.currentScript;
- const base = (() => {
- try {
- const u = new URL(current && current.src ? current.src : 'xjs.js', location.href);
- return u.href.replace(/[^/]*$/, ''); // directory of xjs.js
- } catch {
- return '';
- }
- })();
- const loadScript = (src) => new Promise((resolve, reject) => {
- const s = doc.createElement('script');
- s.src = src;
- s.async = false; // preserve execution order
- s.onload = () => resolve();
- s.onerror = () => reject(new Error('Failed to load: ' + src));
- (doc.head || doc.documentElement).appendChild(s);
- });
- const ensureGlobals = () => {
- try {
- if (g.animal && !g.xjs) g.xjs = g.animal;
- // Optional: export `$` only when explicitly requested
- if (g.XJS_GLOBAL_DOLLAR && !g.$ && g.xjs) g.$ = g.xjs;
- } catch {}
- };
- (async () => {
- // Load animal first so xjs alias is available before layer tries to query it
- if (!g.animal) await loadScript(base + 'animal.js');
- ensureGlobals();
- if (!g.Layer) await loadScript(base + 'layer.js');
- ensureGlobals();
- })().catch((err) => {
- try { console.error('[xjs.dev] load failed', err); } catch {}
- });
- })(typeof window !== 'undefined' ? window : globalThis);
- // xjs.js - combined single-file build (animal + layer)
- // Generated from source files in this repo.
- // Usage:
- // <script src="xjs.js"></script>
- // Globals:
- // - window.xjs (primary, same as animal)
- // - window.animal (compat)
- // - window.Layer
- // Optional:
- // - set window.XJS_GLOBAL_DOLLAR = true before loading to also export window.$ (only if not already defined)
- (function(){
- var __GLOBAL__ = (typeof window !== 'undefined') ? window : (typeof globalThis !== 'undefined' ? globalThis : this);
- /* --- layer.js --- */
- (function (global, factory) {
- const LayerClass = factory();
- // Allow usage as `Layer({...})` or `new Layer()`
- // But Layer is a class. We can wrap it in a proxy or factory function.
-
- function LayerFactory(options) {
- if (options && typeof options === 'object') {
- return LayerClass.$(options);
- }
- return new LayerClass();
- }
-
- // Copy static methods (including non-enumerable class statics like `fire` / `$`)
- // Class static methods are non-enumerable by default, so Object.assign() would miss them.
- const copyStatic = (to, from) => {
- try {
- Object.getOwnPropertyNames(from).forEach((k) => {
- if (k === 'prototype' || k === 'name' || k === 'length') return;
- const desc = Object.getOwnPropertyDescriptor(from, k);
- if (!desc) return;
- Object.defineProperty(to, k, desc);
- });
- } catch (e) {
- // Best-effort fallback
- try { Object.assign(to, from); } catch {}
- }
- };
- copyStatic(LayerFactory, LayerClass);
- // Also copy prototype for instanceof checks if needed (though tricky with factory)
- LayerFactory.prototype = LayerClass.prototype;
-
- typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = LayerFactory :
- typeof define === 'function' && define.amd ? define(() => LayerFactory) :
- (global.Layer = LayerFactory);
- }(__GLOBAL__, (function () {
- 'use strict';
- const PREFIX = 'layer-';
-
- // Theme & Styles
- const css = `
- .${PREFIX}overlay {
- position: fixed;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- background: rgba(0, 0, 0, 0.4);
- display: flex;
- justify-content: center;
- align-items: center;
- z-index: 1000;
- opacity: 0;
- transition: opacity 0.3s;
- }
- .${PREFIX}overlay.show {
- opacity: 1;
- }
- .${PREFIX}popup {
- background: #fff;
- border-radius: 8px;
- box-shadow: 0 4px 12px rgba(0,0,0,0.15);
- width: 32em;
- max-width: 90%;
- padding: 1.5em;
- display: flex;
- flex-direction: column;
- align-items: center;
- position: relative;
- z-index: 1;
- transform: scale(0.92);
- transition: transform 0.26s ease;
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
- }
- .${PREFIX}overlay.show .${PREFIX}popup {
- transform: scale(1);
- }
- .${PREFIX}title {
- font-size: 1.8em;
- font-weight: 600;
- color: #333;
- margin: 0 0 0.5em;
- text-align: center;
- }
- .${PREFIX}content {
- font-size: 1.125em;
- color: #545454;
- margin-bottom: 1.5em;
- text-align: center;
- line-height: 1.5;
- }
- .${PREFIX}actions {
- display: flex;
- justify-content: center;
- gap: 1em;
- width: 100%;
- }
- .${PREFIX}button {
- border: none;
- border-radius: 4px;
- padding: 0.6em 1.2em;
- font-size: 1em;
- cursor: pointer;
- transition: background-color 0.2s, box-shadow 0.2s;
- color: #fff;
- }
- .${PREFIX}confirm {
- background-color: #3085d6;
- }
- .${PREFIX}confirm:hover {
- background-color: #2b77c0;
- }
- .${PREFIX}cancel {
- background-color: #aaa;
- }
- .${PREFIX}cancel:hover {
- background-color: #999;
- }
- .${PREFIX}icon {
- position: relative;
- box-sizing: content-box;
- width: 5em;
- height: 5em;
- margin: 2.5em auto 0.6em;
- border: 0.25em solid rgba(0, 0, 0, 0);
- border-radius: 50%;
- font-family: inherit;
- line-height: 5em;
- cursor: default;
- user-select: none;
- /* Ring sweep angles (match SweetAlert2 success feel) */
- --${PREFIX}ring-start-rotate: -45deg;
- /* End is one full turn from start so it "sweeps then settles" */
- --${PREFIX}ring-end-rotate: -405deg;
- }
- /* SVG mark content (kept), sits above the ring */
- .${PREFIX}icon svg {
- width: 100%;
- height: 100%;
- display: block;
- overflow: visible;
- position: relative;
- z-index: 3;
- }
- .${PREFIX}svg-ring,
- .${PREFIX}svg-mark {
- fill: none;
- stroke-linecap: round;
- stroke-linejoin: round;
- }
- .${PREFIX}svg-ring {
- stroke-width: 4.5;
- opacity: 0.95;
- }
- .${PREFIX}svg-mark {
- stroke-width: 6;
- }
- .${PREFIX}svg-dot {
- transform-box: fill-box;
- transform-origin: center;
- }
- /* =========================================
- "success-like" ring (shared by all icons)
- - we reuse the success ring pieces/rotation for every icon type
- - color is driven by currentColor
- ========================================= */
- .${PREFIX}icon.success { border-color: #a5dc86; color: #a5dc86; }
- .${PREFIX}icon.error { border-color: #f27474; color: #f27474; }
- .${PREFIX}icon.warning { border-color: #f8bb86; color: #f8bb86; }
- .${PREFIX}icon.info { border-color: #3fc3ee; color: #3fc3ee; }
- .${PREFIX}icon.question { border-color: #b18cff; color: #b18cff; }
- /* Keep ring sweep logic identical across built-in icons (match success). */
- .${PREFIX}icon .${PREFIX}success-ring {
- position: absolute;
- z-index: 2;
- top: -0.25em;
- left: -0.25em;
- box-sizing: content-box;
- width: 100%;
- height: 100%;
- border: 0.25em solid currentColor;
- opacity: 0.3;
- border-radius: 50%;
- }
- .${PREFIX}icon .${PREFIX}success-fix {
- position: absolute;
- z-index: 1;
- top: 0.5em;
- left: 1.625em;
- width: 0.4375em;
- height: 5.625em;
- transform: rotate(-45deg);
- background-color: #fff; /* adjusted at runtime to popup bg */
- }
- .${PREFIX}icon .${PREFIX}success-circular-line-left,
- .${PREFIX}icon .${PREFIX}success-circular-line-right {
- position: absolute;
- width: 3.75em;
- height: 7.5em;
- border-radius: 50%;
- background-color: #fff; /* adjusted at runtime to popup bg */
- }
- .${PREFIX}icon .${PREFIX}success-circular-line-left {
- top: -0.4375em;
- left: -2.0635em;
- transform: rotate(-45deg);
- transform-origin: 3.75em 3.75em;
- border-radius: 7.5em 0 0 7.5em;
- }
- .${PREFIX}icon .${PREFIX}success-circular-line-right {
- top: -0.6875em;
- left: 1.875em;
- transform: rotate(-45deg);
- transform-origin: 0 3.75em;
- border-radius: 0 7.5em 7.5em 0;
- }
- .${PREFIX}icon .${PREFIX}success-line-tip,
- .${PREFIX}icon .${PREFIX}success-line-long {
- display: block;
- position: absolute;
- z-index: 2;
- height: 0.3125em;
- border-radius: 0.125em;
- background-color: currentColor;
- }
- .${PREFIX}icon .${PREFIX}success-line-tip {
- top: 2.875em;
- left: 0.8125em;
- width: 1.5625em;
- transform: rotate(45deg);
- }
- .${PREFIX}icon .${PREFIX}success-line-long {
- top: 2.375em;
- right: 0.5em;
- width: 2.9375em;
- transform: rotate(-45deg);
- }
- /* Triggered when icon has .${PREFIX}icon-show */
- .${PREFIX}icon.${PREFIX}icon-show .${PREFIX}success-line-tip {
- animation: ${PREFIX}animate-success-line-tip 0.75s;
- }
- .${PREFIX}icon.${PREFIX}icon-show .${PREFIX}success-line-long {
- animation: ${PREFIX}animate-success-line-long 0.75s;
- }
- .${PREFIX}icon.${PREFIX}icon-show .${PREFIX}success-circular-line-right {
- animation: ${PREFIX}rotate-icon-circular-line 4.25s ease-in;
- }
- @keyframes ${PREFIX}animate-success-line-tip {
- 0% {
- top: 1.1875em;
- left: 0.0625em;
- width: 0;
- }
- 54% {
- top: 1.0625em;
- left: 0.125em;
- width: 0;
- }
- 70% {
- top: 2.1875em;
- left: -0.375em;
- width: 3.125em;
- }
- 84% {
- top: 3em;
- left: 1.3125em;
- width: 1.0625em;
- }
- 100% {
- top: 2.8125em;
- left: 0.8125em;
- width: 1.5625em;
- }
- }
- @keyframes ${PREFIX}animate-success-line-long {
- 0% {
- top: 3.375em;
- right: 2.875em;
- width: 0;
- }
- 65% {
- top: 3.375em;
- right: 2.875em;
- width: 0;
- }
- 84% {
- top: 2.1875em;
- right: 0;
- width: 3.4375em;
- }
- 100% {
- top: 2.375em;
- right: 0.5em;
- width: 2.9375em;
- }
- }
- @keyframes ${PREFIX}rotate-icon-circular-line {
- 0% { transform: rotate(var(--${PREFIX}ring-start-rotate)); }
- 5% { transform: rotate(var(--${PREFIX}ring-start-rotate)); }
- 12% { transform: rotate(var(--${PREFIX}ring-end-rotate)); }
- 100% { transform: rotate(var(--${PREFIX}ring-end-rotate)); } /* match 12% so it "sweeps then stops" */
- }
- /* (Other icon shapes stay SVG-driven; only the ring animation is unified above) */
- `;
- // Inject Styles
- const injectStyles = () => {
- if (document.getElementById(`${PREFIX}styles`)) return;
- const styleSheet = document.createElement('style');
- styleSheet.id = `${PREFIX}styles`;
- styleSheet.textContent = css;
- document.head.appendChild(styleSheet);
- };
- class Layer {
- constructor() {
- injectStyles();
- this.params = {};
- this.dom = {};
- this.promise = null;
- this.resolve = null;
- this.reject = null;
- this._onKeydown = null;
- }
- // Constructor helper when called as function: const popup = Layer({...})
- static get isProxy() { return true; }
-
- // Static entry point
- static fire(options) {
- const instance = new Layer();
- return instance._fire(options);
- }
-
- // Chainable entry point (builder-style)
- // Example:
- // Layer.$({ title: 'Hi' }).fire().then(...)
- // Layer.$().config({ title: 'Hi' }).fire()
- static $(options) {
- const instance = new Layer();
- if (options !== undefined) instance.config(options);
- return instance;
- }
-
- // Chainable config helper (does not render until `.fire()` is called)
- config(options = {}) {
- // Support the same shorthand as Layer.fire(title, text, icon)
- if (typeof options === 'string') {
- options = { title: options };
- if (arguments[1]) options.text = arguments[1];
- if (arguments[2]) options.icon = arguments[2];
- }
- this.params = { ...(this.params || {}), ...options };
- return this;
- }
-
- // Instance entry point (chainable)
- fire(options) {
- const merged = (options === undefined) ? (this.params || {}) : options;
- return this._fire(merged);
- }
- _fire(options = {}) {
- if (typeof options === 'string') {
- options = { title: options };
- if (arguments[1]) options.text = arguments[1];
- if (arguments[2]) options.icon = arguments[2];
- }
- this.params = {
- title: '',
- text: '',
- icon: null,
- iconSize: null, // e.g. '6em' / '72px'
- confirmButtonText: 'OK',
- cancelButtonText: 'Cancel',
- showCancelButton: false,
- confirmButtonColor: '#3085d6',
- cancelButtonColor: '#aaa',
- closeOnClickOutside: true,
- closeOnEsc: true,
- iconAnimation: true,
- popupAnimation: true,
- ...options
- };
- this.promise = new Promise((resolve, reject) => {
- this.resolve = resolve;
- this.reject = reject;
- });
- this._render();
- return this.promise;
- }
- static _getXjs() {
- // Prefer xjs, fallback to animal (compat)
- const g = (typeof window !== 'undefined') ? window : (typeof globalThis !== 'undefined' ? globalThis : null);
- if (!g) return null;
- const x = g.xjs || g.animal;
- return (typeof x === 'function') ? x : null;
- }
- _render() {
- // Remove existing if any
- const existing = document.querySelector(`.${PREFIX}overlay`);
- if (existing) existing.remove();
- // Create Overlay
- this.dom.overlay = document.createElement('div');
- this.dom.overlay.className = `${PREFIX}overlay`;
- // Create Popup
- this.dom.popup = document.createElement('div');
- this.dom.popup.className = `${PREFIX}popup`;
- this.dom.overlay.appendChild(this.dom.popup);
- // Icon
- if (this.params.icon) {
- this.dom.icon = this._createIcon(this.params.icon);
- this.dom.popup.appendChild(this.dom.icon);
- }
- // Title
- if (this.params.title) {
- this.dom.title = document.createElement('h2');
- this.dom.title.className = `${PREFIX}title`;
- this.dom.title.textContent = this.params.title;
- this.dom.popup.appendChild(this.dom.title);
- }
- // Content (Text / HTML / Element)
- if (this.params.text || this.params.html || this.params.content) {
- this.dom.content = document.createElement('div');
- this.dom.content.className = `${PREFIX}content`;
-
- if (this.params.content) {
- // DOM Element or Selector
- let el = this.params.content;
- if (typeof el === 'string') el = document.querySelector(el);
- if (el instanceof Element) this.dom.content.appendChild(el);
- } else if (this.params.html) {
- this.dom.content.innerHTML = this.params.html;
- } else {
- this.dom.content.textContent = this.params.text;
- }
-
- this.dom.popup.appendChild(this.dom.content);
- }
- // Actions
- this.dom.actions = document.createElement('div');
- this.dom.actions.className = `${PREFIX}actions`;
-
- // Cancel Button
- if (this.params.showCancelButton) {
- this.dom.cancelBtn = document.createElement('button');
- this.dom.cancelBtn.className = `${PREFIX}button ${PREFIX}cancel`;
- this.dom.cancelBtn.textContent = this.params.cancelButtonText;
- this.dom.cancelBtn.style.backgroundColor = this.params.cancelButtonColor;
- this.dom.cancelBtn.onclick = () => this._close(false);
- this.dom.actions.appendChild(this.dom.cancelBtn);
- }
- // Confirm Button
- this.dom.confirmBtn = document.createElement('button');
- this.dom.confirmBtn.className = `${PREFIX}button ${PREFIX}confirm`;
- this.dom.confirmBtn.textContent = this.params.confirmButtonText;
- this.dom.confirmBtn.style.backgroundColor = this.params.confirmButtonColor;
- this.dom.confirmBtn.onclick = () => this._close(true);
- this.dom.actions.appendChild(this.dom.confirmBtn);
- this.dom.popup.appendChild(this.dom.actions);
- // Event Listeners
- if (this.params.closeOnClickOutside) {
- this.dom.overlay.addEventListener('click', (e) => {
- if (e.target === this.dom.overlay) {
- this._close(null); // Dismiss
- }
- });
- }
- document.body.appendChild(this.dom.overlay);
- // Animation
- requestAnimationFrame(() => {
- this.dom.overlay.classList.add('show');
- this._didOpen();
- });
- }
- _createIcon(type) {
- const icon = document.createElement('div');
- icon.className = `${PREFIX}icon ${type}`;
-
- const applyIconSize = (mode) => {
- if (!(this.params && this.params.iconSize)) return;
- try {
- const raw = this.params.iconSize;
- const s = String(raw).trim();
- if (!s) return;
- // For SweetAlert2-style success icon, scale via font-size so all `em`-based
- // parts remain proportional.
- if (mode === 'font') {
- const m = s.match(/^(-?\d*\.?\d+)\s*(px|em|rem)$/i);
- if (m) {
- const n = parseFloat(m[1]);
- const unit = m[2];
- if (Number.isFinite(n) && n > 0) {
- icon.style.fontSize = (n / 5) + unit; // icon is 5em wide/tall
- return;
- }
- }
- }
- // Fallback: directly size the box (works great for SVG icons)
- icon.style.width = s;
- icon.style.height = s;
- } catch {}
- };
- const svgNs = 'http://www.w3.org/2000/svg';
- if (type === 'success') {
- // SweetAlert2-compatible DOM structure (circle + tick) for exact style parity
- const left = document.createElement('div');
- left.className = `${PREFIX}success-circular-line-left`;
- const tip = document.createElement('span');
- tip.className = `${PREFIX}success-line-tip`;
- const long = document.createElement('span');
- long.className = `${PREFIX}success-line-long`;
- const ring = document.createElement('div');
- ring.className = `${PREFIX}success-ring`;
- const fix = document.createElement('div');
- fix.className = `${PREFIX}success-fix`;
- const right = document.createElement('div');
- right.className = `${PREFIX}success-circular-line-right`;
- icon.appendChild(left);
- icon.appendChild(tip);
- icon.appendChild(long);
- icon.appendChild(ring);
- icon.appendChild(fix);
- icon.appendChild(right);
- applyIconSize('font');
- return icon;
- }
- if (type === 'error' || type === 'warning' || type === 'info' || type === 'question') {
- // Use the same "success-like" ring parts for every icon
- const left = document.createElement('div');
- left.className = `${PREFIX}success-circular-line-left`;
- const ring = document.createElement('div');
- ring.className = `${PREFIX}success-ring`;
- const fix = document.createElement('div');
- fix.className = `${PREFIX}success-fix`;
- const right = document.createElement('div');
- right.className = `${PREFIX}success-circular-line-right`;
- icon.appendChild(left);
- icon.appendChild(ring);
- icon.appendChild(fix);
- icon.appendChild(right);
- applyIconSize('font');
- // SVG only draws the inner symbol (no SVG ring)
- const svg = document.createElementNS(svgNs, 'svg');
- svg.setAttribute('viewBox', '0 0 80 80');
- svg.setAttribute('aria-hidden', 'true');
- svg.setAttribute('focusable', 'false');
- const addPath = (d, extraClass) => {
- const p = document.createElementNS(svgNs, 'path');
- p.setAttribute('d', d);
- p.setAttribute('class', `${PREFIX}svg-mark ${PREFIX}svg-draw${extraClass ? ' ' + extraClass : ''}`);
- p.setAttribute('stroke', 'currentColor');
- svg.appendChild(p);
- return p;
- };
- if (type === 'error') {
- addPath('M28 28 L52 52', `${PREFIX}svg-error-left`);
- addPath('M52 28 L28 52', `${PREFIX}svg-error-right`);
- } else if (type === 'warning') {
- addPath('M40 20 L40 46', `${PREFIX}svg-warning-line`);
- const dot = document.createElementNS(svgNs, 'circle');
- dot.setAttribute('class', `${PREFIX}svg-dot`);
- dot.setAttribute('cx', '40');
- dot.setAttribute('cy', '58');
- dot.setAttribute('r', '3.2');
- dot.setAttribute('fill', 'currentColor');
- svg.appendChild(dot);
- } else if (type === 'info') {
- addPath('M40 34 L40 56', `${PREFIX}svg-info-line`);
- const dot = document.createElementNS(svgNs, 'circle');
- dot.setAttribute('class', `${PREFIX}svg-dot`);
- dot.setAttribute('cx', '40');
- dot.setAttribute('cy', '25');
- dot.setAttribute('r', '3.2');
- dot.setAttribute('fill', 'currentColor');
- svg.appendChild(dot);
- } else if (type === 'question') {
- 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`);
- const dot = document.createElementNS(svgNs, 'circle');
- dot.setAttribute('class', `${PREFIX}svg-dot`);
- dot.setAttribute('cx', '42');
- dot.setAttribute('cy', '61');
- dot.setAttribute('r', '3.2');
- dot.setAttribute('fill', 'currentColor');
- svg.appendChild(dot);
- }
- icon.appendChild(svg);
- return icon;
- }
- applyIconSize('box');
- const svg = document.createElementNS(svgNs, 'svg');
- svg.setAttribute('viewBox', '0 0 80 80');
- svg.setAttribute('aria-hidden', 'true');
- svg.setAttribute('focusable', 'false');
- const ring = document.createElementNS(svgNs, 'circle');
- ring.setAttribute('class', `${PREFIX}svg-ring ${PREFIX}svg-draw`);
- ring.setAttribute('cx', '40');
- ring.setAttribute('cy', '40');
- ring.setAttribute('r', '34');
- ring.setAttribute('stroke', 'currentColor');
- svg.appendChild(ring);
- const addPath = (d, extraClass) => {
- const p = document.createElementNS(svgNs, 'path');
- p.setAttribute('d', d);
- p.setAttribute('class', `${PREFIX}svg-mark ${PREFIX}svg-draw${extraClass ? ' ' + extraClass : ''}`);
- p.setAttribute('stroke', 'currentColor');
- svg.appendChild(p);
- return p;
- };
- if (type === 'error') {
- addPath('M28 28 L52 52', `${PREFIX}svg-error-left`);
- addPath('M52 28 L28 52', `${PREFIX}svg-error-right`);
- } else if (type === 'warning') {
- addPath('M40 20 L40 46', `${PREFIX}svg-warning-line`);
- const dot = document.createElementNS(svgNs, 'circle');
- dot.setAttribute('class', `${PREFIX}svg-dot`);
- dot.setAttribute('cx', '40');
- dot.setAttribute('cy', '58');
- dot.setAttribute('r', '3.2');
- dot.setAttribute('fill', 'currentColor');
- svg.appendChild(dot);
- } else if (type === 'info') {
- addPath('M40 34 L40 56', `${PREFIX}svg-info-line`);
- const dot = document.createElementNS(svgNs, 'circle');
- dot.setAttribute('class', `${PREFIX}svg-dot`);
- dot.setAttribute('cx', '40');
- dot.setAttribute('cy', '25');
- dot.setAttribute('r', '3.2');
- dot.setAttribute('fill', 'currentColor');
- svg.appendChild(dot);
- } else if (type === 'question') {
- 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`);
- const dot = document.createElementNS(svgNs, 'circle');
- dot.setAttribute('class', `${PREFIX}svg-dot`);
- dot.setAttribute('cx', '42');
- dot.setAttribute('cy', '61');
- dot.setAttribute('r', '3.2');
- dot.setAttribute('fill', 'currentColor');
- svg.appendChild(dot);
- } else {
- // ring only
- }
- icon.appendChild(svg);
-
- return icon;
- }
- _adjustRingBackgroundColor() {
- try {
- const icon = this.dom && this.dom.icon;
- const popup = this.dom && this.dom.popup;
- if (!icon || !popup) return;
- const bg = getComputedStyle(popup).backgroundColor;
- const parts = icon.querySelectorAll(`.${PREFIX}success-circular-line-left, .${PREFIX}success-circular-line-right, .${PREFIX}success-fix`);
- parts.forEach((el) => {
- try { el.style.backgroundColor = bg; } catch {}
- });
- } catch {}
- }
- _didOpen() {
- // Keyboard close (ESC)
- if (this.params.closeOnEsc) {
- this._onKeydown = (e) => {
- if (!e) return;
- if (e.key === 'Escape') this._close(null);
- };
- document.addEventListener('keydown', this._onKeydown);
- }
- // Keep the "success-like" ring perfectly blended with popup bg
- this._adjustRingBackgroundColor();
- // Popup animation (optional)
- if (this.params.popupAnimation) {
- const X = Layer._getXjs();
- if (X && this.dom.popup) {
- try {
- this.dom.popup.style.transition = 'none';
- this.dom.popup.style.transform = 'scale(0.92)';
- X(this.dom.popup).animate({
- scale: [0.92, 1],
- y: [-6, 0],
- duration: 520,
- easing: { stiffness: 260, damping: 16 }
- });
- } catch {}
- }
- }
- if (this.params.iconAnimation) {
- this._animateIcon();
- }
- try {
- if (typeof this.params.didOpen === 'function') this.params.didOpen(this.dom.popup);
- } catch {}
- }
- _animateIcon() {
- const icon = this.dom.icon;
- if (!icon) return;
- const type = (this.params && this.params.icon) || '';
- const ringTypes = new Set(['success', 'error', 'warning', 'info', 'question']);
- // Ring animation (same as success) for all built-in icons
- if (ringTypes.has(type)) this._adjustRingBackgroundColor();
- try { icon.classList.remove(`${PREFIX}icon-show`); } catch {}
- requestAnimationFrame(() => {
- try { icon.classList.add(`${PREFIX}icon-show`); } catch {}
- });
- // Success tick is CSS-driven; others still draw their SVG mark after the ring starts.
- if (type === 'success') return;
- const X = Layer._getXjs();
- if (!X) return;
- const svg = icon.querySelector('svg');
- if (!svg) return;
- const marks = Array.from(svg.querySelectorAll(`.${PREFIX}svg-mark`));
- const dot = svg.querySelector(`.${PREFIX}svg-dot`);
- // If this is a built-in icon, keep the SweetAlert-like order:
- // ring sweep first (~0.51s), then draw the inner mark.
- const baseDelay = ringTypes.has(type) ? 520 : 0;
- if (type === 'error') {
- // Draw order: left-top -> right-bottom, then right-top -> left-bottom
- // NOTE: A tiny delay (like 70ms) looks simultaneous; make it strictly sequential.
- const a = svg.querySelector(`.${PREFIX}svg-error-left`) || marks[0]; // M28 28 L52 52
- const b = svg.querySelector(`.${PREFIX}svg-error-right`) || marks[1]; // M52 28 L28 52
- const dur = 320;
- const gap = 60;
- try { if (a) X(a).draw({ duration: dur, easing: 'ease-out', delay: baseDelay }); } catch {}
- try { if (b) X(b).draw({ duration: dur, easing: 'ease-out', delay: baseDelay + dur + gap }); } catch {}
- } else {
- marks.forEach((m, i) => {
- try { X(m).draw({ duration: 420, easing: 'ease-out', delay: baseDelay + i * 60 }); } catch {}
- });
- }
- if (dot) {
- try {
- dot.style.opacity = '0';
- const d = baseDelay + 140;
- if (type === 'info') {
- X(dot).animate({ opacity: [0, 1], y: [-8, 0], scale: [0.2, 1], duration: 420, delay: d, easing: { stiffness: 300, damping: 14 } });
- } else {
- X(dot).animate({ opacity: [0, 1], scale: [0.2, 1], duration: 320, delay: d, easing: { stiffness: 320, damping: 18 } });
- }
- } catch {}
- }
- }
- _close(isConfirmed) {
- this.dom.overlay.classList.remove('show');
- setTimeout(() => {
- if (this.dom.overlay && this.dom.overlay.parentNode) {
- this.dom.overlay.parentNode.removeChild(this.dom.overlay);
- }
- }, 300);
- if (this._onKeydown) {
- try { document.removeEventListener('keydown', this._onKeydown); } catch {}
- this._onKeydown = null;
- }
- try {
- if (typeof this.params.willClose === 'function') this.params.willClose(this.dom.popup);
- } catch {}
- try {
- if (typeof this.params.didClose === 'function') this.params.didClose();
- } catch {}
- if (isConfirmed === true) {
- this.resolve({ isConfirmed: true, isDenied: false, isDismissed: false });
- } else if (isConfirmed === false) {
- this.resolve({ isConfirmed: false, isDenied: false, isDismissed: true, dismiss: 'cancel' });
- } else {
- this.resolve({ isConfirmed: false, isDenied: false, isDismissed: true, dismiss: 'backdrop' });
- }
- }
- }
- return Layer;
- })));
- /* --- animal.js --- */
- (function (global, factory) {
- typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
- typeof define === 'function' && define.amd ? define(factory) :
- (global.animal = factory());
- }(__GLOBAL__, (function () {
- 'use strict';
- // --- Utils ---
- const isArr = (a) => Array.isArray(a);
- const isStr = (s) => typeof s === 'string';
- const isFunc = (f) => typeof f === 'function';
- const isNil = (v) => v === undefined || v === null;
- const isSVG = (el) => (typeof SVGElement !== 'undefined' && el instanceof SVGElement) || ((typeof Element !== 'undefined') && (el instanceof Element) && el.namespaceURI === 'http://www.w3.org/2000/svg');
- const isEl = (v) => (typeof Element !== 'undefined') && (v instanceof Element);
- const clamp01 = (n) => Math.max(0, Math.min(1, n));
- const toKebab = (s) => s.replace(/[A-Z]/g, (m) => '-' + m.toLowerCase());
-
- const toArray = (targets) => {
- if (isArr(targets)) return targets;
- if (isStr(targets)) {
- if (typeof document === 'undefined') return [];
- return Array.from(document.querySelectorAll(targets));
- }
- if ((typeof NodeList !== 'undefined') && (targets instanceof NodeList)) return Array.from(targets);
- if ((typeof Element !== 'undefined') && (targets instanceof Element)) return [targets];
- if ((typeof Window !== 'undefined') && (targets instanceof Window)) return [targets];
- // Plain objects (for anime.js-style object tweening)
- if (!isNil(targets) && (typeof targets === 'object' || isFunc(targets))) return [targets];
- return [];
- };
- // Selection helper (chainable, still Array-compatible)
- const _layerHandlerByEl = (typeof WeakMap !== 'undefined') ? new WeakMap() : null;
- class Selection extends Array {
- animate(params) { return animate(this, params); }
- draw(params = {}) { return svgDraw(this, params); }
- inViewAnimate(params, options = {}) { return inViewAnimate(this, params, options); }
- // Layer.js plugin-style helper:
- // const $ = animal.$;
- // $('.btn').layer({ title: 'Hi' });
- // Clicking the element will open Layer.
- layer(options) {
- const LayerCtor =
- (typeof window !== 'undefined' && window.Layer) ? window.Layer :
- (typeof globalThis !== 'undefined' && globalThis.Layer) ? globalThis.Layer :
- (typeof Layer !== 'undefined' ? Layer : null);
- if (!LayerCtor) return this;
-
- this.forEach((el) => {
- if (!isEl(el)) return;
- // De-dupe / replace previous binding
- if (_layerHandlerByEl) {
- const prev = _layerHandlerByEl.get(el);
- if (prev) el.removeEventListener('click', prev);
- }
-
- const handler = () => {
- let opts = options;
- if (isFunc(options)) {
- opts = options(el);
- } else if (isNil(options)) {
- opts = null;
- }
- // If no explicit options, allow data-* configuration
- if (!opts || (typeof opts === 'object' && Object.keys(opts).length === 0)) {
- const d = el.dataset || {};
- opts = {
- title: d.layerTitle || el.getAttribute('data-layer-title') || (el.textContent || '').trim(),
- text: d.layerText || el.getAttribute('data-layer-text') || '',
- icon: d.layerIcon || el.getAttribute('data-layer-icon') || null,
- showCancelButton: (d.layerCancel === 'true') || (el.getAttribute('data-layer-cancel') === 'true')
- };
- }
-
- // Prefer builder API if present, fallback to static fire.
- if (LayerCtor.$ && isFunc(LayerCtor.$)) return LayerCtor.$(opts).fire();
- if (LayerCtor.fire && isFunc(LayerCtor.fire)) return LayerCtor.fire(opts);
- };
-
- if (_layerHandlerByEl) _layerHandlerByEl.set(el, handler);
- el.addEventListener('click', handler);
- });
-
- return this;
- }
-
- // Remove click bindings added by `.layer()`
- unlayer() {
- this.forEach((el) => {
- if (!isEl(el)) return;
- if (!_layerHandlerByEl) return;
- const prev = _layerHandlerByEl.get(el);
- if (prev) el.removeEventListener('click', prev);
- _layerHandlerByEl.delete(el);
- });
- return this;
- }
- }
-
- const $ = (targets) => Selection.from(toArray(targets));
- const UNITLESS_KEYS = ['opacity', 'scale', 'scaleX', 'scaleY', 'scaleZ', 'zIndex', 'fontWeight', 'strokeDashoffset', 'strokeDasharray', 'strokeWidth'];
- const getUnit = (val, prop) => {
- if (UNITLESS_KEYS.includes(prop)) return '';
- 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);
- return split ? split[1] : undefined;
- };
-
- const isCssVar = (k) => isStr(k) && k.startsWith('--');
- // Keep this intentionally broad so "unknown" config keys don't accidentally become animated props.
- // Prefer putting non-anim-prop config under `params.options`.
- const isAnimOptionKey = (k) => [
- 'options',
- 'duration', 'delay', 'easing', 'direction', 'fill', 'loop', 'endDelay', 'autoplay',
- 'update', 'begin', 'complete',
- // WAAPI-ish common keys (ignored unless we explicitly support them)
- 'iterations', 'iterationStart', 'iterationComposite', 'composite', 'playbackRate',
- // Spring helpers
- 'springFrames'
- ].includes(k);
-
- const isEasingFn = (e) => typeof e === 'function';
-
- // Minimal cubic-bezier implementation (for JS engine easing)
- function cubicBezier(x1, y1, x2, y2) {
- // Inspired by https://github.com/gre/bezier-easing (simplified)
- const NEWTON_ITERATIONS = 4;
- const NEWTON_MIN_SLOPE = 0.001;
- const SUBDIVISION_PRECISION = 0.0000001;
- const SUBDIVISION_MAX_ITERATIONS = 10;
-
- const kSplineTableSize = 11;
- const kSampleStepSize = 1.0 / (kSplineTableSize - 1.0);
-
- const float32ArraySupported = typeof Float32Array === 'function';
-
- function A(aA1, aA2) { return 1.0 - 3.0 * aA2 + 3.0 * aA1; }
- function B(aA1, aA2) { return 3.0 * aA2 - 6.0 * aA1; }
- function C(aA1) { return 3.0 * aA1; }
-
- function calcBezier(aT, aA1, aA2) {
- return ((A(aA1, aA2) * aT + B(aA1, aA2)) * aT + C(aA1)) * aT;
- }
-
- function getSlope(aT, aA1, aA2) {
- return 3.0 * A(aA1, aA2) * aT * aT + 2.0 * B(aA1, aA2) * aT + C(aA1);
- }
-
- function binarySubdivide(aX, aA, aB) {
- let currentX, currentT, i = 0;
- do {
- currentT = aA + (aB - aA) / 2.0;
- currentX = calcBezier(currentT, x1, x2) - aX;
- if (currentX > 0.0) aB = currentT;
- else aA = currentT;
- } while (Math.abs(currentX) > SUBDIVISION_PRECISION && ++i < SUBDIVISION_MAX_ITERATIONS);
- return currentT;
- }
-
- function newtonRaphsonIterate(aX, aGuessT) {
- for (let i = 0; i < NEWTON_ITERATIONS; ++i) {
- const currentSlope = getSlope(aGuessT, x1, x2);
- if (currentSlope === 0.0) return aGuessT;
- const currentX = calcBezier(aGuessT, x1, x2) - aX;
- aGuessT -= currentX / currentSlope;
- }
- return aGuessT;
- }
-
- const sampleValues = float32ArraySupported ? new Float32Array(kSplineTableSize) : new Array(kSplineTableSize);
- for (let i = 0; i < kSplineTableSize; ++i) {
- sampleValues[i] = calcBezier(i * kSampleStepSize, x1, x2);
- }
-
- function getTForX(aX) {
- let intervalStart = 0.0;
- let currentSample = 1;
- const lastSample = kSplineTableSize - 1;
-
- for (; currentSample !== lastSample && sampleValues[currentSample] <= aX; ++currentSample) {
- intervalStart += kSampleStepSize;
- }
- --currentSample;
-
- const dist = (aX - sampleValues[currentSample]) / (sampleValues[currentSample + 1] - sampleValues[currentSample]);
- const guessForT = intervalStart + dist * kSampleStepSize;
-
- const initialSlope = getSlope(guessForT, x1, x2);
- if (initialSlope >= NEWTON_MIN_SLOPE) return newtonRaphsonIterate(aX, guessForT);
- if (initialSlope === 0.0) return guessForT;
- return binarySubdivide(aX, intervalStart, intervalStart + kSampleStepSize);
- }
-
- return (x) => {
- if (x === 0 || x === 1) return x;
- return calcBezier(getTForX(x), y1, y2);
- };
- }
-
- function resolveJsEasing(easing, springValues) {
- if (isEasingFn(easing)) return easing;
- if (typeof easing === 'object' && easing && !Array.isArray(easing)) {
- // Spring: map linear progress -> simulated spring curve (0..1)
- const values = springValues || getSpringValues(easing);
- const last = Math.max(0, values.length - 1);
- return (t) => {
- const p = clamp01(t);
- const idx = p * last;
- const i0 = Math.floor(idx);
- const i1 = Math.min(last, i0 + 1);
- const frac = idx - i0;
- const v0 = values[i0] ?? p;
- const v1 = values[i1] ?? p;
- return v0 + (v1 - v0) * frac;
- };
- }
- if (Array.isArray(easing) && easing.length === 4) {
- return cubicBezier(easing[0], easing[1], easing[2], easing[3]);
- }
- // Common named easings
- switch (easing) {
- case 'linear': return (t) => t;
- case 'ease': return cubicBezier(0.25, 0.1, 0.25, 1);
- case 'ease-in': return cubicBezier(0.42, 0, 1, 1);
- case 'ease-out': return cubicBezier(0, 0, 0.58, 1);
- case 'ease-in-out': return cubicBezier(0.42, 0, 0.58, 1);
- default: return (t) => t;
- }
- }
- // --- Spring Physics (Simplified) ---
- // Returns an array of [time, value] or just value for WAAPI linear easing
- function spring({ stiffness = 100, damping = 10, mass = 1, velocity = 0, precision = 0.01 } = {}) {
- const values = [];
- let t = 0;
- const timeStep = 1 / 60; // 60fps simulation
- let current = 0;
- let v = velocity;
- const target = 1;
- let running = true;
- while (running && t < 10) { // Safety break at 10s
- const fSpring = -stiffness * (current - target);
- const fDamper = -damping * v;
- const a = (fSpring + fDamper) / mass;
-
- v += a * timeStep;
- current += v * timeStep;
-
- values.push(current);
- t += timeStep;
- if (Math.abs(current - target) < precision && Math.abs(v) < precision) {
- running = false;
- }
- }
- if (values[values.length - 1] !== 1) values.push(1);
-
- return values;
- }
-
- // Cache spring curves by config (perf: avoid recomputing for many targets)
- // LRU-ish capped cache to avoid unbounded growth in long-lived apps.
- const SPRING_CACHE_MAX = 50;
- const springCache = new Map();
- function springCacheGet(key) {
- const v = springCache.get(key);
- if (!v) return v;
- // refresh LRU
- springCache.delete(key);
- springCache.set(key, v);
- return v;
- }
- function springCacheSet(key, values) {
- if (springCache.has(key)) springCache.delete(key);
- springCache.set(key, values);
- if (springCache.size > SPRING_CACHE_MAX) {
- const firstKey = springCache.keys().next().value;
- if (firstKey !== undefined) springCache.delete(firstKey);
- }
- }
- function getSpringValues(config = {}) {
- const {
- stiffness = 100,
- damping = 10,
- mass = 1,
- velocity = 0,
- precision = 0.01
- } = config || {};
- const key = `${stiffness}|${damping}|${mass}|${velocity}|${precision}`;
- const cached = springCacheGet(key);
- if (cached) return cached;
- const values = spring({ stiffness, damping, mass, velocity, precision });
- springCacheSet(key, values);
- return values;
- }
-
- function downsample(values, maxFrames = 120) {
- if (!values || values.length <= maxFrames) return values || [];
- const out = [];
- const lastIndex = values.length - 1;
- const step = lastIndex / (maxFrames - 1);
- for (let i = 0; i < maxFrames; i++) {
- const idx = i * step;
- const i0 = Math.floor(idx);
- const i1 = Math.min(lastIndex, i0 + 1);
- const frac = idx - i0;
- const v0 = values[i0];
- const v1 = values[i1];
- out.push(v0 + (v1 - v0) * frac);
- }
- if (out[out.length - 1] !== 1) out[out.length - 1] = 1;
- return out;
- }
- // --- WAAPI Core ---
- const TRANSFORMS = ['translateX', 'translateY', 'translateZ', 'rotate', 'rotateX', 'rotateY', 'rotateZ', 'scale', 'scaleX', 'scaleY', 'scaleZ', 'skew', 'skewX', 'skewY', 'perspective', 'x', 'y'];
-
- const ALIASES = {
- x: 'translateX',
- y: 'translateY',
- z: 'translateZ'
- };
- // Basic rAF loop for non-WAAPI props or fallback
- class RafEngine {
- constructor() {
- this.animations = [];
- this.tick = this.tick.bind(this);
- this.running = false;
- }
- add(anim) {
- this.animations.push(anim);
- if (!this.running) {
- this.running = true;
- requestAnimationFrame(this.tick);
- }
- }
- remove(anim) {
- this.animations = this.animations.filter(a => a !== anim);
- }
- tick(t) {
- const now = t;
- this.animations = this.animations.filter(anim => {
- return anim.tick(now); // return true to keep
- });
- if (this.animations.length) {
- requestAnimationFrame(this.tick);
- } else {
- this.running = false;
- }
- }
- }
- const rafEngine = new RafEngine();
-
- // --- WAAPI update sampler (only used when update callback is provided) ---
- class WaapiUpdateSampler {
- constructor() {
- this.items = new Set();
- this._running = false;
- this._tick = this._tick.bind(this);
- }
- add(item) {
- this.items.add(item);
- if (!this._running) {
- this._running = true;
- requestAnimationFrame(this._tick);
- }
- }
- remove(item) {
- this.items.delete(item);
- }
- _tick(t) {
- if (!this.items.size) {
- this._running = false;
- return;
- }
- let anyActive = false;
- let anyChanged = false;
- this.items.forEach((item) => {
- const { anim, target, update } = item;
- if (!anim || !anim.effect) return;
- let dur = item._dur || 0;
- if (!dur) {
- const timing = readWaapiEffectTiming(anim.effect);
- dur = timing.duration || 0;
- item._dur = dur;
- }
- if (!dur) return;
- const p = clamp01((anim.currentTime || 0) / dur);
- if (anim.playState === 'running') anyActive = true;
- if (item._lastP !== p) {
- anyChanged = true;
- item._lastP = p;
- update({ target, progress: p, time: t });
- }
- if (anim.playState === 'finished' || anim.playState === 'idle') {
- this.items.delete(item);
- }
- });
- if (this.items.size && (anyActive || anyChanged)) {
- requestAnimationFrame(this._tick);
- } else {
- this._running = false;
- }
- }
- }
- const waapiUpdateSampler = new WaapiUpdateSampler();
-
- function readWaapiEffectTiming(effect) {
- // We compute this once per animation and cache it to avoid per-frame getComputedTiming().
- // We primarily cache `duration` to preserve existing behavior (progress maps to one iteration).
- let duration = 0;
- let endTime = 0;
- if (!effect) return { duration, endTime };
- try {
- const ct = effect.getComputedTiming ? effect.getComputedTiming() : null;
- if (ct) {
- if (typeof ct.duration === 'number' && Number.isFinite(ct.duration)) duration = ct.duration;
- if (typeof ct.endTime === 'number' && Number.isFinite(ct.endTime)) endTime = ct.endTime;
- }
- } catch (e) {
- // ignore
- }
- if (!endTime) endTime = duration || 0;
- return { duration, endTime };
- }
-
- function getDefaultUnit(prop) {
- if (!prop) return '';
- if (UNITLESS_KEYS.includes(prop)) return '';
- if (prop.startsWith('scale')) return '';
- if (prop === 'opacity') return '';
- if (prop.startsWith('rotate') || prop.startsWith('skew')) return 'deg';
- return 'px';
- }
-
- function normalizeTransformPartValue(prop, v) {
- if (typeof v !== 'number') return v;
- if (prop && prop.startsWith('scale')) return '' + v;
- if (prop && (prop.startsWith('rotate') || prop.startsWith('skew'))) return v + 'deg';
- return v + 'px';
- }
- // String number template: interpolate numeric tokens inside a string.
- // Useful for SVG path `d`, `points`, and other attributes that contain many numbers.
- 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)?$/;
- function isPureNumberLike(str) {
- return _PURE_NUMBER_RE.test(String(str || '').trim());
- }
- function countDecimals(numStr) {
- const s = String(numStr || '');
- const dot = s.indexOf('.');
- if (dot < 0) return 0;
- const e = s.search(/[eE]/);
- const end = e >= 0 ? e : s.length;
- const frac = s.slice(dot + 1, end);
- const trimmed = frac.replace(/0+$/, '');
- return Math.min(6, trimmed.length);
- }
- function parseNumberTemplate(str) {
- const s = String(str ?? '');
- const nums = [];
- const parts = [];
- let last = 0;
- const re = /[+-]?(?:\d*\.)?\d+(?:[eE][+-]?\d+)?/g;
- let m;
- while ((m = re.exec(s))) {
- const start = m.index;
- const end = start + m[0].length;
- parts.push(s.slice(last, start));
- nums.push(parseFloat(m[0]));
- last = end;
- }
- parts.push(s.slice(last));
- return { nums, parts };
- }
- function extractNumberStrings(str) {
- return String(str ?? '').match(/[+-]?(?:\d*\.)?\d+(?:[eE][+-]?\d+)?/g) || [];
- }
- function formatInterpolatedNumber(n, decimals) {
- let v = n;
- if (!Number.isFinite(v)) v = 0;
- if (Math.abs(v) < 1e-12) v = 0;
- if (!decimals) return String(Math.round(v));
- return v.toFixed(decimals).replace(/\.?0+$/, '');
- }
-
- // --- JS Interpolator (anime.js-ish, simplified) ---
- class JsAnimation {
- constructor(target, propValues, opts = {}, callbacks = {}) {
- this.target = target;
- this.propValues = propValues;
- this.duration = opts.duration ?? 1000;
- this.delay = opts.delay ?? 0;
- this.direction = opts.direction ?? 'normal';
- this.loop = opts.loop ?? 1;
- this.endDelay = opts.endDelay ?? 0;
- this.easing = opts.easing ?? 'linear';
- this.autoplay = opts.autoplay !== false;
-
- this.update = callbacks.update;
- this.begin = callbacks.begin;
- this.complete = callbacks.complete;
-
- this._resolve = null;
- this.finished = new Promise((res) => (this._resolve = res));
-
- this._started = false;
- this._running = false;
- this._paused = false;
- this._cancelled = false;
- this._startTime = 0;
- this._progress = 0;
- this._didBegin = false;
-
- this._ease = resolveJsEasing(this.easing, opts.springValues);
-
- this._tween = this._buildTween();
- this.tick = this.tick.bind(this);
-
- if (this.autoplay) this.play();
- }
-
- _readCurrentValue(k) {
- const t = this.target;
- // JS object
- if (!isEl(t) && !isSVG(t)) return t[k];
-
- // scroll
- if (k === 'scrollTop' || k === 'scrollLeft') return t[k];
-
- // CSS var
- if (isEl(t) && isCssVar(k)) {
- const cs = getComputedStyle(t);
- return (cs.getPropertyValue(k) || t.style.getPropertyValue(k) || '').trim();
- }
-
- // style
- if (isEl(t)) {
- const cs = getComputedStyle(t);
- // Prefer computed style (kebab) because many props aren't direct keys on cs
- const v = cs.getPropertyValue(toKebab(k));
- if (v && v.trim()) return v.trim();
- // Fallback to inline style access
- if (k in t.style) return t.style[k];
- }
-
- // SVG
- // For many SVG presentation attributes (e.g. strokeDashoffset), style overrides attribute.
- // Prefer computed style / inline style when available, and fallback to attributes.
- if (isSVG(t)) {
- // Geometry attrs must remain attrs (not style)
- if (k === 'd' || k === 'points') {
- const attr = toKebab(k);
- if (t.hasAttribute(attr)) return t.getAttribute(attr);
- if (t.hasAttribute(k)) return t.getAttribute(k);
- return t.getAttribute(k);
- }
- try {
- const cs = getComputedStyle(t);
- const v = cs.getPropertyValue(toKebab(k));
- if (v && v.trim()) return v.trim();
- } catch (_) {}
- try {
- if (k in t.style) return t.style[k];
- } catch (_) {}
- const attr = toKebab(k);
- if (t.hasAttribute(attr)) return t.getAttribute(attr);
- if (t.hasAttribute(k)) return t.getAttribute(k);
- }
-
- // Generic property
- return t[k];
- }
-
- _writeValue(k, v) {
- const t = this.target;
- // JS object
- if (!isEl(t) && !isSVG(t)) {
- t[k] = v;
- return;
- }
-
- // scroll
- if (k === 'scrollTop' || k === 'scrollLeft') {
- t[k] = v;
- return;
- }
-
- // CSS var
- if (isEl(t) && isCssVar(k)) {
- t.style.setProperty(k, v);
- return;
- }
-
- // style
- if (isEl(t) && (k in t.style)) {
- t.style[k] = v;
- return;
- }
-
- // SVG
- // Prefer style for presentation attrs so it can animate when svgDraw initialized styles.
- if (isSVG(t)) {
- if (k === 'd' || k === 'points') {
- t.setAttribute(k, v);
- return;
- }
- try {
- if (k in t.style) {
- t.style[k] = v;
- return;
- }
- } catch (_) {}
- const attr = toKebab(k);
- t.setAttribute(attr, v);
- return;
- }
-
- // Fallback for path/d/points even if isSVG check somehow failed
- if ((k === 'd' || k === 'points') && isEl(t)) {
- t.setAttribute(k, v);
- return;
- }
-
- // Generic property
- t[k] = v;
- }
-
- _buildTween() {
- const tween = {};
- Object.keys(this.propValues).forEach((k) => {
- const raw = this.propValues[k];
- const fromRaw = isArr(raw) ? raw[0] : this._readCurrentValue(k);
- const toRaw = isArr(raw) ? raw[1] : raw;
-
- const fromStr = isNil(fromRaw) ? '0' : ('' + fromRaw).trim();
- const toStr = isNil(toRaw) ? '0' : ('' + toRaw).trim();
- const fromScalar = isPureNumberLike(fromStr);
- const toScalar = isPureNumberLike(toStr);
- // numeric tween (with unit preservation) — only when BOTH sides are pure scalars.
- if (fromScalar && toScalar) {
- const fromNum = parseFloat(fromStr);
- const toNum = parseFloat(toStr);
- const unit = getUnit(toStr, k) ?? getUnit(fromStr, k) ?? '';
- tween[k] = { type: 'number', from: fromNum, to: toNum, unit };
- return;
- }
- // string number-template tween (SVG path `d`, `points`, etc.)
- const a = parseNumberTemplate(fromStr);
- const b = parseNumberTemplate(toStr);
- if (a.nums.length >= 2 && a.nums.length === b.nums.length && a.parts.length === b.parts.length) {
- const aStrs = extractNumberStrings(fromStr);
- const bStrs = extractNumberStrings(toStr);
- const decs = a.nums.map((_, i) => Math.max(countDecimals(aStrs[i]), countDecimals(bStrs[i])));
- tween[k] = { type: 'number-template', fromNums: a.nums, toNums: b.nums, parts: b.parts, decimals: decs };
- return;
- } else if ((k === 'd' || k === 'points') && (a.nums.length > 0 || b.nums.length > 0)) {
- console.warn(`[Animal.js] Morph mismatch for property "${k}".\nValues must have matching number count.`,
- { from: a.nums.length, to: b.nums.length, fromVal: fromStr, toVal: toStr }
- );
- }
- // Non-numeric: fall back to "switch" (still useful for seek endpoints)
- tween[k] = { type: 'discrete', from: fromStr, to: toStr };
- });
- return tween;
- }
-
- _apply(progress, time) {
- this._progress = clamp01(progress);
- Object.keys(this._tween).forEach((k) => {
- const t = this._tween[k];
- if (t.type === 'number') {
- const eased = this._ease ? this._ease(this._progress) : this._progress;
- const val = t.from + (t.to - t.from) * eased;
- if (!isEl(this.target) && !isSVG(this.target) && t.unit === '') {
- this._writeValue(k, val);
- } else {
- this._writeValue(k, (val + t.unit));
- }
- } else if (t.type === 'number-template') {
- const eased = this._ease ? this._ease(this._progress) : this._progress;
- const { fromNums, toNums, parts, decimals } = t;
- let out = parts[0] || '';
- for (let i = 0; i < fromNums.length; i++) {
- const v = fromNums[i] + (toNums[i] - fromNums[i]) * eased;
- out += formatInterpolatedNumber(v, decimals ? decimals[i] : 0);
- out += parts[i + 1] || '';
- }
- this._writeValue(k, out);
- } else {
- const val = this._progress >= 1 ? t.to : t.from;
- this._writeValue(k, val);
- }
- });
-
- if (this.update) this.update({ target: this.target, progress: this._progress, time });
- }
-
- seek(progress) {
- // Seek does not auto-play; it's intended for scroll-linked or manual control.
- const t = (typeof performance !== 'undefined' ? performance.now() : 0);
- this._apply(progress, t);
- }
-
- play() {
- if (this._cancelled) return;
- if (!this._started) {
- this._started = true;
- this._startTime = performance.now() + this.delay - (this._progress * this.duration);
- // begin fired on first active tick to avoid firing during delay.
- }
- this._paused = false;
- if (!this._running) {
- this._running = true;
- rafEngine.add(this);
- }
- }
-
- pause() {
- this._paused = true;
- }
-
- cancel() {
- this._cancelled = true;
- this._running = false;
- rafEngine.remove(this);
- // Resolve to avoid hanging awaits
- if (this._resolve) this._resolve();
- }
-
- finish() {
- this.seek(1);
- this._running = false;
- rafEngine.remove(this);
- if (this.complete) this.complete(this.target);
- if (this._resolve) this._resolve();
- }
-
- tick(now) {
- if (this._cancelled) return false;
- if (this._paused) return true;
-
- if (!this._started) {
- this._started = true;
- this._startTime = now + this.delay;
- }
-
- if (now < this._startTime) return true;
- if (!this._didBegin) {
- this._didBegin = true;
- if (this.begin) this.begin(this.target);
- }
-
- const totalDur = this.duration + (this.endDelay || 0);
- const elapsed = now - this._startTime;
- const iter = totalDur > 0 ? Math.floor(elapsed / totalDur) : 0;
- const inIter = totalDur > 0 ? (elapsed - iter * totalDur) : elapsed;
-
- const iterations = this.loop === true ? Infinity : this.loop;
- if (iterations !== Infinity && iter >= iterations) {
- this._apply(this._mapDirection(1, iterations - 1));
- this._running = false;
- if (this.complete) this.complete(this.target);
- if (this._resolve) this._resolve();
- return false;
- }
-
- // if we're in endDelay portion, hold the end state
- let p = clamp01(inIter / this.duration);
- if (this.duration <= 0) p = 1;
- if (this.endDelay && inIter > this.duration) p = 1;
-
- this._apply(this._mapDirection(p, iter), now);
-
- // Keep running until loops exhausted
- return true;
- }
-
- _mapDirection(p, iterIndex) {
- const dir = this.direction;
- const flip = (dir === 'reverse') || (dir === 'alternate-reverse');
- const isAlt = (dir === 'alternate') || (dir === 'alternate-reverse');
- let t = flip ? (1 - p) : p;
- if (isAlt && (iterIndex % 2 === 1)) t = 1 - t;
- return t;
- }
- }
-
- // --- Controls (Motion One-ish, chainable / thenable) ---
- class Controls {
- constructor({ waapi = [], js = [], finished }) {
- this.animations = waapi; // backward compat with old `.animations` usage
- this.jsAnimations = js;
- this.finished = finished || Promise.resolve();
- }
- then(onFulfilled, onRejected) { return this.finished.then(onFulfilled, onRejected); }
- catch(onRejected) { return this.finished.catch(onRejected); }
- finally(onFinally) { return this.finished.finally(onFinally); }
-
- play() {
- if (this._onPlay) this._onPlay.forEach((fn) => fn && fn());
- if (this._ensureWaapiUpdate) this._ensureWaapiUpdate();
- this.animations.forEach((a) => a && a.play && a.play());
- this.jsAnimations.forEach((a) => a && a.play && a.play());
- return this;
- }
- pause() {
- this.animations.forEach((a) => a && a.pause && a.pause());
- this.jsAnimations.forEach((a) => a && a.pause && a.pause());
- return this;
- }
- cancel() {
- this.animations.forEach((a) => a && a.cancel && a.cancel());
- this.jsAnimations.forEach((a) => a && a.cancel && a.cancel());
- return this;
- }
- finish() {
- this.animations.forEach((a) => a && a.finish && a.finish());
- this.jsAnimations.forEach((a) => a && a.finish && a.finish());
- return this;
- }
- seek(progress) {
- const p = clamp01(progress);
- const t = (typeof performance !== 'undefined' ? performance.now() : 0);
- this.animations.forEach((anim) => {
- if (anim && anim.effect) {
- let timing = this._waapiTimingByAnim && this._waapiTimingByAnim.get(anim);
- if (!timing) {
- timing = readWaapiEffectTiming(anim.effect);
- if (this._waapiTimingByAnim) this._waapiTimingByAnim.set(anim, timing);
- }
- const dur = timing.duration || 0;
- if (!dur) return;
- anim.currentTime = dur * p;
- const target = this._waapiTargetByAnim && this._waapiTargetByAnim.get(anim);
- if (this._fireUpdate && target) this._fireUpdate({ target, progress: p, time: t });
- }
- });
- this.jsAnimations.forEach((a) => a && a.seek && a.seek(p));
- return this;
- }
- }
- // --- Main Animation Logic ---
- function animate(targets, params) {
- const elements = toArray(targets);
- const safeParams = params || {};
- const optionsNamespace = (safeParams.options && typeof safeParams.options === 'object') ? safeParams.options : {};
- // `params.options` provides a safe namespace for config keys without risking them being treated as animated props.
- // Top-level keys still win for backward compatibility.
- const merged = { ...optionsNamespace, ...safeParams };
- const {
- duration = 1000,
- delay = 0,
- easing = 'ease-out',
- direction = 'normal',
- fill = 'forwards',
- loop = 1,
- endDelay = 0,
- autoplay = true,
- springFrames = 120,
- update, // callback
- begin, // callback
- complete // callback
- } = merged;
- let isSpring = false;
- let springValuesRaw = null;
- let springValuesSampled = null;
- let springDurationMs = null;
- if (typeof easing === 'object' && !Array.isArray(easing)) {
- isSpring = true;
- springValuesRaw = getSpringValues(easing);
- const frames = (typeof springFrames === 'number' && springFrames > 1) ? Math.floor(springFrames) : 120;
- springValuesSampled = downsample(springValuesRaw, frames);
- springDurationMs = springValuesRaw.length * 1000 / 60;
- }
-
- // Callback aggregation (avoid double-calling when WAAPI+JS both run)
- const cbState = typeof WeakMap !== 'undefined' ? new WeakMap() : null;
- const getState = (t) => {
- if (!cbState) return { begun: false, completed: false, lastUpdateBucket: -1 };
- let s = cbState.get(t);
- if (!s) {
- s = { begun: false, completed: false, lastUpdateBucket: -1 };
- cbState.set(t, s);
- }
- return s;
- };
- const fireBegin = (t) => {
- if (!begin) return;
- const s = getState(t);
- if (s.begun) return;
- s.begun = true;
- begin(t);
- };
- const fireComplete = (t) => {
- if (!complete) return;
- const s = getState(t);
- if (s.completed) return;
- s.completed = true;
- complete(t);
- };
- const fireUpdate = (payload) => {
- if (!update) return;
- const t = payload && payload.target;
- const time = payload && payload.time;
- if (!t) return update(payload);
- const s = getState(t);
- const bucket = Math.floor(((typeof time === 'number' ? time : (typeof performance !== 'undefined' ? performance.now() : 0))) / 16);
- if (bucket === s.lastUpdateBucket) return;
- s.lastUpdateBucket = bucket;
- update(payload);
- };
- const propEntries = [];
- Object.keys(safeParams).forEach((key) => {
- if (key === 'options') return;
- if (isAnimOptionKey(key)) return;
- propEntries.push({ key, canonical: ALIASES[key] || key, val: safeParams[key] });
- });
- // Create animations but don't play if autoplay is false
- const waapiAnimations = [];
- const jsAnimations = [];
- const engineInfo = {
- isSpring,
- waapiKeys: [],
- jsKeys: []
- };
- const waapiKeySet = new Set();
- const jsKeySet = new Set();
-
- const onPlayHooks = [];
- const waapiUpdateItems = [];
- const waapiTargetByAnim = (typeof WeakMap !== 'undefined') ? new WeakMap() : null;
- const waapiTimingByAnim = (typeof WeakMap !== 'undefined') ? new WeakMap() : null;
- let waapiUpdateStarted = false;
- const ensureWaapiUpdate = () => {
- if (waapiUpdateStarted) return;
- waapiUpdateStarted = true;
- waapiUpdateItems.forEach((item) => waapiUpdateSampler.add(item));
- };
- const promises = elements.map((el) => {
- // Route props per target (fix mixed HTML/SVG/object target arrays).
- const waapiProps = {};
- const jsProps = {};
- propEntries.forEach(({ key, canonical, val }) => {
- const isTransform = TRANSFORMS.includes(canonical) || TRANSFORMS.includes(key);
- if (!el || (!isEl(el) && !isSVG(el))) {
- jsProps[key] = val;
- jsKeySet.add(key);
- return;
- }
- const isSvgTarget = isSVG(el);
- if (isSvgTarget) {
- if (isTransform || key === 'opacity' || key === 'filter') {
- waapiProps[canonical] = val;
- waapiKeySet.add(canonical);
- } else {
- jsProps[key] = val;
- jsKeySet.add(key);
- }
- return;
- }
- // HTML element
- if (isCssVar(key)) {
- jsProps[key] = val;
- jsKeySet.add(key);
- return;
- }
- const isCssLike = isTransform || key === 'opacity' || key === 'filter' || (isEl(el) && (key in el.style));
- if (isCssLike) {
- waapiProps[canonical] = val;
- waapiKeySet.add(canonical);
- } else {
- jsProps[key] = val;
- jsKeySet.add(key);
- }
- });
- // 1. WAAPI Animation
- let waapiAnim = null;
- let waapiPromise = Promise.resolve();
-
- if (Object.keys(waapiProps).length > 0) {
- const buildFrames = (propValues) => {
- // Spring: share sampled progress; per-target we only compute start/end once per prop.
- if (isSpring && springValuesSampled && springValuesSampled.length) {
- const cs = (isEl(el) && typeof getComputedStyle !== 'undefined') ? getComputedStyle(el) : null;
- const metas = Object.keys(propValues).map((k) => {
- const raw = propValues[k];
- const rawFrom = Array.isArray(raw) ? raw[0] : undefined;
- const rawTo = Array.isArray(raw) ? raw[1] : raw;
- const toNum = parseFloat(('' + rawTo).trim());
- const fromNumExplicit = Array.isArray(raw) ? parseFloat(('' + rawFrom).trim()) : NaN;
- const isT = TRANSFORMS.includes(ALIASES[k] || k) || TRANSFORMS.includes(k);
- const unit = getUnit(('' + rawTo), k) ?? getUnit(('' + rawFrom), k) ?? getDefaultUnit(k);
-
- let fromNum = 0;
- if (Number.isFinite(fromNumExplicit)) {
- fromNum = fromNumExplicit;
- } else if (k.startsWith('scale')) {
- fromNum = 1;
- } else if (k === 'opacity' && cs) {
- const n = parseFloat(cs.opacity);
- if (!Number.isNaN(n)) fromNum = n;
- } else if (!isT && cs) {
- const cssVal = cs.getPropertyValue(toKebab(k));
- const n = parseFloat(cssVal);
- if (!Number.isNaN(n)) fromNum = n;
- }
-
- const to = Number.isFinite(toNum) ? toNum : 0;
- return { k, isT, unit: unit ?? '', from: fromNum, to };
- });
-
- const frames = new Array(springValuesSampled.length);
- for (let i = 0; i < springValuesSampled.length; i++) {
- const v = springValuesSampled[i];
- const frame = {};
- let transformStr = '';
- for (let j = 0; j < metas.length; j++) {
- const m = metas[j];
- const current = m.from + (m.to - m.from) * v;
- const outVal = (m.unit === '' ? ('' + current) : (current + m.unit));
- if (m.isT) transformStr += `${m.k}(${outVal}) `;
- else frame[m.k] = outVal;
- }
- if (transformStr) frame.transform = transformStr.trim();
- frames[i] = frame;
- }
- if (frames[0] && Object.keys(frames[0]).length === 0) frames.shift();
- return frames;
- }
-
- // Non-spring: 2-keyframe path
- const frame0 = {};
- const frame1 = {};
- let transform0 = '';
- let transform1 = '';
- Object.keys(propValues).forEach((k) => {
- const val = propValues[k];
- const isT = TRANSFORMS.includes(ALIASES[k] || k) || TRANSFORMS.includes(k);
- if (Array.isArray(val)) {
- const from = isT ? normalizeTransformPartValue(k, val[0]) : val[0];
- const to = isT ? normalizeTransformPartValue(k, val[1]) : val[1];
- if (isT) {
- transform0 += `${k}(${from}) `;
- transform1 += `${k}(${to}) `;
- } else {
- frame0[k] = from;
- frame1[k] = to;
- }
- } else {
- if (isT) transform1 += `${k}(${normalizeTransformPartValue(k, val)}) `;
- else frame1[k] = val;
- }
- });
- if (transform0) frame0.transform = transform0.trim();
- if (transform1) frame1.transform = transform1.trim();
- const out = [frame0, frame1];
- if (Object.keys(out[0]).length === 0) out.shift();
- return out;
- };
- const finalFrames = buildFrames(waapiProps);
- const opts = {
- duration: isSpring ? springDurationMs : duration,
- delay,
- fill,
- iterations: loop,
- easing: isSpring ? 'linear' : easing,
- direction,
- endDelay
- };
- const animation = el.animate(finalFrames, opts);
- if (!autoplay) animation.pause();
- waapiAnim = animation;
- waapiPromise = animation.finished;
- waapiAnimations.push(waapiAnim);
- if (waapiTargetByAnim) waapiTargetByAnim.set(waapiAnim, el);
- if (waapiTimingByAnim) waapiTimingByAnim.set(waapiAnim, readWaapiEffectTiming(waapiAnim.effect));
-
- if (begin) {
- // Fire begin when play starts (and also for autoplay on next frame)
- onPlayHooks.push(() => fireBegin(el));
- if (autoplay && typeof requestAnimationFrame !== 'undefined') requestAnimationFrame(() => fireBegin(el));
- }
- if (complete) {
- waapiAnim.addEventListener?.('finish', () => fireComplete(el));
- }
- if (update) {
- const timing = (waapiTimingByAnim && waapiTimingByAnim.get(waapiAnim)) || readWaapiEffectTiming(waapiAnim.effect);
- const item = { anim: waapiAnim, target: el, update: fireUpdate, _lastP: null, _dur: timing.duration || 0 };
- waapiUpdateItems.push(item);
- // Ensure removal on finish/cancel
- waapiAnim.addEventListener?.('finish', () => waapiUpdateSampler.remove(item));
- waapiAnim.addEventListener?.('cancel', () => waapiUpdateSampler.remove(item));
- // Start sampler only when needed (autoplay or explicit play)
- if (autoplay) ensureWaapiUpdate();
- }
- }
- // 2. JS Animation (Fallback / Attributes)
- let jsPromise = Promise.resolve();
- if (Object.keys(jsProps).length > 0) {
- const jsAnim = new JsAnimation(
- el,
- jsProps,
- { duration, delay, easing, autoplay, direction, loop, endDelay, springValues: isSpring ? springValuesRaw : null },
- { update: fireUpdate, begin: fireBegin, complete: fireComplete }
- );
- jsAnimations.push(jsAnim);
- jsPromise = jsAnim.finished;
- }
- return Promise.all([waapiPromise, jsPromise]);
- });
- const finished = Promise.all(promises);
- const controls = new Controls({ waapi: waapiAnimations, js: jsAnimations, finished });
- controls.engine = engineInfo;
- controls._onPlay = onPlayHooks;
- controls._fireUpdate = fireUpdate;
- controls._waapiTargetByAnim = waapiTargetByAnim;
- controls._waapiTimingByAnim = waapiTimingByAnim;
- controls._ensureWaapiUpdate = update ? ensureWaapiUpdate : null;
- if (!autoplay) controls.pause();
- engineInfo.waapiKeys = Array.from(waapiKeySet);
- engineInfo.jsKeys = Array.from(jsKeySet);
- return controls;
- }
- // --- SVG Draw ---
- function svgDraw(targets, params = {}) {
- const elements = toArray(targets);
- elements.forEach(el => {
- if (!isSVG(el)) return;
- const len = el.getTotalLength ? el.getTotalLength() : 0;
- el.style.strokeDasharray = len;
- el.style.strokeDashoffset = len;
-
- animate(el, {
- strokeDashoffset: [len, 0],
- ...params
- });
- });
- }
- // --- In View ---
- function inViewAnimate(targets, params, options = {}) {
- const elements = toArray(targets);
- const observer = new IntersectionObserver((entries) => {
- entries.forEach(entry => {
- if (entry.isIntersecting) {
- animate(entry.target, params);
- if (options.once !== false) observer.unobserve(entry.target);
- }
- });
- }, { threshold: options.threshold || 0.1 });
-
- elements.forEach(el => observer.observe(el));
- // Return cleanup so callers can disconnect observers in long-lived pages (esp. once:false).
- return () => {
- try {
- elements.forEach((el) => observer.unobserve(el));
- observer.disconnect();
- } catch (e) {
- // ignore
- }
- };
- }
-
- // --- Scroll Linked ---
- function scroll(animationPromise, options = {}) {
- // options: container (default window), range [start, end] (default viewport logic)
- const container = options.container || window;
- const target = options.target || document.body; // Element to track for progress
- // If passing an animation promise, we control its WAAPI animations
-
- const controls = animationPromise;
- // Back-compat: old return value was a Promise with `.animations`
- const hasSeek = controls && isFunc(controls.seek);
- const anims = (controls && controls.animations) || (animationPromise && animationPromise.animations) || [];
- const jsAnims = (controls && controls.jsAnimations) || [];
- if (!hasSeek && !anims.length && !jsAnims.length) return;
-
- // Cache WAAPI timing per animation to avoid repeated getComputedTiming() during scroll.
- const timingCache = (typeof WeakMap !== 'undefined') ? new WeakMap() : null;
- const getEndTime = (anim) => {
- if (!anim || !anim.effect) return 0;
- if (timingCache) {
- const cached = timingCache.get(anim);
- if (cached) return cached;
- }
- const timing = readWaapiEffectTiming(anim.effect);
- const end = timing.duration || 0;
- if (timingCache && end) timingCache.set(anim, end);
- return end;
- };
- const updateScroll = () => {
- let progress = 0;
-
- if (container === window) {
- const scrollY = window.scrollY;
- const winH = window.innerHeight;
- const docH = document.body.scrollHeight;
-
- // Simple progress: how far down the page (0 to 1)
- // Or element based?
- // Motion One defaults to element entering view.
-
- if (options.target) {
- const rect = options.target.getBoundingClientRect();
- const start = winH;
- const end = -rect.height;
- // progress 0 when rect.top == start (just entering)
- // progress 1 when rect.top == end (just left)
-
- const totalDistance = start - end;
- const currentDistance = start - rect.top;
- progress = currentDistance / totalDistance;
- } else {
- // Whole page scroll
- progress = scrollY / (docH - winH);
- }
- } else if (container && (typeof Element !== 'undefined') && (container instanceof Element)) {
- // Scroll container progress
- const el = container;
- const scrollTop = el.scrollTop;
- const max = (el.scrollHeight - el.clientHeight) || 1;
- if (options.target) {
- const containerRect = el.getBoundingClientRect();
- const rect = options.target.getBoundingClientRect();
- const start = containerRect.height;
- const end = -rect.height;
- const totalDistance = start - end;
- const currentDistance = start - (rect.top - containerRect.top);
- progress = currentDistance / totalDistance;
- } else {
- progress = scrollTop / max;
- }
- }
-
- // Clamp
- progress = clamp01(progress);
-
- if (hasSeek) {
- controls.seek(progress);
- return;
- }
-
- anims.forEach((anim) => {
- if (anim.effect) {
- const end = getEndTime(anim);
- if (!end) return;
- anim.currentTime = end * progress;
- }
- });
- };
- let rafId = 0;
- const onScroll = () => {
- if (rafId) return;
- rafId = requestAnimationFrame(() => {
- rafId = 0;
- updateScroll();
- });
- };
-
- const eventTarget = (container && container.addEventListener) ? container : window;
- eventTarget.addEventListener('scroll', onScroll, { passive: true });
- updateScroll(); // Initial
-
- return () => {
- if (rafId) cancelAnimationFrame(rafId);
- eventTarget.removeEventListener('scroll', onScroll);
- };
- }
- // --- Timeline ---
- function timeline(defaults = {}) {
- const steps = [];
- const api = {
- currentTime: 0,
- add: (targets, params, offset) => {
- const animParams = { ...defaults, ...params };
- let start = api.currentTime;
-
- if (offset !== undefined) {
- if (isStr(offset) && offset.startsWith('-=')) start -= parseFloat(offset.slice(2));
- else if (isStr(offset) && offset.startsWith('+=')) start += parseFloat(offset.slice(2));
- else if (typeof offset === 'number') start = offset;
- }
-
- const dur = animParams.duration || 1000;
- const step = { targets, animParams, start, _scheduled: false };
- steps.push(step);
-
- // Backward compatible: schedule immediately (existing docs rely on this)
- if (start <= 0) {
- animate(targets, animParams);
- step._scheduled = true;
- } else {
- setTimeout(() => {
- animate(targets, animParams);
- }, start);
- step._scheduled = true;
- }
-
- api.currentTime = Math.max(api.currentTime, start + dur);
- return api;
- },
- // Optional: if you create a timeline and want to defer scheduling yourself
- play: () => {
- steps.forEach((s) => {
- if (s._scheduled) return;
- if (s.start <= 0) animate(s.targets, s.animParams);
- else setTimeout(() => animate(s.targets, s.animParams), s.start);
- s._scheduled = true;
- });
- return api;
- }
- };
- return api;
- }
- // --- Export ---
- // transform `$` to be the main export `animal`, with statics attached
- const animal = $;
-
- // Extend $ behavior to act as a global selector and property accessor
- Object.assign(animal, {
- animate,
- timeline,
- draw: svgDraw,
- svgDraw,
- inViewAnimate,
- spring,
- scroll,
- $: animal // Self-reference for backward compatibility
- });
- // Expose Layer if available or allow lazy loading
- Object.defineProperty(animal, 'Layer', {
- get: () => {
- return (typeof window !== 'undefined' && window.Layer) ? window.Layer :
- (typeof globalThis !== 'undefined' && globalThis.Layer) ? globalThis.Layer :
- (typeof Layer !== 'undefined' ? Layer : null);
- }
- });
- // Shortcut for Layer.fire or new Layer()
- // Allows $.fire({ title: 'Hi' }) or $.layer({ title: 'Hi' })
- animal.fire = (options) => {
- const L = animal.Layer;
- if (L) return (L.fire ? L.fire(options) : new L(options).fire());
- console.warn('Layer module not loaded.');
- return Promise.reject('Layer module not loaded');
- };
- // 'layer' alias for static usage
- animal.layer = animal.fire;
- return animal;
- })));
- // Unified entrypoint
- if (__GLOBAL__ && __GLOBAL__.animal) {
- __GLOBAL__.xjs = __GLOBAL__.animal;
- }
- // Optional jQuery-like alias (opt-in, and won't clobber existing $)
- try {
- if (__GLOBAL__ && __GLOBAL__.XJS_GLOBAL_DOLLAR === true && !__GLOBAL__.$ && __GLOBAL__.xjs) {
- __GLOBAL__.$ = __GLOBAL__.xjs;
- }
- } catch (_) {}
- })();
|