|
@@ -0,0 +1,2240 @@
|
|
|
|
|
+// xjs.js - combined single-file build (animal + layer)
|
|
|
|
|
+// Generated by `go run main.go build`.
|
|
|
|
|
+// NOTE: Do not edit this file directly in development.
|
|
|
|
|
+// Edit animal.js / layer.js instead.
|
|
|
|
|
+
|
|
|
|
|
+/* --- 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);
|
|
|
|
|
+}(this, (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;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /* 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; }
|
|
|
|
|
+
|
|
|
|
|
+ .${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-success-circular-line 4.25s ease-in;
|
|
|
|
|
+ }
|
|
|
|
|
+ /* Warning needs a shorter arc (45deg) */
|
|
|
|
|
+ .${PREFIX}icon.warning.${PREFIX}icon-show .${PREFIX}success-circular-line-right {
|
|
|
|
|
+ animation: ${PREFIX}rotate-warning-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-success-circular-line {
|
|
|
|
|
+ 0% { transform: rotate(-45deg); }
|
|
|
|
|
+ 5% { transform: rotate(-45deg); }
|
|
|
|
|
+ 12% { transform: rotate(-405deg); }
|
|
|
|
|
+ 100% { transform: rotate(-405deg); }
|
|
|
|
|
+ }
|
|
|
|
|
+ @keyframes ${PREFIX}rotate-warning-circular-line {
|
|
|
|
|
+ 0% { transform: rotate(-45deg); }
|
|
|
|
|
+ 5% { transform: rotate(-45deg); }
|
|
|
|
|
+ 12% { transform: rotate(-180deg); }
|
|
|
|
|
+ 100% { transform: rotate(-180deg); } /* match 12% so it "sweeps then stops" like success */
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /* (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
|
|
|
|
|
+ // <div class="...success-circular-line-left"></div>
|
|
|
|
|
+ // <span class="...success-line-tip"></span>
|
|
|
|
|
+ // <span class="...success-line-long"></span>
|
|
|
|
|
+ // <div class="...success-ring"></div>
|
|
|
|
|
+ // <div class="...success-fix"></div>
|
|
|
|
|
+ // <div class="...success-circular-line-right"></div>
|
|
|
|
|
+ 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;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Default to SVG icons for other/custom types
|
|
|
|
|
+ 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') {
|
|
|
|
|
+ // Question mark (single stroke + dot)
|
|
|
|
|
+ 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 {
|
|
|
|
|
+ // Fallback: 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) {
|
|
|
|
|
+ // Override the CSS scale transition with a spring-ish entrance.
|
|
|
|
|
+ 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 {}
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Icon SVG draw animation
|
|
|
|
|
+ if (this.params.iconAnimation) {
|
|
|
|
|
+ this._animateIcon();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // User hook (SweetAlert-ish naming)
|
|
|
|
|
+ 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, we unify the "ring first, then mark" order.
|
|
|
|
|
+ const baseDelay = ringTypes.has(type) ? 180 : 0;
|
|
|
|
|
+
|
|
|
|
|
+ if (type === 'error') {
|
|
|
|
|
+ const left = marks[0];
|
|
|
|
|
+ const right = marks[1];
|
|
|
|
|
+ try { if (left) X(left).draw({ duration: 320, easing: 'ease-out', delay: baseDelay }); } catch {}
|
|
|
|
|
+ try { if (right) X(right).draw({ duration: 320, easing: 'ease-out', delay: baseDelay + 70 }); } catch {}
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // warning / info / question (single stroke) or custom SVG symbols
|
|
|
|
|
+ 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';
|
|
|
|
|
+ // Keep dot pop after the ring begins
|
|
|
|
|
+ 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());
|
|
|
|
|
+}(this, (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;
|
|
|
|
|
+ // Strip exponent part for decimal count
|
|
|
|
|
+ const e = s.search(/[eE]/);
|
|
|
|
|
+ const end = e >= 0 ? e : s.length;
|
|
|
|
|
+ const frac = s.slice(dot + 1, end);
|
|
|
|
|
+ // Ignore trailing zeros for display
|
|
|
|
|
+ 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) {
|
|
|
|
|
+ // Use a fresh regex to avoid any `lastIndex` surprises.
|
|
|
|
|
+ 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));
|
|
|
|
|
+ // Use fixed, then strip trailing zeros/dot to keep strings compact.
|
|
|
|
|
+ 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.)
|
|
|
|
|
+ // Requires both strings to have the same count of numeric tokens (>= 2).
|
|
|
|
|
+ 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;
|
|
|
|
|
+
|
|
|
|
|
+})));
|
|
|
|
|
+
|
|
|
|
|
+;(function(g){
|
|
|
|
|
+ try { if (g && g.animal && !g.xjs) g.xjs = g.animal; } catch {}
|
|
|
|
|
+ try { if (g && g.XJS_GLOBAL_DOLLAR && !g.$ && g.xjs) g.$ = g.xjs; } catch {}
|
|
|
|
|
+})(typeof window !== 'undefined' ? window : (typeof globalThis !== 'undefined' ? globalThis : this));
|