// 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: // // 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 (_) {} })();